Skip to content

Commit 1ea255f

Browse files
committed
feat(hfork): rewrite hard fork detector based on #7087
1 parent 2dbb1b0 commit 1ea255f

26 files changed

+832
-202
lines changed

src/app/firedancer/config/default.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1371,15 +1371,15 @@ user = ""
13711371
[tiles.tower]
13721372
# Solana reaches consensus via replay, but can "cluster confirm"
13731373
# slots ahead of the replay tip by listening to vote txns from
1374-
# gossip or TPU. The larger max_lookahead_conf, the further
1374+
# gossip or TPU. The larger max_vote_lookahead, the further
13751375
# ahead slots can be cluster confirmed before they are replayed.
13761376
#
13771377
# Specifically, tower will ignore gossip or TPU votes that are
1378-
# more than max_lookahead_conf slots ahead of the root.
1378+
# more than max_vote_lookahead slots ahead of the root.
13791379
#
1380-
# Note max_lookahead_conf must be >= max_live_slots and
1380+
# Note max_vote_lookahead must be >= max_live_slots and
13811381
# Firedancer will ignore a value where this is not the case.
1382-
max_lookahead_conf = 4096
1382+
max_vote_lookahead = 4096
13831383

13841384
[tiles.send]
13851385
# The port the send tile uses for QUIC, to send votes and other

