Skip to content

Commit fbd2366

Browse files
committed
gui: serve country code
1 parent b2365c8 commit fbd2366

File tree

12 files changed

+285
-33
lines changed

12 files changed

+285
-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: 102 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,108 @@
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+
#define IPINFO_DECOMPRESSED_SZ (1UL<<24UL) /* 16 MiB */
23+
#define IPINFO_MAX_NODES (1UL<<22UL) /* 4M nodes */
24+
#endif
25+
1526
FD_FN_CONST ulong
1627
fd_gui_align( void ) {
1728
return 128UL;
1829
}
1930

2031
FD_FN_CONST ulong
2132
fd_gui_footprint( void ) {
22-
return sizeof(fd_gui_t);
33+
ulong l = FD_LAYOUT_INIT;
34+
l = FD_LAYOUT_APPEND( l, fd_gui_align(), sizeof( fd_gui_t ) );
35+
l = FD_LAYOUT_APPEND( l, 1UL, IPINFO_DECOMPRESSED_SZ );
36+
l = FD_LAYOUT_APPEND( l, alignof(fd_gui_ipinfo_node_t), sizeof(fd_gui_ipinfo_node_t)*IPINFO_MAX_NODES );
37+
return FD_LAYOUT_FINI( l, fd_gui_align() );
2338
}
2439

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

55-
fd_gui_t * gui = (fd_gui_t *)shmem;
146+
FD_SCRATCH_ALLOC_INIT( l, shmem );
147+
fd_gui_t * gui = FD_SCRATCH_ALLOC_APPEND( l, fd_gui_align(), sizeof(fd_gui_t) );
148+
#if FD_HAS_ZSTD
149+
uchar * _ipinfo_buffer = FD_SCRATCH_ALLOC_APPEND( l, 1UL, IPINFO_DECOMPRESSED_SZ );
150+
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 );
151+
#endif
56152

57153
gui->http = http;
58154
gui->topo = topo;
@@ -154,7 +250,6 @@ fd_gui_new( void * shmem,
154250
for( ulong i=0UL; i<FD_GUI_LEADER_CNT; i++ ) gui->leader_slots[ i ]->slot = ULONG_MAX;
155251
gui->leader_slots_cnt = 0UL;
156252

157-
158253
gui->block_engine.has_block_engine = 0;
159254

160255
gui->epoch.has_epoch[ 0 ] = 0;
@@ -174,6 +269,10 @@ fd_gui_new( void * shmem,
174269
gui->summary.catch_up_repair_sz = 0UL;
175270
gui->summary.catch_up_turbine_sz = 0UL;
176271

272+
#if FD_HAS_ZSTD
273+
build_ipinfo_tree( gui, _ipinfo_buffer, _nodes );
274+
#endif
275+
177276
return gui;
178277
}
179278

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 ];

src/disco/gui/fd_gui_peers.c

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -464,10 +464,48 @@ fd_gui_peers_handle_gossip_message( fd_gui_peers_ctx_t * peers,
464464
#endif
465465
}
466466

