diff --git a/src/ballet/chacha/fd_chacha.h b/src/ballet/chacha/fd_chacha.h index fce664e4f8a..4a2c9c2a7fb 100644 --- a/src/ballet/chacha/fd_chacha.h +++ b/src/ballet/chacha/fd_chacha.h @@ -7,9 +7,9 @@ #define FD_CHACHA_BLOCK_SZ (64UL) -/* FD_CHACHA20_KEY_SZ is the size of the ChaCha20 encryption key */ +/* FD_CHACHA_KEY_SZ is the size of the ChaCha20 encryption key */ -#define FD_CHACHA20_KEY_SZ (32UL) +#define FD_CHACHA_KEY_SZ (32UL) FD_PROTOTYPES_BEGIN diff --git a/src/ballet/chacha/fd_chacha_rng.c b/src/ballet/chacha/fd_chacha_rng.c index 290a2532041..b9b31724310 100644 --- a/src/ballet/chacha/fd_chacha_rng.c +++ b/src/ballet/chacha/fd_chacha_rng.c @@ -58,10 +58,20 @@ fd_chacha_rng_delete( void * shrng ) { return shrng; } +fd_chacha_rng_t * +fd_chacha8_rng_init( fd_chacha_rng_t * rng, + void const * key ) { + memcpy( rng->key, key, FD_CHACHA_KEY_SZ ); + rng->buf_off = 0UL; + rng->buf_fill = 0UL; + fd_chacha8_rng_private_refill( rng ); + return rng; +} + fd_chacha_rng_t * fd_chacha20_rng_init( fd_chacha_rng_t * rng, void const * key ) { - memcpy( rng->key, key, FD_CHACHA20_KEY_SZ ); + memcpy( rng->key, key, FD_CHACHA_KEY_SZ ); rng->buf_off = 0UL; rng->buf_fill = 0UL; fd_chacha20_rng_private_refill( rng ); diff --git a/src/ballet/chacha/fd_chacha_rng.h b/src/ballet/chacha/fd_chacha_rng.h index 71f310c2d78..e7d3d23e865 100644 --- a/src/ballet/chacha/fd_chacha_rng.h +++ b/src/ballet/chacha/fd_chacha_rng.h @@ -6,9 +6,7 @@ fd_rng is a better choice in all other cases. */ #include "fd_chacha.h" -#if !FD_HAS_INT128 #include "../../util/bits/fd_uwide.h" -#endif /* FD_CHACHA_RNG_DEBUG controls debug logging. 0 is off; 1 is on. */ @@ -231,16 +229,31 @@ fd_chacha20_rng_ulong_roll( fd_chacha_rng_t * rng, (n << (63 - fd_ulong_find_msb( n ) )) - 1UL ); for( int i=0; 1; i++ ) { - ulong v = fd_chacha20_rng_ulong( rng ); -#if FD_HAS_INT128 - /* Compiles to one mulx instruction */ - uint128 res = (uint128)v * (uint128)n; - ulong hi = (ulong)(res>>64); - ulong lo = (ulong) res; -#else + ulong v = fd_chacha20_rng_ulong( rng ); + ulong hi, lo; + fd_uwide_mul( &hi, &lo, v, n ); + +# if FD_CHACHA_RNG_DEBUG + FD_LOG_DEBUG(( "roll (attempt %d): n=%016lx zone: %016lx v=%016lx lo=%016lx hi=%016lx", i, n, zone, v, lo, hi )); +# else + (void)i; +# endif /* FD_CHACHA_RNG_DEBUG */ + + if( FD_LIKELY( lo<=zone ) ) return hi; + } +} + +static inline ulong +fd_chacha8_rng_ulong_roll( fd_chacha_rng_t * rng, + ulong n ) { + ulong const zone = fd_ulong_if( rng->mode==FD_CHACHA_RNG_MODE_MOD, + ULONG_MAX - (ULONG_MAX-n+1UL)%n, + (n << (63 - fd_ulong_find_msb( n ) )) - 1UL ); + + for( int i=0; 1; i++ ) { + ulong v = fd_chacha8_rng_ulong( rng ); ulong hi, lo; fd_uwide_mul( &hi, &lo, v, n ); -#endif # if FD_CHACHA_RNG_DEBUG FD_LOG_DEBUG(( "roll (attempt %d): n=%016lx zone: %016lx v=%016lx lo=%016lx hi=%016lx", i, n, zone, v, lo, hi )); diff --git a/src/ballet/wsample/fd_wsample.c b/src/ballet/wsample/fd_wsample.c index 6e49e067110..dbecff33500 100644 --- a/src/ballet/wsample/fd_wsample.c +++ b/src/ballet/wsample/fd_wsample.c @@ -127,7 +127,8 @@ struct __attribute__((aligned(64UL))) fd_wsample_private { uint height; char restore_enabled; char poisoned_mode; - /* Two bytes of padding here */ + uchar rng_algo; + /* One byte of padding here */ fd_chacha_rng_t * rng; @@ -302,6 +303,7 @@ fd_wsample_new_init( void * shmem, sampler->height = (uint)height; sampler->restore_enabled = (char)!!restore_enabled; sampler->poisoned_mode = 0; + sampler->rng_algo = 0; sampler->rng = rng; fd_memset( sampler->tree, (char)0, internal_cnt*sizeof(tree_ele_t) ); @@ -389,18 +391,25 @@ fd_wsample_delete( void * shmem ) { return shmem; } - - -fd_chacha_rng_t * fd_wsample_get_rng( fd_wsample_t * sampler ) { return sampler->rng; } - - -/* TODO: Should this function exist at all? */ void -fd_wsample_seed_rng( fd_chacha_rng_t * rng, - uchar seed[static 32] ) { - fd_chacha20_rng_init( rng, seed ); +fd_wsample_seed_rng( fd_wsample_t * sampler, + uchar seed[ 32 ], + int use_chacha8 ) { + sampler->rng_algo = fd_uchar_if( use_chacha8, FD_WSAMPLE_RNG_CHACHA8, FD_WSAMPLE_RNG_CHACHA20 ); + if( FD_UNLIKELY( sampler->rng_algo==FD_WSAMPLE_RNG_CHACHA8 ) ) { + fd_chacha8_rng_init( sampler->rng, seed ); + } else { + fd_chacha20_rng_init( sampler->rng, seed ); + } } +ulong +fd_wsample_rng_ulong_roll( fd_wsample_t * sampler, ulong n ) { + if( FD_UNLIKELY( sampler->rng_algo==FD_WSAMPLE_RNG_CHACHA8 ) ) { + return fd_chacha8_rng_ulong_roll( sampler->rng, n ); + } + return fd_chacha20_rng_ulong_roll( sampler->rng, n ); +} fd_wsample_t * fd_wsample_restore_all( fd_wsample_t * sampler ) { @@ -597,7 +606,7 @@ fd_wsample_remove_idx( fd_wsample_t * sampler, but operations with mask registers are frustratingly slow. Instead, we first define x'=x+1, so then v0<=x is equivalent to v0-x'<0, or whether v0-x' has the high bit set. Because of - fd_chacha20_rng_ulong_roll's contract, we know xunremoved_weight ) ) { idxs[ i ] = FD_WSAMPLE_EMPTY; continue; } if( FD_UNLIKELY( sampler->poisoned_mode ) ) { idxs[ i ] = FD_WSAMPLE_INDETERMINATE; continue; } - ulong unif = fd_chacha20_rng_ulong_roll( sampler->rng, sampler->unremoved_weight+sampler->poisoned_weight ); + ulong unif = fd_wsample_rng_ulong_roll( sampler, sampler->unremoved_weight+sampler->poisoned_weight ); if( FD_UNLIKELY( unif>=sampler->unremoved_weight ) ) { idxs[ i ] = FD_WSAMPLE_INDETERMINATE; sampler->poisoned_mode = 1; @@ -771,7 +780,7 @@ ulong fd_wsample_sample( fd_wsample_t * sampler ) { if( FD_UNLIKELY( !sampler->unremoved_weight ) ) return FD_WSAMPLE_EMPTY; if( FD_UNLIKELY( sampler->poisoned_mode ) ) return FD_WSAMPLE_INDETERMINATE; - ulong unif = fd_chacha20_rng_ulong_roll( sampler->rng, sampler->unremoved_weight+sampler->poisoned_weight ); + ulong unif = fd_wsample_rng_ulong_roll( sampler, sampler->unremoved_weight+sampler->poisoned_weight ); if( FD_UNLIKELY( unif>=sampler->unremoved_weight ) ) return FD_WSAMPLE_INDETERMINATE; return (ulong)fd_wsample_map_sample( sampler, unif ); } @@ -780,7 +789,7 @@ ulong fd_wsample_sample_and_remove( fd_wsample_t * sampler ) { if( FD_UNLIKELY( !sampler->unremoved_weight ) ) return FD_WSAMPLE_EMPTY; if( FD_UNLIKELY( sampler->poisoned_mode ) ) return FD_WSAMPLE_INDETERMINATE; - ulong unif = fd_chacha20_rng_ulong_roll( sampler->rng, sampler->unremoved_weight+sampler->poisoned_weight ); + ulong unif = fd_wsample_rng_ulong_roll( sampler, sampler->unremoved_weight+sampler->poisoned_weight ); if( FD_UNLIKELY( unif>=sampler->unremoved_weight ) ) { sampler->poisoned_mode = 1; return FD_WSAMPLE_INDETERMINATE; diff --git a/src/ballet/wsample/fd_wsample.h b/src/ballet/wsample/fd_wsample.h index 74f4f0217d8..409483e8fc1 100644 --- a/src/ballet/wsample/fd_wsample.h +++ b/src/ballet/wsample/fd_wsample.h @@ -20,6 +20,9 @@ struct fd_wsample_private; typedef struct fd_wsample_private fd_wsample_t; +#define FD_WSAMPLE_RNG_CHACHA20 (0U) +#define FD_WSAMPLE_RNG_CHACHA8 (1U) + #define FD_WSAMPLE_ALIGN (64UL) /* fd_leaders really wants a compile time-compatible footprint... The internal count is 1/8 * (9^ceil(log_9(ele_cnt)) - 1) */ @@ -135,10 +138,10 @@ void * fd_wsample_new_fini( void * shmem, ulong poisoned_weight ); /* fd_wsample_get_rng returns the value provided for rng in new. */ fd_chacha_rng_t * fd_wsample_get_rng( fd_wsample_t * sampler ); -/* fd_wsample_seed_rng seeds the ChaCha20 rng with the provided seed in +/* fd_wsample_seed_rng seeds the ChaCha rng with the provided seed in preparation for sampling. This function is compatible with Solana's ChaChaRng::from_seed. */ -void fd_wsample_seed_rng( fd_chacha_rng_t * rng, uchar seed[static 32] ); +void fd_wsample_seed_rng( fd_wsample_t * sampler, uchar seed[ 32 ], int use_chacha8 ); /* fd_wsample_sample{_and_remove}{,_many} produces one or cnt (in the _many case) weighted random samples from the sampler. If the @@ -191,6 +194,9 @@ void fd_wsample_remove_idx( fd_wsample_t * sampler, ulong idx ); in which case no elements are restored. */ fd_wsample_t * fd_wsample_restore_all( fd_wsample_t * sampler ); - +/* fd_wsample_rng_ulong_roll returns an uniform IID rand in [0,n) + analogous to fd_rng_ulong_roll. Internally it uses chacha8 or + chacha20, based on how the wsample was initialized. */ +ulong fd_wsample_rng_ulong_roll( fd_wsample_t * sampler, ulong n ); #endif /* HEADER_fd_src_ballet_wsample_fd_wsample_h */ diff --git a/src/ballet/wsample/test_wsample.c b/src/ballet/wsample/test_wsample.c index 90c17fbac28..5c4adaab949 100644 --- a/src/ballet/wsample/test_wsample.c +++ b/src/ballet/wsample/test_wsample.c @@ -191,7 +191,7 @@ test_matches_solana( void ) { void * partial = fd_wsample_new_init( _shmem, rng, 2UL, 0, FD_WSAMPLE_HINT_FLAT ); fd_wsample_t * tree = fd_wsample_join( fd_wsample_new_fini( fd_wsample_new_add( fd_wsample_new_add( partial, 2UL ), 1UL ), 0UL ) ); - fd_wsample_seed_rng( fd_wsample_get_rng( tree ), zero_seed ); + fd_wsample_seed_rng( tree, zero_seed, 0 /* use_chacha8 */ ); FD_TEST( fd_wsample_sample( tree ) == 0UL ); FD_TEST( fd_wsample_sample( tree ) == 0UL ); @@ -214,13 +214,10 @@ test_matches_solana( void ) { ulong weights2[18] = { 78, 70, 38, 27, 21, 82, 42, 21, 77, 77, 17, 4, 50, 96, 83, 33, 16, 72 }; memset( zero_seed, 48, 32UL ); - fd_chacha20_rng_init( rng, zero_seed ); - partial = fd_wsample_new_init( _shmem, rng, 18UL, 0, FD_WSAMPLE_HINT_FLAT ); for( ulong i=0UL; i<18UL; i++ ) partial = fd_wsample_new_add( partial, weights2[i] ); tree = fd_wsample_join( fd_wsample_new_fini( partial, 0UL ) ); - fd_wsample_seed_rng( fd_wsample_get_rng( tree ), zero_seed ); - + fd_wsample_seed_rng( tree, zero_seed, 0 /* use_chacha8 */ ); FD_TEST( fd_wsample_sample_and_remove( tree ) == 9UL ); FD_TEST( fd_wsample_sample_and_remove( tree ) == 3UL ); @@ -245,6 +242,66 @@ test_matches_solana( void ) { fd_chacha_rng_delete( fd_chacha_rng_leave( rng ) ); } +static void +test_matches_solana_chacha8( void ) { + /* Adopted from test_repeated_leader_schedule_specific: */ + fd_chacha_rng_t _rng[1]; + fd_chacha_rng_t * rng = fd_chacha_rng_join( fd_chacha_rng_new( _rng, FD_CHACHA_RNG_MODE_MOD ) ); + uchar zero_seed[32] = {0}; + + void * partial = fd_wsample_new_init( _shmem, rng, 2UL, 0, FD_WSAMPLE_HINT_FLAT ); + fd_wsample_t * tree = fd_wsample_join( fd_wsample_new_fini( fd_wsample_new_add( fd_wsample_new_add( partial, 2UL ), 1UL ), 0UL ) ); + fd_wsample_seed_rng( tree, zero_seed, 1 /* use_chacha8 */ ); + + FD_TEST( fd_wsample_sample( tree ) == 1UL ); + FD_TEST( fd_wsample_sample( tree ) == 0UL ); + FD_TEST( fd_wsample_sample( tree ) == 0UL ); + FD_TEST( fd_wsample_sample( tree ) == 0UL ); + FD_TEST( fd_wsample_sample( tree ) == 0UL ); + FD_TEST( fd_wsample_sample( tree ) == 0UL ); + FD_TEST( fd_wsample_sample( tree ) == 0UL ); + FD_TEST( fd_wsample_sample( tree ) == 0UL ); + + fd_wsample_delete( fd_wsample_leave( tree ) ); + fd_chacha_rng_delete( fd_chacha_rng_leave( rng ) ); + + rng = fd_chacha_rng_join( fd_chacha_rng_new( _rng, FD_CHACHA_RNG_MODE_SHIFT ) ); + + /* Adopted from test_weighted_shuffle_hard_coded, except they handle + the special case for 0 weights inside their WeightedShuffle object, + and the test case initially used i32 as weights, which made their + Chacha20 object generate i32s instead of u64s. */ + ulong weights2[18] = { 78, 70, 38, 27, 21, 82, 42, 21, 77, 77, 17, 4, 50, 96, 83, 33, 16, 72 }; + + memset( zero_seed, 48, 32UL ); + partial = fd_wsample_new_init( _shmem, rng, 18UL, 0, FD_WSAMPLE_HINT_FLAT ); + for( ulong i=0UL; i<18UL; i++ ) partial = fd_wsample_new_add( partial, weights2[i] ); + tree = fd_wsample_join( fd_wsample_new_fini( partial, 0UL ) ); + fd_wsample_seed_rng( tree, zero_seed, 1 /* use_chacha8 */ ); + + FD_TEST( fd_wsample_sample_and_remove( tree ) == 13UL ); + FD_TEST( fd_wsample_sample_and_remove( tree ) == 8UL ); + FD_TEST( fd_wsample_sample_and_remove( tree ) == 6UL ); + FD_TEST( fd_wsample_sample_and_remove( tree ) == 14UL ); + FD_TEST( fd_wsample_sample_and_remove( tree ) == 0UL ); + FD_TEST( fd_wsample_sample_and_remove( tree ) == 17UL ); + FD_TEST( fd_wsample_sample_and_remove( tree ) == 1UL ); + FD_TEST( fd_wsample_sample_and_remove( tree ) == 12UL ); + FD_TEST( fd_wsample_sample_and_remove( tree ) == 3UL ); + FD_TEST( fd_wsample_sample_and_remove( tree ) == 16UL ); + FD_TEST( fd_wsample_sample_and_remove( tree ) == 5UL ); + FD_TEST( fd_wsample_sample_and_remove( tree ) == 15UL ); + FD_TEST( fd_wsample_sample_and_remove( tree ) == 9UL ); + FD_TEST( fd_wsample_sample_and_remove( tree ) == 2UL ); + FD_TEST( fd_wsample_sample_and_remove( tree ) == 4UL ); + FD_TEST( fd_wsample_sample_and_remove( tree ) == 7UL ); + FD_TEST( fd_wsample_sample_and_remove( tree ) == 10UL ); + FD_TEST( fd_wsample_sample_and_remove( tree ) == 11UL ); + + fd_wsample_delete( fd_wsample_leave( tree ) ); + fd_chacha_rng_delete( fd_chacha_rng_leave( rng ) ); +} + static void test_sharing( void ) { fd_chacha_rng_t _rng[1]; @@ -472,6 +529,7 @@ main( int argc, FD_TEST( fd_wsample_footprint( MAX, 1 )staked, n ); +} + /* sample_unstaked, sample_unstaked_noprepare, and prepare_unstaked_sampling are used to perform the specific form of unweighted random sampling that Solana uses for unstaked validators. @@ -157,7 +162,7 @@ void * fd_shred_dest_delete( void * mem ) { 1. construct a list of all the unstaked validators, 2. delete the leader (if present) then repeatedly: - 3. choose the chacha20rng_roll( |unstaked| )th element. + 3. choose the chacha_rng_roll( |unstaked| )th element. 4. swap the last element in unstaked with the chosen element 5. return and remove the chosen element (which is now in the last position, so remove is O(1)). @@ -191,7 +196,7 @@ sample_unstaked_noprepare( fd_shred_dest_t * sdest, ulong unstaked_cnt = sdest->unstaked_cnt - (ulong)remove_in_interval; if( FD_UNLIKELY( unstaked_cnt==0UL ) ) return FD_WSAMPLE_EMPTY; - ulong sample = sdest->staked_cnt + fd_chacha20_rng_ulong_roll( sdest->rng, unstaked_cnt ); + ulong sample = sdest->staked_cnt + fd_sdest_rng_ulong_roll( sdest, unstaked_cnt ); return fd_ulong_if( (!remove_in_interval) | (sampleunstaked_unremoved_cnt==0UL ) ) return FD_WSAMPLE_EMPTY; - ulong sample = fd_chacha20_rng_ulong_roll( sdest->rng, sdest->unstaked_unremoved_cnt ); + ulong sample = fd_sdest_rng_ulong_roll( sdest, sdest->unstaked_unremoved_cnt ); ulong to_return = sdest->unstaked[sample]; sdest->unstaked[sample] = sdest->unstaked[--sdest->unstaked_unremoved_cnt]; return to_return; @@ -261,7 +266,8 @@ fd_shred_dest_idx_t * fd_shred_dest_compute_first( fd_shred_dest_t * sdest, fd_shred_t const * const * input_shreds, ulong shred_cnt, - fd_shred_dest_idx_t * out ) { + fd_shred_dest_idx_t * out, + int use_chacha8 ) { if( FD_UNLIKELY( shred_cnt==0UL ) ) return out; @@ -290,7 +296,7 @@ fd_shred_dest_compute_first( fd_shred_dest_t * sdest, int any_staked_candidates = sdest->staked_cnt > (ulong)source_validator_is_staked; for( ulong i=0UL; istaked ), dest_hash_outputs[ i ] ); + fd_wsample_seed_rng( sdest->staked, dest_hash_outputs[ i ], use_chacha8 ); /* Map FD_WSAMPLE_INDETERMINATE to FD_SHRED_DEST_NO_DEST */ if( FD_LIKELY( any_staked_candidates ) ) out[i] = (fd_shred_dest_idx_t)fd_ulong_min( fd_wsample_sample( sdest->staked ), FD_SHRED_DEST_NO_DEST ); else out[i] = (fd_shred_dest_idx_t)sample_unstaked_noprepare( sdest, sdest->source_validator_orig_idx ); @@ -308,7 +314,8 @@ fd_shred_dest_compute_children( fd_shred_dest_t * sdest, ulong out_stride, ulong fanout, ulong dest_cnt, - ulong * opt_max_dest_cnt ) { + ulong * opt_max_dest_cnt, + int use_chacha8 ) { /* The logic here is a little tricky since we are keeping track of staked and unstaked separately and only logically concatenating @@ -358,7 +365,7 @@ fd_shred_dest_compute_children( fd_shred_dest_t * sdest, if( FD_LIKELY( query && leader_is_staked ) ) fd_wsample_remove_idx( sdest->staked, leader_idx ); ulong my_idx = 0UL; - fd_wsample_seed_rng( fd_wsample_get_rng( sdest->staked ), dest_hash_outputs[ i ] ); /* Seeds both samplers since the rng is shared */ + fd_wsample_seed_rng( sdest->staked, dest_hash_outputs[ i ], use_chacha8 ); /* Seeds both samplers since the rng is shared */ if( FD_UNLIKELY( !i_am_staked ) ) { /* If there's excluded stake, we don't know about any unstaked diff --git a/src/disco/shred/fd_shred_dest.h b/src/disco/shred/fd_shred_dest.h index 80262d9524d..9742a94498d 100644 --- a/src/disco/shred/fd_shred_dest.h +++ b/src/disco/shred/fd_shred_dest.h @@ -161,7 +161,8 @@ fd_shred_dest_idx_t * fd_shred_dest_compute_first( fd_shred_dest_t * sdest, fd_shred_t const * const * input_shreds, ulong shred_cnt, - fd_shred_dest_idx_t * out ); + fd_shred_dest_idx_t * out, + int use_chacha8 ); /* fd_shred_dest_compute_children computes the source validator's children in the Turbine tree for each of the provided shreds. @@ -205,7 +206,8 @@ fd_shred_dest_compute_children( fd_shred_dest_t * sdest, ulong out_stride, ulong fanout, ulong dest_cnt, - ulong * opt_max_dest_cnt ); + ulong * opt_max_dest_cnt, + int use_chacha8 ); /* fd_shred_dest_idx_to_dest maps a destination index (as produced by fd_shred_dest_compute_children or fd_shred_dest_compute_first) to an diff --git a/src/disco/shred/fd_shred_tile.c b/src/disco/shred/fd_shred_tile.c index c27ede0a8fe..afb5c7fd9a6 100644 --- a/src/disco/shred/fd_shred_tile.c +++ b/src/disco/shred/fd_shred_tile.c @@ -16,6 +16,8 @@ #include "../../flamenco/leaders/fd_leaders.h" #include "../../util/net/fd_net_headers.h" #include "../../flamenco/gossip/fd_gossip_types.h" +#include "../../flamenco/types/fd_types.h" +#include "../../flamenco/runtime/sysvar/fd_sysvar_epoch_schedule.h" /* The shred tile handles shreds from two data sources: shreds generated from microblocks from the banking tile, and shreds retransmitted from @@ -260,6 +262,26 @@ typedef struct { uchar block_ids[ BLOCK_IDS_TABLE_CNT ][ FD_SHRED_MERKLE_ROOT_SZ ]; } fd_shred_ctx_t; +/* shred features are generally considered active at the epoch *following* + the epoch in which the feature gate is activated. + fd_shred_check_feature_activation() performs the check and it's equivalent + to the rust functions check_feature_activation(): + https://github.com/anza-xyz/agave/blob/v3.1.4/turbine/src/cluster_nodes.rs#L771 + https://github.com/anza-xyz/agave/blob/v3.1.4/core/src/shred_fetch_stage.rs#L456 */ +static inline int +fd_shred_check_feature_activation( ulong shred_slot, ulong feature_slot, fd_shred_ctx_t * ctx ) { + fd_epoch_leaders_t const * lsched = fd_stake_ci_get_lsched_for_slot( ctx->stake_ci, shred_slot ); + if( lsched==NULL ) { + /* when the shred slot is out of a reasonable range it won't be propagated, + so the feature activation doesn't really matter */ + return 0; + } + fd_epoch_schedule_t default_schedule[1] = {{ lsched->slot_cnt, lsched->slot_cnt, 0, 0, 0 }}; + ulong shred_epoch = fd_slot_to_epoch( default_schedule, shred_slot, NULL ); + ulong feature_epoch = fd_slot_to_epoch( default_schedule, feature_slot, NULL ); + return feature_epoch < shred_epoch; +} + FD_FN_CONST static inline ulong scratch_align( void ) { return 128UL; @@ -920,7 +942,8 @@ after_frag( fd_shred_ctx_t * ctx, the shred, but still send it to the blockstore. */ fd_shred_dest_t * sdest = fd_stake_ci_get_sdest_for_slot( ctx->stake_ci, shred->slot ); if( FD_UNLIKELY( !sdest ) ) break; - fd_shred_dest_idx_t * dests = fd_shred_dest_compute_children( sdest, &shred, 1UL, ctx->scratchpad_dests, 1UL, fanout, fanout, max_dest_cnt ); + int use_chacha8 = fd_shred_check_feature_activation( shred->slot, ctx->features_activation->switch_to_chacha8_turbine, ctx ); + fd_shred_dest_idx_t * dests = fd_shred_dest_compute_children( sdest, &shred, 1UL, ctx->scratchpad_dests, 1UL, fanout, fanout, max_dest_cnt, use_chacha8 ); if( FD_UNLIKELY( !dests ) ) break; for( ulong i=0UL; iadtl_dests_retransmit_cnt; i++ ) send_shred( ctx, stem, *out_shred, ctx->adtl_dests_retransmit+i, ctx->tsorig ); @@ -1120,6 +1143,7 @@ after_frag( fd_shred_ctx_t * ctx, if( FD_UNLIKELY( !k ) ) return; fd_shred_dest_t * sdest = fd_stake_ci_get_sdest_for_slot( ctx->stake_ci, new_shreds[ 0 ]->slot ); if( FD_UNLIKELY( !sdest ) ) return; + int use_chacha8 = fd_shred_check_feature_activation( new_shreds[ 0 ]->slot, ctx->features_activation->switch_to_chacha8_turbine, ctx ); ulong out_stride; ulong max_dest_cnt[1]; @@ -1132,14 +1156,14 @@ after_frag( fd_shred_ctx_t * ctx, /* In the case of feature activation, the fanout used below is the same as the one calculated/modified previously at the beginning of after_frag() for IN_KIND_NET in this slot. */ - dests = fd_shred_dest_compute_children( sdest, new_shreds, k, ctx->scratchpad_dests, k, fanout, fanout, max_dest_cnt ); + dests = fd_shred_dest_compute_children( sdest, new_shreds, k, ctx->scratchpad_dests, k, fanout, fanout, max_dest_cnt, use_chacha8 ); } else { for( ulong i=0UL; iadtl_dests_leader_cnt; j++ ) send_shred( ctx, stem, new_shreds[ i ], ctx->adtl_dests_leader+j, ctx->tsorig ); } out_stride = 1UL; *max_dest_cnt = 1UL; - dests = fd_shred_dest_compute_first ( sdest, new_shreds, k, ctx->scratchpad_dests ); + dests = fd_shred_dest_compute_first ( sdest, new_shreds, k, ctx->scratchpad_dests, use_chacha8 ); } if( FD_UNLIKELY( !dests ) ) return; diff --git a/src/disco/shred/fd_stake_ci.c b/src/disco/shred/fd_stake_ci.c index c7e6d0e6ba5..7d133768167 100644 --- a/src/disco/shred/fd_stake_ci.c +++ b/src/disco/shred/fd_stake_ci.c @@ -4,7 +4,7 @@ #define SORT_NAME sort_pubkey #define SORT_KEY_T fd_shred_dest_weighted_t -#define SORT_BEFORE(a,b) (memcmp( (a).pubkey.uc, (b).pubkey.uc, 32UL )<0) +#define SORT_BEFORE(a,b) (memcmp( (a).pubkey.uc, (b).pubkey.uc, 32UL )>0) #include "../../util/tmpl/fd_sort.c" #define SORT_NAME sort_weights_by_stake_id diff --git a/src/disco/shred/test_shred_dest.c b/src/disco/shred/test_shred_dest.c index 977c77459c1..b6eb8a99382 100644 --- a/src/disco/shred/test_shred_dest.c +++ b/src/disco/shred/test_shred_dest.c @@ -53,7 +53,7 @@ test_compute_first_matches_agave( void ) { shred->variant = fd_shred_variant( type==0 ? FD_SHRED_TYPE_MERKLE_DATA : FD_SHRED_TYPE_MERKLE_CODE, 2 ); for( ulong idx=(ulong)(type+1); idx<67UL; idx += 3UL ) { shred->idx = (uint)idx; - FD_TEST( fd_shred_dest_compute_first( sdest, shred_ptr, 1UL, result ) ); + FD_TEST( fd_shred_dest_compute_first( sdest, shred_ptr, 1UL, result, 0 /*use_chacha8*/ ) ); fd_shred_dest_weighted_t const * rresult = fd_shred_dest_idx_to_dest( sdest, *result ); /* The test stores a 0 pubkey when we don't know the contact info even if we know the pubkey. */ @@ -106,7 +106,7 @@ test_compute_children_matches_agave( void ) { for( ulong idx=(ulong)(type+1); idx<67UL; idx += 3UL ) { shred->idx = (uint)idx; ulong max_dest_cnt[1] = { 0UL }; - FD_TEST( fd_shred_dest_compute_children( sdest, shred_ptr, 1UL, result, 1UL, 200UL, 200UL, max_dest_cnt ) ); + FD_TEST( fd_shred_dest_compute_children( sdest, shred_ptr, 1UL, result, 1UL, 200UL, 200UL, max_dest_cnt, 0 /*use_chacha8*/ ) ); ulong answer_cnt = ans_ul[j++]; FD_TEST( *max_dest_cnt == answer_cnt ); @@ -150,13 +150,13 @@ test_distribution_is_tree( fd_shred_dest_weighted_t const * info, ulong cnt, fd_ ulong dest_cnt = 0UL; if( !memcmp( &(info[src_idx].pubkey), leader, 32UL ) ) { //FD_LOG_NOTICE(( "%lu is leader", src_idx )); - FD_TEST( out==fd_shred_dest_compute_first( sdest, shred_ptr, 1UL, out ) ); + FD_TEST( out==fd_shred_dest_compute_first( sdest, shred_ptr, 1UL, out, 0 /*use_chacha8*/ ) ); FD_TEST( !hit[ src_idx ] ); hit[ src_idx ] = 1; dest_cnt = 1UL; } else { //FD_LOG_NOTICE(( "%lu is not leader", src_idx )); - FD_TEST( out==fd_shred_dest_compute_children( sdest, shred_ptr, 1UL, out, 1UL, fanout, fanout, &dest_cnt ) ); + FD_TEST( out==fd_shred_dest_compute_children( sdest, shred_ptr, 1UL, out, 1UL, fanout, fanout, &dest_cnt, 0 /*use_chacha8*/ ) ); } for( ulong i=0; i=dcnt_t ); /* == in the good case */ if( FD_UNLIKELY( o_trunc==NULL ) ) { @@ -478,23 +478,44 @@ test_performance( void ) { } dt = -fd_log_wallclock(); -#define TEST_CNT 1000000 +#define TEST_CNT 100000 for( ulong j=0UL; jepoch = 123UL; + stake_msg->start_slot = 0UL; + stake_msg->slot_cnt = 432000UL; + stake_msg->excluded_stake = 0UL; + stake_msg->vote_keyed_lsched = 0UL; /* Use identity-keyed leader schedule */ + + /* Count staked nodes and build stake weights */ + ulong staked_cnt = 0UL; + for( ulong i=0UL; i<20UL; i++ ) { + if( CLUSTER_NODES[i].stake > 0UL ) { + memcpy( stake_msg->weights[staked_cnt].id_key.uc, pubkeys[i].uc, 32UL ); + memcpy( stake_msg->weights[staked_cnt].vote_key.uc, pubkeys[i].uc, 32UL ); + stake_msg->weights[staked_cnt].stake = CLUSTER_NODES[i].stake; + staked_cnt++; + } + } + stake_msg->staked_cnt = staked_cnt; + + FD_LOG_NOTICE(( "Staked nodes: %lu / 20", staked_cnt )); + + /* Process stake message */ + fd_stake_ci_stake_msg_init( stake_ci, stake_msg ); + fd_stake_ci_stake_msg_fini( stake_ci ); + + /* Add destination contact info for all nodes */ + fd_shred_dest_weighted_t * dest_info = fd_stake_ci_dest_add_init( stake_ci ); + for( ulong i=0UL; i<20UL; i++ ) { + memcpy( dest_info[i].pubkey.uc, pubkeys[i].uc, 32UL ); + dest_info[i].stake_lamports = CLUSTER_NODES[i].stake; + dest_info[i].ip4 = (uint)(i+1); /* Dummy IP */ + dest_info[i].port = (ushort)(8000 + i); /* Dummy port */ + } + fd_stake_ci_dest_add_fini( stake_ci, 20UL ); + + /* Get leader schedule for testing */ + fd_epoch_leaders_t * lsched = fd_stake_ci_get_lsched_for_slot( stake_ci, EXPECTED_LEADERS[0].slot ); + FD_TEST( lsched ); + + /* Test: Leader schedule */ + FD_LOG_NOTICE(( "\n=== Testing Leader Schedule ===" )); + ulong leader_passed = 0UL; + ulong leader_failed = 0UL; + + for( ulong i=0UL; i<9UL; i++ ) { + fd_pubkey_t const * slot_leader = fd_epoch_leaders_get( lsched, EXPECTED_LEADERS[i].slot ); + + uchar expected_pubkey[32]; + uchar * decode_result = fd_base58_decode_32( EXPECTED_LEADERS[i].pubkey_base58, expected_pubkey ); + FD_TEST( decode_result != NULL ); + + if( !memcmp( slot_leader->uc, expected_pubkey, 32UL ) ) { + leader_passed++; + FD_LOG_NOTICE(( " PASS: slot=%lu -> leader=%s", + EXPECTED_LEADERS[i].slot, EXPECTED_LEADERS[i].pubkey_base58 )); + } else { + leader_failed++; + char leader_str[FD_BASE58_ENCODED_32_SZ]; + fd_base58_encode_32( slot_leader->uc, NULL, leader_str ); + FD_LOG_WARNING(( " FAIL: slot=%lu, expected leader=%s, got leader=%s", + EXPECTED_LEADERS[i].slot, EXPECTED_LEADERS[i].pubkey_base58, leader_str )); + } + } + + FD_LOG_NOTICE(( "\nLeader schedule: %lu passed, %lu failed out of 9 tests", + leader_passed, leader_failed )); + + /* Test 2: Broadcast node */ + FD_LOG_NOTICE(( "\n=== Testing Broadcast Node ===" )); + + /* Get the leader for the test slot */ + fd_pubkey_t const * test_slot_leader = fd_epoch_leaders_get( lsched, expected_broadcast->slot ); + FD_TEST( test_slot_leader ); + + char test_slot_leader_str[FD_BASE58_ENCODED_32_SZ]; + fd_base58_encode_32( test_slot_leader->uc, NULL, test_slot_leader_str ); + FD_LOG_NOTICE(( "Leader for slot %lu: %s", expected_broadcast->slot, test_slot_leader_str )); + + /* Get shred dest for the test slot */ + fd_shred_dest_t * sdest = fd_stake_ci_get_sdest_for_slot( stake_ci, expected_broadcast->slot ); + FD_TEST( sdest ); + + /* Create test shred */ + fd_shred_t shred[1]; + shred->slot = expected_broadcast->slot; + shred->variant = fd_shred_variant( + expected_broadcast->is_data ? FD_SHRED_TYPE_MERKLE_DATA : FD_SHRED_TYPE_MERKLE_CODE, 2 ); + shred->idx = expected_broadcast->shred_index; + + fd_shred_t const * shred_ptr[1] = { shred }; + + /* Compute broadcast peer */ + fd_shred_dest_idx_t result[1]; + FD_TEST( fd_shred_dest_compute_first( sdest, shred_ptr, 1UL, result, use_chacha8 ) ); + fd_shred_dest_weighted_t const * broadcast_peer = fd_shred_dest_idx_to_dest( sdest, *result ); + + if( !broadcast_peer->ip4 ) broadcast_peer = fd_shred_dest_idx_to_dest( sdest, FD_SHRED_DEST_NO_DEST ); + + /* Encode result pubkey to base58 */ + char result_pubkey_base58[ FD_BASE58_ENCODED_32_SZ ]; + fd_base58_encode_32( broadcast_peer->pubkey.uc, NULL, result_pubkey_base58 ); + + /* Decode expected pubkey */ + uchar expected_broadcast_pubkey[32]; + uchar * decode_result = fd_base58_decode_32( expected_broadcast->expected_broadcast_pubkey, expected_broadcast_pubkey ); + FD_TEST( decode_result != NULL ); + + /* Compare */ + ulong broadcast_passed = 0UL; + ulong broadcast_failed = 0UL; + + if( !memcmp( broadcast_peer->pubkey.uc, expected_broadcast_pubkey, 32UL ) ) { + broadcast_passed++; + FD_LOG_NOTICE(( " PASS: slot=%lu, shred_idx=%u -> broadcast=%s", + expected_broadcast->slot, expected_broadcast->shred_index, result_pubkey_base58 )); + } else { + broadcast_failed++; + FD_LOG_WARNING(( " FAIL: slot=%lu, shred_idx=%u, expected broadcast=%s, got broadcast=%s", + expected_broadcast->slot, expected_broadcast->shred_index, + expected_broadcast->expected_broadcast_pubkey, result_pubkey_base58 )); + } + + FD_LOG_NOTICE(( "\nBroadcast node: %lu passed, %lu failed out of 1 test", + broadcast_passed, broadcast_failed )); + + /* Test 3: Turbine tree children */ + FD_LOG_NOTICE(( "\n=== Testing Turbine Tree Children ===" )); + + /* To compute children from broadcast peer's perspective, we need to recreate stake_ci + with the broadcast peer as the identity */ + fd_stake_ci_t * stake_ci_broadcast = fd_stake_ci_join( fd_stake_ci_new( _stake_ci_broadcast, &broadcast_peer->pubkey ) ); + FD_TEST( stake_ci_broadcast ); + + /* Process stake message */ + fd_stake_ci_stake_msg_init( stake_ci_broadcast, stake_msg ); + fd_stake_ci_stake_msg_fini( stake_ci_broadcast ); + + /* Add destination contact info for all nodes */ + fd_shred_dest_weighted_t * dest_info_broadcast = fd_stake_ci_dest_add_init( stake_ci_broadcast ); + for( ulong i=0UL; i<20UL; i++ ) { + memcpy( dest_info_broadcast[i].pubkey.uc, pubkeys[i].uc, 32UL ); + dest_info_broadcast[i].stake_lamports = CLUSTER_NODES[i].stake; + dest_info_broadcast[i].ip4 = (uint)(i+1); /* Dummy IP */ + dest_info_broadcast[i].port = (ushort)(8000 + i); /* Dummy port */ + } + fd_stake_ci_dest_add_fini( stake_ci_broadcast, 20UL ); + + /* Get shred dest for the test slot from broadcast peer's perspective */ + fd_shred_dest_t * sdest_broadcast = fd_stake_ci_get_sdest_for_slot( stake_ci_broadcast, expected_broadcast->slot ); + FD_TEST( sdest_broadcast ); + + ulong fanout = 10UL; + fd_shred_dest_idx_t children_result[10]; + ulong max_dest_cnt = 0UL; + + /* Compute children for the test shred from the broadcast peer's perspective + For single shred, use out_stride=1 so results are at children_result[0..9] */ + fd_shred_dest_idx_t * children_ptr = fd_shred_dest_compute_children( + sdest_broadcast, shred_ptr, 1UL, children_result, /*out_stride=*/1UL, fanout, fanout, &max_dest_cnt, use_chacha8 ); + FD_TEST( children_ptr ); + + FD_LOG_NOTICE(( "Computed %lu children (max_dest_cnt=%lu)", fanout, max_dest_cnt )); + + ulong children_passed = 0UL; + ulong children_failed = 0UL; + + for( ulong i=0UL; iip4 ) { + FD_LOG_WARNING(( " SKIP: child[%lu] is NO_DEST or has no IP", i )); + children_failed++; + continue; + } + + char child_pubkey_base58[ FD_BASE58_ENCODED_32_SZ ]; + fd_base58_encode_32( child->pubkey.uc, NULL, child_pubkey_base58 ); + + /* Decode expected child pubkey */ + uchar expected_child_pubkey[32]; + uchar * child_decode_result = fd_base58_decode_32( expected_first_layer[i].pubkey_base58, expected_child_pubkey ); + FD_TEST( child_decode_result != NULL ); + + if( !memcmp( child->pubkey.uc, expected_child_pubkey, 32UL ) ) { + children_passed++; + if( i < 3UL ) { /* Print first few */ + FD_LOG_NOTICE(( " PASS: child[%lu] -> %s", i, child_pubkey_base58 )); + } + } else { + children_failed++; + FD_LOG_WARNING(( " FAIL: child[%lu], expected=%s, got=%s", + i, expected_first_layer[i].pubkey_base58, child_pubkey_base58 )); + } + } + + FD_LOG_NOTICE(( "\nTurbine tree children: %lu passed, %lu failed out of 10 tests", + children_passed, children_failed )); + + /* Clean up broadcast stake_ci */ + fd_stake_ci_delete( fd_stake_ci_leave( stake_ci_broadcast ) ); + + /* Test 4: Second layer of turbine tree */ + FD_LOG_NOTICE(( "\n=== Testing Turbine Tree Second Layer ===" )); + + ulong second_layer_passed = 0UL; + ulong second_layer_failed = 0UL; + ulong second_layer_child_idx = 0UL; + + /* For each first-layer child, compute their children */ + for( ulong parent_idx=0UL; parent_idx<10UL; parent_idx++ ) { + /* children_result was computed using sdest_broadcast, so decode using sdest_broadcast */ + fd_shred_dest_weighted_t const * parent = fd_shred_dest_idx_to_dest( sdest_broadcast, children_result[parent_idx] ); + + /* Expected number of children for this parent */ + ulong expected_num_children = expected_first_layer[parent_idx].num_children; + + if( expected_num_children == 0UL ) { + /* No children expected, skip */ + continue; + } + + /* Create stake_ci for this parent */ + fd_stake_ci_t * parent_stake_ci = fd_stake_ci_join( fd_stake_ci_new( _stake_ci_broadcast, &parent->pubkey ) ); + FD_TEST( parent_stake_ci ); + + /* Process stake message */ + fd_stake_ci_stake_msg_init( parent_stake_ci, stake_msg ); + fd_stake_ci_stake_msg_fini( parent_stake_ci ); + + /* Add destination contact info for all nodes */ + fd_shred_dest_weighted_t * parent_dest_info = fd_stake_ci_dest_add_init( parent_stake_ci ); + for( ulong i=0UL; i<20UL; i++ ) { + memcpy( parent_dest_info[i].pubkey.uc, pubkeys[i].uc, 32UL ); + parent_dest_info[i].stake_lamports = CLUSTER_NODES[i].stake; + parent_dest_info[i].ip4 = (uint)(i+1); /* Dummy IP */ + parent_dest_info[i].port = (ushort)(8000 + i); /* Dummy port */ + } + fd_stake_ci_dest_add_fini( parent_stake_ci, 20UL ); + + /* Get shred dest for the test slot from parent's perspective */ + fd_shred_dest_t * parent_sdest = fd_stake_ci_get_sdest_for_slot( parent_stake_ci, expected_broadcast->slot ); + FD_TEST( parent_sdest ); + + /* Compute children for this parent */ + fd_shred_dest_idx_t parent_children_result[10]; + ulong parent_max_dest_cnt = 0UL; + fd_shred_dest_idx_t * parent_children_ptr = fd_shred_dest_compute_children( + parent_sdest, shred_ptr, 1UL, parent_children_result, /*out_stride=*/1UL, fanout, fanout, &parent_max_dest_cnt, use_chacha8 ); + FD_TEST( parent_children_ptr ); + + /* Verify children */ + for( ulong child_idx=0UL; child_idxip4 ) { + second_layer_failed++; + continue; + } + + /* Decode expected child pubkey */ + uchar expected_child_pubkey[32]; + uchar * decode_result = fd_base58_decode_32( expected_second_layer[second_layer_child_idx].child_pubkey_base58, expected_child_pubkey ); + FD_TEST( decode_result != NULL ); + + if( !memcmp( child->pubkey.uc, expected_child_pubkey, 32UL ) ) { + second_layer_passed++; + if( parent_idx < 3UL ) { /* Print first few */ + char child_str[ FD_BASE58_ENCODED_32_SZ ]; + fd_base58_encode_32( child->pubkey.uc, NULL, child_str ); + FD_LOG_NOTICE(( " PASS: parent[%lu] child[%lu] -> %s", parent_idx, child_idx, child_str )); + } + } else { + second_layer_failed++; + char child_str[ FD_BASE58_ENCODED_32_SZ ]; + fd_base58_encode_32( child->pubkey.uc, NULL, child_str ); + FD_LOG_WARNING(( " FAIL: parent[%lu] child[%lu], expected=%s, got=%s", + parent_idx, child_idx, expected_second_layer[second_layer_child_idx].child_pubkey_base58, child_str )); + } + + second_layer_child_idx++; + } + + /* Clean up parent stake_ci */ + fd_stake_ci_delete( fd_stake_ci_leave( parent_stake_ci ) ); + } + + FD_LOG_NOTICE(( "\nTurbine tree second layer: %lu passed, %lu failed out of 8 tests", + second_layer_passed, second_layer_failed )); + + /* Clean up stake_ci (this also cleans up lsched and sdest) */ + fd_stake_ci_delete( fd_stake_ci_leave( stake_ci ) ); + + /* Print summary */ + FD_LOG_NOTICE(( "\n=== Summary ===" )); + FD_LOG_NOTICE(( "Leader schedule: %lu/%lu passed", leader_passed, 9UL )); + FD_LOG_NOTICE(( "Broadcast node: %lu/%lu passed", broadcast_passed, 1UL )); + FD_LOG_NOTICE(( "Turbine tree children (layer 1): %lu/%lu passed", children_passed, 10UL )); + FD_LOG_NOTICE(( "Turbine tree children (layer 2): %lu/%lu passed", second_layer_passed, 8UL )); + + if( leader_failed > 0UL || broadcast_failed > 0UL || children_failed > 0UL || second_layer_failed > 0UL ) { + FD_LOG_WARNING(( "Some tests do not match Rust implementation!" )); + } else { + FD_LOG_NOTICE(( "SUCCESS: All tests match Rust implementation!" )); + } + FD_TEST(!( leader_failed > 0UL || broadcast_failed > 0UL || children_failed > 0UL || second_layer_failed > 0UL )); + + FD_LOG_NOTICE(( "\n=== Test Complete ===" )); +} + +int +main( int argc, char ** argv ) { + fd_boot( &argc, &argv ); + + /* Test with ChaCha20 */ + test_shred_dest_conformance( + 0 /* use_chacha8 */, + &EXPECTED_BROADCAST_CHACHA20, + EXPECTED_FIRST_LAYER_CHACHA20, + 10, + EXPECTED_SECOND_LAYER_CHACHA20, + 8 + ); + + /* Test with ChaCha8 */ + test_shred_dest_conformance( + 1 /* use_chacha8 */, + &EXPECTED_BROADCAST_CHACHA8, + EXPECTED_FIRST_LAYER_CHACHA8, + 10, + EXPECTED_SECOND_LAYER_CHACHA8, + 8 + ); + + FD_LOG_NOTICE(( "pass" )); + fd_halt(); + return 0; +} + +/* ============================================================================ + RUST REFERENCE IMPLEMENTATION + + This is the Rust code that generates the expected test data above. + + USAGE: + - Add this code to agave/turbine/src/cluster_nodes.rs inside the #[cfg(test)] mod tests block + - Run with: cd agave/turbine && cargo test test_deterministic_cluster_conformance -- --nocapture + - Copy the output between === START C CODE === and === END C CODE === markers + + ============================================================================ */ + +/* + // Inside #[cfg(test)] mod tests { ... } + + // Conformance tests to generate deterministic test data for C + mod conformance_tests { + use solana_ledger::leader_schedule::IdentityKeyedLeaderSchedule; + use super::*; + use solana_gossip::crds_value::CrdsValue; + use solana_gossip::crds_data::CrdsData; + use solana_gossip::crds::GossipRoute; + use solana_hash::Hash as SolanaHash; + use solana_ledger::shred::{ProcessShredsStats, ReedSolomonCache, Shredder}; + use rand::{SeedableRng, rngs::StdRng}; + use test_case::test_case; + + pub fn make_deterministic_test_cluster( + num_nodes: usize, + ) -> ( + Vec, + HashMap, // stakes + ClusterInfo, + ) { + let mut nodes: Vec<_> = (0..num_nodes) + .map(|i| { + let mut seed = [0u8; 32]; + seed[0] = i as u8; + let pubkey = Pubkey::new_from_array(seed); + GossipContactInfo::new_localhost(&pubkey, timestamp()) + }) + .collect(); + + // Create a deterministic keypair for node[0] using a seeded RNG + let mut rng_seed = [0u8; 32]; + rng_seed[0] = 42; // Fixed seed for determinism + let mut rng = StdRng::from_seed(rng_seed); + use rand::RngCore; + let mut keypair_seed = [0u8; 32]; + rng.fill_bytes(&mut keypair_seed); + let keypair = Arc::new(Keypair::new_from_array(keypair_seed)); + + // Update node[0] to use this keypair's pubkey + nodes[0] = GossipContactInfo::new_localhost(&keypair.pubkey(), timestamp()); + let this_node = nodes[0].clone(); + + let stakes: HashMap = nodes + .iter() + .enumerate() + .map(|(i, node)| { + // 70% staked nodes, 30% unstaked + if i < (num_nodes * 7 / 10) { + (*node.pubkey(), 100*(i/2 + 1) as u64) // Assign increasing stakes + } else { + (*node.pubkey(), 0) // Unstaked + } + }) + .collect(); + + let cluster_info = ClusterInfo::new(this_node, keypair, SocketAddrSpace::Unspecified); + { + let now = timestamp(); + let gossip_keypair = Keypair::new(); + let mut gossip_crds = cluster_info.gossip.crds.write().unwrap(); + // First node is pushed to crds table by ClusterInfo constructor. + for node in nodes.iter().skip(1) { + let node = CrdsData::from(node); + let node = CrdsValue::new(node, &gossip_keypair); + let _ = gossip_crds.insert(node, now, GossipRoute::LocalMessage); + } + } + nodes[1..].shuffle(&mut rng); + (nodes, stakes, cluster_info) + } + + #[test_case(true)] // chacha8 + #[test_case(false)] // chacha20 + fn test_deterministic_cluster_conformance(use_chacha8: bool) { + let num_nodes = 20; + let (nodes, stakes, cluster_info) = make_deterministic_test_cluster(num_nodes); + let slot_leader = cluster_info.id(); + + let rng_name = if use_chacha8 { "CHACHA8" } else { "CHACHA20" }; + + // Output common data only once (for ChaCha20 test) + if !use_chacha8 { + // Output cluster nodes, leader schedule... + } + + // Generate leader schedule for epoch 123 + let epoch = 123u64; + let slot_cnt = 432000u64; + let leader_schedule = IdentityKeyedLeaderSchedule::new(&stakes, epoch, slot_cnt, 4); + + // Find a slot where slot_leader (the first node) is actually the leader + let mut broadcast_test_slot = 0u64; + for slot in 0..slot_cnt { + let leader = &leader_schedule[slot]; + if leader == &slot_leader { + broadcast_test_slot = slot; + break; + } + } + + // Create cluster nodes for broadcast + let cluster_nodes = new_cluster_nodes::( + &cluster_info, + ClusterType::Development, + &stakes, + use_chacha8, + ); + + // Create a test shred + let shred = Shredder::new(broadcast_test_slot, 1, 0, 0) + .unwrap() + .entries_to_merkle_shreds_for_tests( + &Keypair::new(), + &[], + true, + SolanaHash::default(), + 0, + 0, + &ReedSolomonCache::default(), + &mut ProcessShredsStats::default(), + ) + .0 + .pop() + .unwrap(); + + // Compute turbine tree starting from the leader + let fanout = 10usize; + let mut weighted_shuffle = cluster_nodes.weighted_shuffle.clone(); + let mut chacha_rng = TurbineRng::new_seeded(&slot_leader, &shred.id(), use_chacha8); + let shuffled_nodes: Vec<&Node> = weighted_shuffle + .shuffle(&mut chacha_rng) + .map(|i| &cluster_nodes.nodes[i]) + .collect(); + + // The root node is shuffled_nodes[0] - this is who the leader sends to first + let root_pubkey = *shuffled_nodes[0].pubkey(); + + // Get first layer: root's children + let (_, root_children) = get_retransmit_peers( + fanout, + |n: &Node| n.pubkey() == &root_pubkey, + shuffled_nodes.clone(), + ); + let root_children_vec: Vec = root_children.take(fanout).map(|n| *n.pubkey()).collect(); + + // Second layer: compute children for each first-layer child + let mut second_layer_data: Vec<(usize, Pubkey, Vec)> = Vec::new(); + for (idx, child_pk) in root_children_vec.iter().enumerate() { + // Use the SAME shuffle as everyone else (seeded with slot_leader) + // All nodes in the turbine tree use the same deterministic shuffle + let (_, child_children) = get_retransmit_peers( + fanout, + |n: &Node| n.pubkey() == child_pk, + shuffled_nodes.clone(), + ); + let child_children_vec: Vec = child_children.take(fanout).map(|n| *n.pubkey()).collect(); + second_layer_data.push((idx, *child_pk, child_children_vec)); + } + + // Verify complete coverage (all nodes covered exactly once) + let mut covered = std::collections::HashSet::new(); + covered.insert(slot_leader); // Leader has the shred + covered.insert(root_pubkey); // Root node (broadcast peer) + for child_pk in &root_children_vec { + covered.insert(*child_pk); // First layer + } + for (_idx, _parent_pk, children) in &second_layer_data { + for child_pk in children { + covered.insert(*child_pk); // Second layer + } + } + assert_eq!(covered.len(), nodes.len(), "All nodes should be covered exactly once"); + } + } // mod conformance_tests + + ============================================================================ */ diff --git a/src/util/bits/fd_uwide.h b/src/util/bits/fd_uwide.h index b5d0ca8f480..5727ca8d05f 100644 --- a/src/util/bits/fd_uwide.h +++ b/src/util/bits/fd_uwide.h @@ -88,6 +88,12 @@ static inline void fd_uwide_mul( ulong * FD_RESTRICT _zh, ulong * FD_RESTRICT _zl, ulong x, ulong y ) { +#if FD_HAS_INT128 + /* Compiles to one mulx instruction */ + uint128 res = (uint128)x * (uint128)y; + *_zh = (ulong)(res>>64); + *_zl = (ulong) res; +#else ulong x1 = x>>32; ulong x0 = (ulong)(uint)x; /* both 2^32-1 @ worst case (x==y==2^64-1) */ ulong y1 = y>>32; ulong y0 = (ulong)(uint)y; /* both 2^32-1 @ worst case */ @@ -103,6 +109,7 @@ fd_uwide_mul( ulong * FD_RESTRICT _zh, ulong * FD_RESTRICT _zl, /* zh 2^64 + zl == 2^128-2^65+1 @ worst case */ *_zh = zh; *_zl = zl; +#endif } /* fd_uwide_find_msb returns floor( log2 ) exactly. Assumes