src/app/firedancer/topology.c

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,6 @@ fd_topo_initialize( config_t * config ) {
345345

346346
fd_topob_wksp( topo, "funk" );
347347
fd_topob_wksp( topo, "progcache" );
348-
fd_topob_wksp( topo, "bh_cmp" );
349348
fd_topob_wksp( topo, "fec_sets" );
350349
fd_topob_wksp( topo, "txncache" );
351350
fd_topob_wksp( topo, "banks" );
@@ -1199,9 +1198,9 @@ fd_topo_configure_tile( fd_topo_tile_t * tile,
11991198

12001199
} else if( FD_UNLIKELY( !strcmp( tile->name, "tower" ) ) ) {
12011200

1202-
tile->tower.fork_fatal = config->firedancer.development.hard_fork_fatal;
1201+
tile->tower.hard_fork_fatal = config->firedancer.development.hard_fork_fatal;
12031202
tile->tower.max_live_slots = config->firedancer.runtime.max_live_slots;
1204-
tile->tower.max_lookahead_conf = config->tiles.tower.max_lookahead_conf;
1203+
tile->tower.max_vote_lookahead = config->tiles.tower.max_vote_lookahead;
12051204
strncpy( tile->tower.identity_key, config->paths.identity_key, sizeof(tile->tower.identity_key) );
12061205
strncpy( tile->tower.vote_account, config->paths.vote_account, sizeof(tile->tower.vote_account) );
12071206
strncpy( tile->tower.base_path, config->paths.base, sizeof(tile->tower.base_path) );

src/app/shared/fd_config.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,7 @@ struct fd_config {
490490
} shredcap;
491491

492492
struct {
493-
ulong max_lookahead_conf;
493+
ulong max_vote_lookahead;
494494
} tower;
495495

496496
} tiles;

src/app/shared/fd_config_parse.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ fd_config_extract_pod( uchar * pod,
259259

260260
CFG_POP ( ushort, tiles.send.send_src_port );
261261

262-
CFG_POP ( ulong, tiles.tower.max_lookahead_conf );
262+
CFG_POP ( ulong, tiles.tower.max_vote_lookahead );
263263

264264
CFG_POP ( bool, tiles.archiver.enabled );
265265
CFG_POP ( ulong, tiles.archiver.end_slot );

src/choreo/fd_choreo_base.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,6 @@ typedef uchar fd_block_id_t[ 32UL ];
3636
typedef fd_slot_hash_t fd_slot_pubkey_t;
3737

3838
static const fd_pubkey_t pubkey_null = {{ 0 }};
39+
static const fd_hash_t hash_null = {{ 0 }};
3940

4041
#endif /* HEADER_fd_src_choreo_fd_choreo_base_h */

src/choreo/ghost/fd_ghost.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -271,18 +271,18 @@ fd_ghost_insert( fd_ghost_t * ghost,
271271
void
272272
fd_ghost_count_vote( fd_ghost_t * ghost,
273273
fd_ghost_blk_t * blk,
274-
fd_pubkey_t const * vtr_addr,
274+
fd_pubkey_t const * vote_acc,
275275
ulong stake,
276276
ulong slot ) {
277277

278278
fd_ghost_blk_t const * root = fd_ghost_root( ghost );
279279
fd_ghost_blk_t * pool = ghost->pool;
280-
fd_ghost_vtr_t * vtr = vtr_map_query( ghost->vtr_map, *vtr_addr, NULL );
280+
fd_ghost_vtr_t * vtr = vtr_map_query( ghost->vtr_map, *vote_acc, NULL );
281281

282282
if( FD_UNLIKELY( slot == ULONG_MAX ) ) return; /* hasn't voted */
283283
if( FD_UNLIKELY( slot < root->slot ) ) return; /* vote older than root */
284284

285-
if( FD_UNLIKELY( !vtr ) ) vtr = vtr_map_insert( ghost->vtr_map, *vtr_addr );
285+
if( FD_UNLIKELY( !vtr ) ) vtr = vtr_map_insert( ghost->vtr_map, *vote_acc );
286286
else {
287287

288288
/* Only process the vote if it is not the same as the previous vote

src/choreo/hfork/Local.mk

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
$(call add-hdrs,fd_hfork.h)
2+
$(call add-objs,fd_hfork,fd_choreo)
3+
ifdef FD_HAS_HOSTED
4+
ifdef FD_HAS_SECP256K1
5+
$(call make-unit-test,test_hfork,test_hfork,fd_choreo fd_flamenco fd_tango fd_ballet fd_util)
6+
$(call run-unit-test,test_hfork)
7+
endif
8+
endif

src/choreo/hfork/fd_hfork.c

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
#include "fd_hfork.h"
2+
#include "fd_hfork_private.h"
3+
4+
static void
5+
check( fd_hfork_t * hfork,
6+
ulong total_stake,
7+
candidate_t * candidate,
8+
int invalid,
9+
fd_hash_t * our_bank_hash ) {
10+
11+
if( FD_LIKELY( candidate->checked ) ) return; /* already checked this bank hash against our own */
12+
if( FD_LIKELY( candidate->stake * 100UL / total_stake < 52UL ) ) return; /* not enough stake to compare */
13+
14+
if( FD_UNLIKELY( invalid ) ) {
15+
char msg[ 4096UL ];
16+
FD_TEST( fd_cstr_printf_check( msg, sizeof( msg ), NULL,
17+
"HARD FORK DETECTED: our validator has marked slot %lu with block ID `%s` dead, but %lu validators with %.1f of stake have voted on it",
18+
candidate->slot,
19+
FD_BASE58_ENC_32_ALLOCA( candidate->key.block_id.uc ),
20+
candidate->cnt,
21+
100.0*(double)candidate->stake/(double)total_stake ) );
22+
23+
if( FD_UNLIKELY( hfork->fatal ) ) FD_LOG_ERR (( "%s", msg ));
24+
else FD_LOG_WARNING(( "%s", msg ));
25+
} else if( FD_UNLIKELY( 0!=memcmp( our_bank_hash, &candidate->key.bank_hash, 32UL ) ) ) {
26+
char msg[ 4096UL ];
27+
FD_TEST( fd_cstr_printf_check( msg, sizeof( msg ), NULL,
28+
"HARD FORK DETECTED: our validator has produced block hash `%s` for slot %lu with block ID `%s`, but %lu validators with %.1f of stake have voted on a different block hash `%s` for the same slot",
29+
FD_BASE58_ENC_32_ALLOCA( candidate->key.bank_hash.uc ),
30+
candidate->slot,
31+
FD_BASE58_ENC_32_ALLOCA( candidate->key.block_id.uc ),
32+
candidate->cnt,
33+
100.0*(double)candidate->stake/(double)total_stake,
34+
FD_BASE58_ENC_32_ALLOCA( candidate->key.bank_hash.uc ) ) );
35+
36+
if( FD_UNLIKELY( hfork->fatal ) ) FD_LOG_ERR (( "%s", msg ));
37+
else FD_LOG_WARNING(( "%s", msg ));
38+
}
39+
candidate->checked = 1;
40+
}
41+
42+
ulong
43+
fd_hfork_align( void ) {
44+
return 128UL;
45+
}
46+
47+
ulong
48+
fd_hfork_footprint( ulong max_live_slots,
49+
ulong max_vote_accounts ) {
50+
int lg_blk_max = fd_ulong_find_msb( fd_ulong_pow2_up( max_live_slots * max_vote_accounts ) ) + 1;
51+
int lg_vtr_max = fd_ulong_find_msb( fd_ulong_pow2_up( max_vote_accounts ) ) + 1;
52+
53+
ulong l = FD_LAYOUT_INIT;
54+
l = FD_LAYOUT_APPEND( l, alignof(fd_hfork_t), sizeof(fd_hfork_t) );
55+
l = FD_LAYOUT_APPEND( l, blk_map_align(), blk_map_footprint( lg_blk_max ) );
56+
l = FD_LAYOUT_APPEND( l, vtr_map_align(), vtr_map_footprint( lg_vtr_max ) );
57+
l = FD_LAYOUT_APPEND( l, candidate_map_align(), candidate_map_footprint( lg_blk_max ) );
58+
for( ulong i = 0UL; i < fd_ulong_pow2( lg_vtr_max ); i++ ) {
59+
l = FD_LAYOUT_APPEND( l, votes_align(), votes_footprint( max_live_slots ) );
60+
}
61+
return FD_LAYOUT_FINI( l, fd_hfork_align() );
62+
}
63+
64+
void *
65+
fd_hfork_new( void * shmem,
66+
ulong max_live_slots,
67+
ulong max_vote_accounts,
68+
ulong seed,
69+
int fatal ) {
70+
(void)seed; /* TODO map seed */
71+
72+
if( FD_UNLIKELY( !shmem ) ) {
73+
FD_LOG_WARNING(( "NULL mem" ));
74+
return NULL;
75+
}
76+
77+
if( FD_UNLIKELY( !fd_ulong_is_aligned( (ulong)shmem, fd_hfork_align() ) ) ) {
78+
FD_LOG_WARNING(( "misaligned mem" ));
79+
return NULL;
80+
}
81+
82+
ulong footprint = fd_hfork_footprint( max_live_slots, max_vote_accounts );
83+
if( FD_UNLIKELY( !footprint ) ) {
84+
FD_LOG_WARNING(( "bad max_live_slots (%lu) or max_vote_accounts (%lu)", max_live_slots, max_vote_accounts ));
85+
return NULL;
86+
}
87+
88+
fd_memset( shmem, 0, footprint );
89+
90+
int lg_blk_max = fd_ulong_find_msb( fd_ulong_pow2_up( max_live_slots * max_vote_accounts ) ) + 1;
91+
int lg_vtr_max = fd_ulong_find_msb( fd_ulong_pow2_up( max_vote_accounts ) ) + 1;
92+
93+
FD_SCRATCH_ALLOC_INIT( l, shmem );
94+
fd_hfork_t * hfork = FD_SCRATCH_ALLOC_APPEND( l, fd_hfork_align(), sizeof( fd_hfork_t ) );
95+
void * blk_map = FD_SCRATCH_ALLOC_APPEND( l, blk_map_align(), blk_map_footprint( lg_blk_max ) );
96+
void * vtr_map = FD_SCRATCH_ALLOC_APPEND( l, vtr_map_align(), vtr_map_footprint( lg_vtr_max ) );
97+
void * candidate_map = FD_SCRATCH_ALLOC_APPEND( l, candidate_map_align(), candidate_map_footprint( lg_blk_max ) );
98+
99+
hfork->blk_map = blk_map_new( blk_map, lg_blk_max );
100+
hfork->vtr_map = vtr_map_new( vtr_map, lg_vtr_max );
101+
hfork->candidate_map = candidate_map_new( candidate_map, lg_blk_max );
102+
for( ulong i = 0UL; i < fd_ulong_pow2( lg_vtr_max ); i++ ) {
103+
void * votes = FD_SCRATCH_ALLOC_APPEND( l, votes_align(), votes_footprint( max_live_slots ) );
104+
vtr_t * join = vtr_map_join( hfork->vtr_map );
105+
join[i].votes = votes_new( votes, max_live_slots );
106+
}
107+
FD_TEST( FD_SCRATCH_ALLOC_FINI( l, fd_hfork_align() ) == (ulong)shmem + footprint );
108+
hfork->fatal = fatal;
109+
return shmem;
110+
}
111+
112+
fd_hfork_t *
113+
fd_hfork_join( void * shhfork ) {
114+
fd_hfork_t * hfork = (fd_hfork_t *)shhfork;
115+
116+
if( FD_UNLIKELY( !hfork ) ) {
117+
FD_LOG_WARNING(( "NULL hfork" ));
118+
return NULL;
119+
}
120+
121+
if( FD_UNLIKELY( !fd_ulong_is_aligned((ulong)hfork, fd_hfork_align() ) ) ) {
122+
FD_LOG_WARNING(( "misaligned hfork" ));
123+
return NULL;
124+
}
125+
126+
hfork->blk_map = blk_map_join( hfork->blk_map );
127+
hfork->vtr_map = vtr_map_join( hfork->vtr_map );
128+
hfork->candidate_map = candidate_map_join( hfork->candidate_map );
129+
for( ulong i = 0UL; i < vtr_map_slot_cnt( hfork->vtr_map ); i++ ) {
130+
hfork->vtr_map[i].votes = votes_join( hfork->vtr_map[i].votes );
131+
}
132+
133+
return hfork;
134+
}
135+
136+
void *
137+
fd_hfork_leave( fd_hfork_t const * hfork ) {
138+
139+
if( FD_UNLIKELY( !hfork ) ) {
140+
FD_LOG_WARNING(( "NULL hfork" ));
141+
return NULL;
142+
}
143+
144+
return (void *)hfork;
145+
}
146+
147+
void *
148+
fd_hfork_delete( void * hfork ) {
149+
150+
if( FD_UNLIKELY( !hfork ) ) {
151+
FD_LOG_WARNING(( "NULL hfork" ));
152+
return NULL;
153+
}
154+
155+
if( FD_UNLIKELY( !fd_ulong_is_aligned((ulong)hfork, fd_hfork_align() ) ) ) {
156+
FD_LOG_WARNING(( "misaligned hfork" ));
157+
return NULL;
158+
}
159+
160+
return hfork;
161+
}
162+
163+
void
164+
fd_hfork_count_vote( fd_hfork_t * hfork,
165+
fd_hash_t const * vote_acc,
166+
fd_hash_t const * block_id,
167+
fd_hash_t const * bank_hash,
168+
ulong slot,
169+
ulong stake,
170+
ulong total_stake ) {
171+
172+
/* Get the vtr. */
173+
174+
FD_BASE58_ENCODE_32_BYTES( vote_acc->uc, tmp );
175+
vtr_t * vtr = vtr_map_query( hfork->vtr_map, *vote_acc, NULL );
176+
if( FD_UNLIKELY( !vtr ) ) {
177+
vtr = vtr_map_insert( hfork->vtr_map, *vote_acc );
178+
}
179+
180+
/* Ignore out of order or duplicate votes. */
181+
182+
if( FD_UNLIKELY( !votes_empty( vtr->votes ) ) ) {
183+
vote_t const * tail = votes_peek_tail_const( vtr->votes );
184+
if( FD_UNLIKELY( tail && tail->slot >= slot ) ) return;
185+
}
186+
187+
/* Evict the candidate's oldest vote (by vote slot). */
188+
189+
if( FD_UNLIKELY( votes_full( vtr->votes ) ) ) {
190+
vote_t vote = votes_pop_head( vtr->votes );
191+
candidate_key_t key = { .block_id = vote.block_id, .bank_hash = vote.bank_hash };
192+
candidate_t * candidate = candidate_map_query( hfork->candidate_map, key, NULL );
193+
candidate->stake -= vote.stake;
194+
candidate->cnt--;
195+
if( FD_UNLIKELY( candidate->cnt==0 ) ) {
196+
candidate_map_remove( hfork->candidate_map, candidate );
197+
blk_t * blk = blk_map_query( hfork->blk_map, vote.block_id, NULL );
198+
blk->bank_hashes_cnt--;
199+
if( FD_UNLIKELY( blk->bank_hashes_cnt == 0 ) ) blk_map_remove( hfork->blk_map, blk );
200+
}
201+
}
202+
203+
/* Push the vote onto the vtr. */
204+
205+
vote_t vote = { .block_id = *block_id, .bank_hash = *bank_hash, .slot = slot, .stake = stake };
206+
vtr->votes = votes_push_tail( vtr->votes, vote );
207+
208+
/* Update the hard fork candidate for this block id. */
209+
210+
candidate_key_t key = { .block_id = *block_id, .bank_hash = *bank_hash };
211+
candidate_t * candidate = candidate_map_query( hfork->candidate_map, key, NULL );
212+
if( FD_UNLIKELY( !candidate ) ) {
213+
candidate = candidate_map_insert( hfork->candidate_map, key );
214+
candidate->slot = slot;
215+
candidate->stake = 0UL;
216+
candidate->cnt = 0UL;
217+
}
218+
candidate->cnt++;
219+
candidate->stake += stake;
220+
221+
/* Update the list of bank hashes for this block_id. */
222+
223+
blk_t * blk = blk_map_query( hfork->blk_map, *block_id, NULL );
224+
if( FD_UNLIKELY( !blk ) ) {
225+
FD_TEST( blk_map_key_cnt( hfork->blk_map ) < blk_map_key_max( hfork->blk_map ) ); /* invariant violation: blk_map full */
226+
blk = blk_map_insert( hfork->blk_map, *block_id );
227+
FD_TEST( blk );
228+
blk->bank_hashes_cnt = 0UL;
229+
blk->replayed = 0;
230+
blk->invalid = 0;
231+
}
232+
blk->bank_hashes[ blk->bank_hashes_cnt++ ] = *bank_hash;
233+
234+
/* Check for hard forks. */
235+
236+
if( FD_LIKELY( blk->replayed ) ) check( hfork, total_stake, candidate, blk->invalid, &blk->our_bank_hash );
237+
}
238+
239+
void
240+
fd_hfork_record_our_bank_hash( fd_hfork_t * hfork,
241+
fd_hash_t * block_id,
242+
fd_hash_t * bank_hash,
243+
ulong total_stake ) {
244+
blk_t * blk = blk_map_query( hfork->blk_map, *block_id, NULL );
245+
if( FD_UNLIKELY( !blk ) ) {
246+
blk = blk_map_insert( hfork->blk_map, *block_id );
247+
FD_TEST( blk );
248+
blk->bank_hashes_cnt = 0UL;
249+
blk->replayed = 1;
250+
}
251+
if( FD_LIKELY( bank_hash ) ) { blk->invalid = 0; blk->our_bank_hash = *bank_hash; }
252+
else blk->invalid = 1;
253+
254+
for( ulong i=0UL; i<blk->bank_hashes_cnt; i++ ) {
255+
candidate_key_t key = { .block_id = *block_id, .bank_hash = blk->bank_hashes[i] };
256+
candidate_t * candidate = candidate_map_query( hfork->candidate_map, key, NULL );
257+
if( FD_LIKELY( candidate ) && !candidate->checked ) {
258+
check( hfork, total_stake, candidate, blk->invalid, &blk->our_bank_hash );
259+
candidate->checked = 1;
260+
}
261+
}
262+
}

0 commit comments

Comments
 (0)