467+
#if FD_HAS_ZSTD
468+
469+
static uchar
470+
ipinfo_lookup( fd_gui_ipinfo_node_t const * nodes,
471+
uint ip_addr ) {
472+
uchar best_country_idx = 0;
473+
uchar found_match = 0;
474+
475+
uint ip_addr_host = fd_uint_bswap( ip_addr );
476+
477+
fd_gui_ipinfo_node_t const * node = &nodes[0];
478+
479+
for( uchar bit_pos=0; bit_pos<32; bit_pos++ ) {
480+
if( FD_UNLIKELY( node->has_prefix ) ) {
481+
best_country_idx = node->country_code_idx;
482+
found_match = 1;
483+
}
484+
485+
uchar bit = (ip_addr_host >> (31 - bit_pos)) & 1;
486+
fd_gui_ipinfo_node_t const * child = bit ? node->right : node->left;
487+
if( FD_UNLIKELY( !child ) ) break;
488+
489+
node = child;
490+
}
491+
492+
if( FD_UNLIKELY( node->has_prefix ) ) {
493+
best_country_idx = node->country_code_idx;
494+
found_match = 1;
495+
}
496+
497+
if( FD_LIKELY( found_match ) ) return best_country_idx;
498+
return UCHAR_MAX;
499+
}
500+
501+
#endif
502+
467503
void
468504
fd_gui_peers_handle_gossip_update( fd_gui_peers_ctx_t * peers,
505+
fd_gui_ipinfo_node_t const * ipinfo_nodes,
469506
fd_gossip_update_message_t const * update,
470-
long now ) {
507+
long now,
508+
char country_code_map[ static 512 ][ 3 ] ) {
471509
switch( update->tag ) {
472510
case FD_GOSSIP_UPDATE_TAG_CONTACT_INFO: {
473511
/* origin_pubkey should be the same as the contact info pubkey */
@@ -525,7 +563,7 @@ fd_gui_peers_handle_gossip_update( fd_gui_peers_ctx_t * peers,
525563
fd_gui_peers_node_sock_map_idx_insert ( peers->node_sock_map, update->contact_info.idx, peers->contact_info_table );
526564

527565
/* broadcast update to WebSocket clients */
528-
fd_gui_peers_printf_nodes( peers, (int[]){ FD_GUI_PEERS_NODE_UPDATE }, (ulong[]){ update->contact_info.idx }, 1UL );
566+
fd_gui_peers_printf_nodes( peers, (int[]){ FD_GUI_PEERS_NODE_UPDATE }, (ulong[]){ update->contact_info.idx }, 1UL, country_code_map );
529567
fd_http_server_ws_broadcast( peers->http );
530568
} else {
531569
FD_TEST( !fd_gui_peers_node_pubkey_map_ele_query_const( peers->node_pubkey_map, &update->contact_info.contact_info->pubkey, NULL, peers->contact_info_table ) );
@@ -544,6 +582,9 @@ fd_gui_peers_handle_gossip_update( fd_gui_peers_ctx_t * peers,
544582
peer->update_time_nanos = now;
545583
fd_memcpy( &peer->contact_info, update->contact_info.contact_info, sizeof(peer->contact_info) );
546584

585+
/* fetch and set country code */
586+
peer->country_code_idx = ipinfo_lookup( ipinfo_nodes, peer->contact_info.sockets[ FD_CONTACT_INFO_SOCKET_GOSSIP ].addr );
587+
547588
/* update pubkey_map, sock_map */
548589
fd_gui_peers_node_sock_map_idx_insert ( peers->node_sock_map, update->contact_info.idx, peers->contact_info_table );
549590
fd_gui_peers_node_pubkey_map_idx_insert( peers->node_pubkey_map, update->contact_info.idx, peers->contact_info_table );
@@ -556,7 +597,7 @@ fd_gui_peers_handle_gossip_update( fd_gui_peers_ctx_t * peers,
556597
fd_http_server_ws_broadcast( peers->http );
557598

558599
/* broadcast update to WebSocket clients */
559-
fd_gui_peers_printf_nodes( peers, (int[]){ FD_GUI_PEERS_NODE_ADD }, (ulong[]){ update->contact_info.idx }, 1UL );
600+
fd_gui_peers_printf_nodes( peers, (int[]){ FD_GUI_PEERS_NODE_ADD }, (ulong[]){ update->contact_info.idx }, 1UL, country_code_map );
560601
fd_http_server_ws_broadcast( peers->http );
561602
}
562603
break;
@@ -594,7 +635,7 @@ fd_gui_peers_handle_gossip_update( fd_gui_peers_ctx_t * peers,
594635
fd_http_server_ws_broadcast( peers->http );
595636

596637
/* broadcast update to WebSocket clients */
597-
fd_gui_peers_printf_nodes( peers, (int[]){ FD_GUI_PEERS_NODE_DELETE }, (ulong[]){ update->contact_info.idx }, 1UL );
638+
fd_gui_peers_printf_nodes( peers, (int[]){ FD_GUI_PEERS_NODE_DELETE }, (ulong[]){ update->contact_info.idx }, 1UL, country_code_map );
598639
fd_http_server_ws_broadcast( peers->http );
599640
break;
600641
}
@@ -621,7 +662,8 @@ void
621662
fd_gui_peers_handle_vote_update( fd_gui_peers_ctx_t * peers,
622663
fd_gui_peers_vote_t * votes,
623664
ulong vote_cnt,
624-
long now ) {
665+
long now,
666+
char country_code_map[ static 512 ][ 3 ] ) {
625667
(void)now;
626668
fd_gui_peers_vote_t * votes_sorted = votes;
627669
fd_gui_peers_vote_t * votes_scratch = peers->votes_scratch;
@@ -708,7 +750,7 @@ fd_gui_peers_handle_vote_update( fd_gui_peers_ctx_t * peers,
708750
}
709751

710752
if( FD_UNLIKELY( count ) ) {
711-
fd_gui_peers_printf_nodes( peers, actions, idxs, count );
753+
fd_gui_peers_printf_nodes( peers, actions, idxs, count, country_code_map );
712754
fd_http_server_ws_broadcast( peers->http );
713755
}
714756
}
@@ -1099,20 +1141,24 @@ fd_gui_peers_poll( fd_gui_peers_ctx_t * peers, long now ) {
10991141
}
11001142

11011143
void
1102-
fd_gui_peers_ws_open( fd_gui_peers_ctx_t * peers, ulong ws_conn_id, long now ) {
1144+
fd_gui_peers_ws_open( fd_gui_peers_ctx_t * peers,
1145+
ulong ws_conn_id,
1146+
long now,
1147+
char country_code_map[ static 512 ][ 3 ] ) {
11031148
peers->client_viewports[ ws_conn_id ].connected = 1;
11041149
peers->client_viewports[ ws_conn_id ].connected_time = now;
11051150
peers->client_viewports[ ws_conn_id ].start_row = 0;
11061151
peers->client_viewports[ ws_conn_id ].row_cnt = 0;
11071152
peers->client_viewports[ ws_conn_id ].sort_key = FD_GUI_PEERS_LIVE_TABLE_DEFAULT_SORT_KEY;
11081153
fd_gui_peers_ws_conn_rr_grow( peers, ws_conn_id );
11091154

1110-
fd_gui_peers_printf_node_all( peers );
1155+
fd_gui_peers_printf_node_all( peers, country_code_map );
11111156
FD_TEST( !fd_http_server_ws_send( peers->http, ws_conn_id ) );
11121157
}
11131158

11141159
void
1115-
fd_gui_peers_ws_close( fd_gui_peers_ctx_t * peers, ulong ws_conn_id ) {
1160+
fd_gui_peers_ws_close( fd_gui_peers_ctx_t * peers,
1161+
ulong ws_conn_id ) {
11161162
fd_gui_peers_live_table_sort_key_remove( peers->live_table, &peers->client_viewports[ ws_conn_id ].sort_key );
11171163
peers->client_viewports[ ws_conn_id ].connected = 0;
11181164
fd_gui_peers_ws_conn_rr_shrink( peers, ws_conn_id );

0 commit comments

Comments
 (0)