Skip to content

Commit 954f238

Browse files
committed
gui: serve country code
1 parent a8a9baa commit 954f238

File tree

12 files changed

+317
-33
lines changed

12 files changed

+317
-33
lines changed

book/api/websocket.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1400,7 +1400,8 @@ identity is no longer in these three data sources, it will be removed.
14001400
"gossip": "93.119.195.160:8001",
14011401
"tpu": "192.64.85.26:8000",
14021402
// ... other sockets ...
1403-
}
1403+
},
1404+
"country_code": "CN"
14041405
},
14051406
"vote": [
14061407
{
@@ -1438,6 +1439,7 @@ identity is no longer in these three data sources, it will be removed.
14381439
| version | `string\|null` | Software version being advertised by the validator. Might be `null` if the validator is not gossiping a version, or we have received the contact information but not the version yet. The version string, if not null, will always be formatted like `major`.`minor`.`patch` where `major`, `minor`, and `patch` are `u16`s |
14391440
| feature_set | `number\|null` | First four bytes of the `FeatureSet` hash interpreted as a little endian `u32`. Might be `null` if the validator is not gossiping a feature set, or we have received the contact information but not the feature set yet |
14401441
| sockets | `[key: string]: string` | A dictionary of sockets that are advertised by the validator. `key` will be one of gossip `serve_repair_quic`, `rpc`, `rpc_pubsub`, `serve_repair`, `tpu`, `tpu_forwards`, `tpu_forwards_quic`, `tpu_quic`, `tpu_vote`, `tvu`, `tvu_quic`, `tpu_vote_quic`, or `alpenglow`. The value is an address like `<addr>:<port>`: the location to send traffic to for this validator with the given protocol. Address might be either an IPv4 or an IPv6 address |
1442+
| country_code | `string\|null` | ISO 3166-1 alpha-2 country code of where the validator is located, determined by GeoIP lookup on the gossip IP address. Country code may not be correct and is a best estimate. If no country code could be determined, will be `null`. |
14411443

14421444
**`PeerUpdateVoteAccount`**
14431445
| Field | Type | Description |

src/disco/gui/Local.mk

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,4 @@ FD_GUI_FRONTEND_DEV_GZ_FILES := $(patsubst src/disco/gui/dist_dev/%, src/disco/g
4141
FD_GUI_FRONTEND_DEV_ZST_FILES := $(patsubst src/disco/gui/dist_dev/%, src/disco/gui/dist_dev_cmp/%.zst, $(FD_GUI_FRONTEND_DEV_FILES))
4242

4343
$(OBJDIR)/obj/disco/gui/generated/http_import_dist.d: $(FD_GUI_FRONTEND_STABLE_GZ_FILES) $(FD_GUI_FRONTEND_STABLE_ZST_FILES) $(FD_GUI_FRONTEND_ALPHA_GZ_FILES) $(FD_GUI_FRONTEND_ALPHA_ZST_FILES) $(FD_GUI_FRONTEND_DEV_GZ_FILES) $(FD_GUI_FRONTEND_DEV_ZST_FILES)
44+
$(OBJDIR)/obj/disco/gui/fd_gui.d: src/disco/gui/ipinfo.bin.zstd

src/disco/gui/convert_ipinfo.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#!/usr/bin/env python3
2+
import struct
3+
import ipaddress
4+
import csv
5+
import argparse
6+
import zstandard as zstd
7+
8+
def main():
9+
parser = argparse.ArgumentParser(description='Convert IPInfo CSV to binary format')
10+
parser.add_argument('input', help='Input CSV file path')
11+
parser.add_argument('output', help='Output binary file path')
12+
args = parser.parse_args()
13+
14+
country_codes = set()
15+
with open(args.input, 'r') as r:
16+
reader = csv.DictReader(r)
17+
for row in reader:
18+
try:
19+
ipaddress.IPv4Network(row['network'])
20+
except ipaddress.AddressValueError:
21+
continue
22+
assert len(row['country_code']) == 2
23+
country_codes.add(row['country_code'])
24+
25+
assert len(country_codes) < 256, f"Too many country codes ({len(country_codes)}) to fit in a byte (max 255)"
26+
27+
country_to_index = {cc: idx for idx, cc in enumerate(sorted(country_codes))}
28+
29+
with open(args.input, 'r') as r:
30+
reader = csv.DictReader(r)
31+
with open(args.output, 'wb') as f:
32+
cctx = zstd.ZstdCompressor(level=22)
33+
with cctx.stream_writer(f) as w:
34+
w.write(struct.pack('<Q', len(country_codes)))
35+
for cc in sorted(country_codes):
36+
w.write(cc.encode('ascii'))
37+
38+
records = 0
39+
for row in reader:
40+
try:
41+
network = ipaddress.IPv4Network(row['network'])
42+
except ipaddress.AddressValueError:
43+
continue
44+
45+
country_idx = country_to_index[row['country_code']]
46+
47+
w.write(struct.pack('>I', int(network.network_address)))
48+
w.write(struct.pack('<B', network.prefixlen))
49+
w.write(struct.pack('<B', country_idx))
50+
records += 1
51+
52+
print(f"Converted {records} records with {len(country_codes)} country codes to {args.output}")
53+
54+
if __name__ == "__main__":
55+
main()

src/disco/gui/dump.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import asyncio
2+
import websockets
3+
import json
4+
5+
async def dump_messages(uri):
6+
async with websockets.connect(uri, max_size=1_000_000_000) as websocket:
7+
while True:
8+
frame = await websocket.recv()
9+
print(json.dumps(json.loads(frame)))
10+
11+
asyncio.get_event_loop().run_until_complete(dump_messages('ws://localhost:80/websocket'))

src/disco/gui/fd_gui.c

Lines changed: 105 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include "fd_gui.h"
2+
#include "fd_gui_peers.h"
23
#include "fd_gui_printf.h"
34

45
#include "../metrics/fd_metrics.h"
@@ -10,18 +11,111 @@
1011
#include "../../disco/pack/fd_pack.h"
1112
#include "../../disco/pack/fd_pack_cost.h"
1213

14+
FD_IMPORT_BINARY( ipinfo, "src/disco/gui/ipinfo.bin.zstd" );
15+
1316
#include <stdio.h>
1417

18+
#if FD_HAS_ZSTD
19+
#define FD_HTTP_ZSTD_COMPRESSION_LEVEL 3
20+
#define ZSTD_STATIC_LINKING_ONLY
21+
#include <zstd.h>
22+
/* TODO: Just use a small buffer with streaming decompression */
23+
#define IPINFO_DECOMPRESSED_SZ (1UL<<24UL) /* 16 MiB */
24+
#define IPINFO_MAX_NODES (1UL<<22UL) /* 4M nodes */
25+
#endif
26+
1527
FD_FN_CONST ulong
1628
fd_gui_align( void ) {
1729
return 128UL;
1830
}
1931

2032
FD_FN_CONST ulong
2133
fd_gui_footprint( void ) {
22-
return sizeof(fd_gui_t);
34+
ulong l = FD_LAYOUT_INIT;
35+
l = FD_LAYOUT_APPEND( l, fd_gui_align(), sizeof( fd_gui_t ) );
36+
#if FD_HAS_ZSTD
37+
l = FD_LAYOUT_APPEND( l, 1UL, IPINFO_DECOMPRESSED_SZ );
38+
l = FD_LAYOUT_APPEND( l, alignof(fd_gui_ipinfo_node_t), sizeof(fd_gui_ipinfo_node_t)*IPINFO_MAX_NODES );
39+
#endif
40+
return FD_LAYOUT_FINI( l, fd_gui_align() );
2341
}
2442

43+
#if FD_HAS_ZSTD
44+
45+
static void
46+
build_ipinfo_trie( fd_gui_t * gui,
47+
uchar * ipinfo_buffer,
48+
fd_gui_ipinfo_node_t * nodes ) {
49+
gui->ipinfo.nodes = nodes;
50+
51+
ulong actual_sz = ZSTD_decompress( ipinfo_buffer, IPINFO_DECOMPRESSED_SZ, ipinfo, ipinfo_sz );
52+
FD_TEST( !ZSTD_isError( actual_sz ) );
53+
FD_TEST( actual_sz>8UL );
54+
55+
ulong country_code_cnt = FD_LOAD( ulong, ipinfo_buffer );
56+
FD_TEST( country_code_cnt && country_code_cnt<256UL ); /* 256 reserved for unknown */
57+
FD_TEST( actual_sz>=8UL+country_code_cnt*2UL );
58+
59+
for( ulong i=0UL; i<country_code_cnt; i++ ) {
60+
fd_memcpy( gui->ipinfo.country_code[ i ], ipinfo_buffer+8UL+i*2UL, 2UL );
61+
gui->ipinfo.country_code[ i ][ 2 ] = '\0';
62+
}
63+
64+
ulong processed = 8UL+country_code_cnt*2UL;
65+
FD_TEST( !((actual_sz-processed)%6UL) );
66+
FD_TEST( (actual_sz-processed)/6UL<=IPINFO_MAX_NODES-1UL );
67+
68+
fd_gui_ipinfo_node_t * root = &nodes[ 0 ];
69+
root->left = NULL;
70+
root->right = NULL;
71+
root->has_prefix = 0;
72+
73+
ulong node_cnt = 1UL;
74+
while( processed<actual_sz ) {
75+
uint ip_addr = fd_uint_bswap( FD_LOAD( uint, ipinfo_buffer+processed ) );
76+
uchar prefix_len = *( ipinfo_buffer+processed+4UL );
77+
FD_TEST( prefix_len<=32UL );
78+
uchar country_idx = *( ipinfo_buffer+processed+5UL );
79+
FD_TEST( country_idx<country_code_cnt );
80+
81+
fd_gui_ipinfo_node_t * node = root;
82+
for( uchar bit_pos=0; bit_pos<prefix_len; bit_pos++ ) {
83+
uchar bit = (ip_addr >> (31 - bit_pos)) & 1;
84+
85+
fd_gui_ipinfo_node_t * child;
86+
if( FD_LIKELY( !bit ) ) {
87+
child = node->left;
88+
if( FD_LIKELY( !child ) ) {
89+
FD_TEST( node_cnt<IPINFO_MAX_NODES );
90+
child = &nodes[ node_cnt++ ];
91+
child->left = NULL;
92+
child->right = NULL;
93+
child->has_prefix = 0;
94+
node->left = child;
95+
}
96+
} else {
97+
child = node->right;
98+
if( FD_LIKELY( !child ) ) {
99+
FD_TEST( node_cnt<IPINFO_MAX_NODES );
100+
child = &nodes[ node_cnt++ ];
101+
child->left = NULL;
102+
child->right = NULL;
103+
child->has_prefix = 0;
104+
node->right = child;
105+
}
106+
}
107+
node = child;
108+
}
109+
110+
node->has_prefix = 1;
111+
node->country_code_idx = country_idx;
112+
113+
processed += 6UL;
114+
}
115+
}
116+
117+
#endif
118+
25119
void *
26120
fd_gui_new( void * shmem,
27121
fd_http_server_t * http,
@@ -52,7 +146,12 @@ fd_gui_new( void * shmem,
52146
return NULL;
53147
}
54148

55-
fd_gui_t * gui = (fd_gui_t *)shmem;
149+
FD_SCRATCH_ALLOC_INIT( l, shmem );
150+
fd_gui_t * gui = FD_SCRATCH_ALLOC_APPEND( l, fd_gui_align(), sizeof(fd_gui_t) );
151+
#if FD_HAS_ZSTD
152+
uchar * _ipinfo_buffer = FD_SCRATCH_ALLOC_APPEND( l, 1UL, IPINFO_DECOMPRESSED_SZ );
153+
fd_gui_ipinfo_node_t * _nodes = FD_SCRATCH_ALLOC_APPEND( l, alignof(fd_gui_ipinfo_node_t), sizeof(fd_gui_ipinfo_node_t)*IPINFO_MAX_NODES );
154+
#endif
56155

57156
gui->http = http;
58157
gui->topo = topo;
@@ -154,7 +253,6 @@ fd_gui_new( void * shmem,
154253
for( ulong i=0UL; i<FD_GUI_LEADER_CNT; i++ ) gui->leader_slots[ i ]->slot = ULONG_MAX;
155254
gui->leader_slots_cnt = 0UL;
156255

157-
158256
gui->block_engine.has_block_engine = 0;
159257

160258
gui->epoch.has_epoch[ 0 ] = 0;
@@ -174,6 +272,10 @@ fd_gui_new( void * shmem,
174272
gui->summary.catch_up_repair_sz = 0UL;
175273
gui->summary.catch_up_turbine_sz = 0UL;
176274

275+
#if FD_HAS_ZSTD
276+
build_ipinfo_trie( gui, _ipinfo_buffer, _nodes );
277+
#endif
278+
177279
return gui;
178280
}
179281

src/disco/gui/fd_gui.h

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -372,8 +372,8 @@ struct fd_gui_slot_rankings {
372372
typedef struct fd_gui_slot_rankings fd_gui_slot_rankings_t;
373373

374374
struct fd_gui_ephemeral_slot {
375-
ulong slot; /* ULONG_MAX indicates invalid/evicted */
376-
long timestamp_arrival_nanos;
375+
ulong slot; /* ULONG_MAX indicates invalid/evicted */
376+
long timestamp_arrival_nanos;
377377
};
378378
typedef struct fd_gui_ephemeral_slot fd_gui_ephemeral_slot_t;
379379

@@ -650,6 +650,11 @@ struct fd_gui {
650650
fd_gui_scheduler_counts_t scheduler_counts_snap[ FD_GUI_SCHEDULER_COUNT_SNAP_CNT ][ 1 ];
651651
} summary;
652652

653+
struct {
654+
fd_gui_ipinfo_node_t * nodes;
655+
char country_code[ 512 ][ 3 ]; /* ISO 3166-1 alpha-2 country codes */
656+
} ipinfo;
657+
653658
fd_gui_slot_t slots[ FD_GUI_SLOTS_CNT ][ 1 ];
654659

655660
fd_gui_leader_slot_t leader_slots[ FD_GUI_LEADER_CNT ][ 1 ];

0 commit comments

Comments
 (0)