diff --git a/agave b/agave index 07e96a2b02..65f12d1c1b 160000 --- a/agave +++ b/agave @@ -1 +1 @@ -Subproject commit 07e96a2b02486a434ff472437b52d48942ef4cda +Subproject commit 65f12d1c1bc43e1e12a6a4fd0931322f0f57528a diff --git a/src/app/fdctl/run/tiles/fd_replay.c b/src/app/fdctl/run/tiles/fd_replay.c index 3db2257385..46756b3101 100644 --- a/src/app/fdctl/run/tiles/fd_replay.c +++ b/src/app/fdctl/run/tiles/fd_replay.c @@ -196,7 +196,7 @@ struct fd_replay_tile_ctx { fd_forks_t * forks; fd_ghost_t * ghost; fd_tower_t * tower; - fd_voter_t * voter; + fd_voter_t voter[1]; fd_bank_hash_cmp_t * bank_hash_cmp; /* Tpool */ @@ -288,7 +288,6 @@ scratch_footprint( fd_topo_tile_t const * tile FD_PARAM_UNUSED ) { l = FD_LAYOUT_APPEND( l, fd_forks_align(), fd_forks_footprint( FD_BLOCK_MAX ) ); l = FD_LAYOUT_APPEND( l, fd_ghost_align(), fd_ghost_footprint( FD_BLOCK_MAX, FD_VOTER_MAX ) ); l = FD_LAYOUT_APPEND( l, fd_tower_align(), fd_tower_footprint() ); - l = FD_LAYOUT_APPEND( l, fd_voter_align(), fd_voter_footprint() ); l = FD_LAYOUT_APPEND( l, fd_bank_hash_cmp_align(), fd_bank_hash_cmp_footprint( ) ); for( ulong i = 0UL; itower, fork, ctx->acc_mgr, ctx->blockstore, ctx->ghost ); + fd_ghost_node_t const * ghost_node = fd_ghost_insert( ctx->ghost, parent_slot, curr_slot ); + #if FD_GHOST_USE_HANDHOLDING + if( FD_UNLIKELY( !ghost_node ) ) { + FD_LOG_ERR(( "failed to insert ghost node %lu", fork->slot )); + } + #endif + // fd_tower_fork_update( ctx->tower, fork, ctx->acc_mgr, ctx->blockstore, ctx->ghost ); + fd_tower_fork_update( ctx->tower, ctx->blockstore, ctx->ghost, ctx->funk, fork->slot_ctx.funk_txn ); /* Check which fork to reset to for pack. */ @@ -1163,8 +1169,9 @@ after_frag( fd_replay_tile_ctx_t * ctx, } fd_forks_print( ctx->forks ); - fd_ghost_print( ctx->ghost ); + fd_ghost_print( ctx->ghost, fd_ghost_root( ctx-> ghost ) ); fd_tower_print( ctx->tower ); + fd_fork_t const * vote_fork = fd_tower_vote_fork( ctx->tower, ctx->forks, ctx->acc_mgr, @@ -1174,7 +1181,7 @@ after_frag( fd_replay_tile_ctx_t * ctx, "# of vote accounts: %lu\n" "best fork: %lu\n", fd_tower_vote_accs_cnt( ctx->tower->vote_accs ), - fd_ghost_head( ctx->ghost )->slot ) ); + fd_ghost_head( ctx->ghost, fd_ghost_root( ctx->ghost ) )->slot ) ); if( FD_UNLIKELY( ctx->vote && fd_fseq_query( ctx->poh ) == ULONG_MAX ) ) { @@ -1456,12 +1463,8 @@ init_after_snapshot( fd_replay_tile_ctx_t * ctx ) { fd_fork_t * snapshot_fork = fd_forks_init( ctx->forks, ctx->slot_ctx ); FD_TEST( snapshot_fork ); - fd_tower_init( ctx->tower, - &ctx->voter->vote_acc_addr, - ctx->acc_mgr, - ctx->epoch_ctx, - snapshot_fork, - ctx->smr ); + fd_tower_init( ctx->tower, &ctx->voter->addr, ctx->funk, snapshot_fork->slot_ctx.funk_txn, ctx->smr ); + fd_tower_epoch_update( ctx->tower, ctx->epoch_ctx ); fd_ghost_init( ctx->ghost, snapshot_slot, ctx->tower->total_stake ); fd_tower_print( ctx->tower ); @@ -1718,7 +1721,6 @@ unprivileged_init( fd_topo_t * topo, void * forks_mem = FD_SCRATCH_ALLOC_APPEND( l, fd_forks_align(), fd_forks_footprint( FD_BLOCK_MAX ) ); void * ghost_mem = FD_SCRATCH_ALLOC_APPEND( l, fd_ghost_align(), fd_ghost_footprint( FD_BLOCK_MAX, FD_VOTER_MAX ) ); void * tower_mem = FD_SCRATCH_ALLOC_APPEND( l, fd_tower_align(), fd_tower_footprint() ); - void * voter_mem = FD_SCRATCH_ALLOC_APPEND( l, fd_voter_align(), fd_voter_footprint() ); void * bank_hash_cmp_mem = FD_SCRATCH_ALLOC_APPEND( l, fd_bank_hash_cmp_align(), fd_bank_hash_cmp_footprint( ) ); for( ulong i = 0UL; ibmtree[i] = FD_SCRATCH_ALLOC_APPEND( l, FD_BMTREE_COMMIT_ALIGN, FD_BMTREE_COMMIT_FOOTPRINT(0) ); @@ -1908,8 +1910,7 @@ unprivileged_init( fd_topo_t * topo, /* voter */ /**********************************************************************/ - ctx->voter = fd_voter_join( fd_voter_new( voter_mem ) ); - memcpy( &ctx->voter->vote_acc_addr.uc, + memcpy( &ctx->voter->addr.uc, fd_keyload_load( tile->replay.vote_account_path, 1 ), sizeof( fd_pubkey_t ) ); memcpy( &ctx->voter->validator_identity.uc, diff --git a/src/ballet/shred/fd_shred.h b/src/ballet/shred/fd_shred.h index 557eaf22ef..2c47b5c52a 100644 --- a/src/ballet/shred/fd_shred.h +++ b/src/ballet/shred/fd_shred.h @@ -235,9 +235,9 @@ struct __attribute__((packed)) fd_shred { } code; }; }; -typedef struct fd_shred fd_shred_t; typedef struct fd_shred_data fd_shred_data_t; typedef struct fd_shred_code fd_shred_code_t; +typedef struct fd_shred fd_shred_t; FD_PROTOTYPES_BEGIN diff --git a/src/choreo/fd_choreo_base.h b/src/choreo/fd_choreo_base.h index 6aa4b8f8f4..a2d263a87a 100644 --- a/src/choreo/fd_choreo_base.h +++ b/src/choreo/fd_choreo_base.h @@ -24,7 +24,7 @@ #define FD_VOTER_MAX (1UL << 12UL) /* the maximum # of unique voters ie. node pubkeys. */ #define FD_EQVOCSAFE_PCT (0.52) #define FD_CONFIRMED_PCT (2.0 / 3.0) -#define FD_FINALIZED_PCT FD_CONFIRMED_PCT +#define FD_FINALIZED_PCT FD_CONFIRMED_PCT #define FD_SLOT_HASH_CMP(k0,k1) (fd_int_if(((k0)->slot)<((k1)->slot),-1,fd_int_if(((k0)->slot)>((k1)->slot),1),memcmp((k0),(k1),sizeof(fd_slot_hash_t)))) #define FD_SLOT_HASH_EQ(k0,k1) ((((k0)->slot)==((k1)->slot)) & !(memcmp(((k0)->hash.uc),((k1)->hash.uc),sizeof(fd_hash_t)))) #define FD_SLOT_HASH_HASH(key,seed) fd_ulong_hash( ((key)->slot) ^ ((key)->hash.ul[0]) ^ (seed) ) diff --git a/src/choreo/forks/fd_forks.c b/src/choreo/forks/fd_forks.c index a75a1f30d3..7b52112dd3 100644 --- a/src/choreo/forks/fd_forks.c +++ b/src/choreo/forks/fd_forks.c @@ -334,7 +334,7 @@ fd_forks_publish( fd_forks_t * forks, ulong slot, fd_ghost_t const * ghost ) { Optimize for unlikely because there is usually just one fork. */ - int stale = fork->slot < slot || !fd_ghost_is_descendant( ghost, fork->slot, slot ); + int stale = fork->slot < slot || !fd_ghost_is_ancestor( ghost, slot, fork->slot ); if( FD_UNLIKELY( !fork->lock && stale ) ) { FD_LOG_NOTICE(( "adding %lu to prune. root %lu", fork->slot, slot )); if( FD_LIKELY( !curr ) ) { diff --git a/src/choreo/ghost/fd_ghost.c b/src/choreo/ghost/fd_ghost.c index 4d676cdb55..445d2563bc 100644 --- a/src/choreo/ghost/fd_ghost.c +++ b/src/choreo/ghost/fd_ghost.c @@ -2,8 +2,6 @@ #include "stdio.h" #include -/* clang-format off */ - void * fd_ghost_new( void * shmem, ulong node_max, ulong vote_max, ulong seed ) { @@ -45,8 +43,14 @@ fd_ghost_new( void * shmem, ulong node_max, ulong vote_max, ulong seed ) { ghost->vote_pool_gaddr = fd_wksp_gaddr_fast( wksp, fd_ghost_vote_pool_join(fd_ghost_vote_pool_new( vote_pool, vote_max ) )); ghost->vote_map_gaddr = fd_wksp_gaddr_fast( wksp, fd_ghost_vote_map_join(fd_ghost_vote_map_new( vote_map, vote_max, seed ) )); - ghost->ghost_gaddr = fd_wksp_gaddr_fast( wksp, ghost ); - ghost->root_idx = fd_ghost_node_pool_idx_null( fd_ghost_node_pool( ghost ) ); + ghost->ghost_gaddr = fd_wksp_gaddr_fast( wksp, ghost ); + ghost->seed = seed; + ghost->root_idx = fd_ghost_node_pool_idx_null( fd_ghost_node_pool( ghost ) ); + ghost->total_stake = 0; + + FD_COMPILER_MFENCE(); + FD_VOLATILE( ghost->magic ) = FD_GHOST_MAGIC; + FD_COMPILER_MFENCE(); return shmem; } @@ -98,13 +102,6 @@ fd_ghost_delete( void * ghost ) { return ghost; } -#define INIT_GHOST_FAMILY( node, node_pool ) \ - do { \ - node->parent_idx = fd_ghost_node_pool_idx_null( node_pool ); \ - node->child_idx = fd_ghost_node_pool_idx_null( node_pool ); \ - node->sibling_idx = fd_ghost_node_pool_idx_null( node_pool ); \ - } while(0) - void fd_ghost_init( fd_ghost_t * ghost, ulong root, ulong total_stake ) { @@ -127,134 +124,156 @@ fd_ghost_init( fd_ghost_t * ghost, ulong root, ulong total_stake ) { return; } - fd_ghost_node_t * node = fd_ghost_node_pool_ele_acquire( node_pool ); - memset( node, 0, sizeof( fd_ghost_node_t ) ); - node->slot = root; - INIT_GHOST_FAMILY( node, node_pool ); + /* Initialize the root node from a pool element. */ - fd_ghost_node_map_ele_insert( node_map, node, node_pool ); + fd_ghost_node_t * root_ele = fd_ghost_node_pool_ele_acquire( node_pool ); + memset( root_ele, 0, sizeof( fd_ghost_node_t ) ); + root_ele->slot = root; + root_ele->next = null_idx; + root_ele->valid = 1; + root_ele->parent_idx = null_idx; + root_ele->child_idx = null_idx; + root_ele->sibling_idx = null_idx; + + /* Insert the root and record the root ele's pool idx. */ + + fd_ghost_node_map_ele_insert( node_map, root_ele, node_pool ); /* cannot fail */ ghost->root_idx = fd_ghost_node_map_idx_query( node_map, &root, null_idx, node_pool ); + + /* Sanity checks. */ + + FD_TEST( fd_ghost_root( ghost ) ); + FD_TEST( fd_ghost_root( ghost )->slot == root ); + FD_TEST( fd_ghost_query( ghost, root ) == fd_ghost_root( ghost ) ); + + /* Set total stake. */ + ghost->total_stake = total_stake; return; } -/* clang-format on */ -bool -fd_ghost_verify(fd_ghost_t const * ghost){ +int +fd_ghost_verify( fd_ghost_t const * ghost ) { if( FD_UNLIKELY( !ghost ) ) { FD_LOG_WARNING(( "NULL ghost" )); - return false; + return -1; } - fd_ghost_node_t * node_pool = fd_ghost_node_pool( ghost ); - fd_ghost_node_map_t * node_map = fd_ghost_node_map( ghost ); + if( FD_UNLIKELY( !fd_ulong_is_aligned( (ulong)ghost, fd_ghost_align() ) ) ) { + FD_LOG_WARNING(( "misaligned ghost" )); + return -1; + } - /* every element that exists in pool exists in map */ + fd_wksp_t * wksp = fd_wksp_containing( ghost ); + if( FD_UNLIKELY( !wksp ) ) { + FD_LOG_WARNING(( "ghost must be part of a workspace" )); + return -1; + } - if(!fd_ghost_node_map_verify( node_map, - fd_ghost_node_pool_used( node_pool), - node_pool )) { - return false; + if( FD_UNLIKELY( ghost->magic!=FD_GHOST_MAGIC ) ) { + FD_LOG_WARNING(( "bad magic" )); + return -1; } - /* check invariant that each parent weight is >= sum of children weights */ - fd_ghost_node_t const * parent = fd_ghost_root_node( ghost ); + fd_ghost_node_t * node_pool = fd_ghost_node_pool( ghost ); + fd_ghost_node_map_t * node_map = fd_ghost_node_map( ghost ); + + /* every element that exists in pool exists in map */ + + if( fd_ghost_node_map_verify( node_map, fd_ghost_node_pool_max( node_pool ), node_pool ) ) return -1; + + /* every node's weight is >= sum of children's weights */ + fd_ghost_node_t const * parent = fd_ghost_root( ghost ); while( parent ) { - ulong child_idx = parent->child_idx; - ulong total_weight = 0; + ulong child_idx = parent->child_idx; + ulong children_weight = 0; while( child_idx != fd_ghost_node_pool_idx_null( node_pool ) ) { fd_ghost_node_t const * child = fd_ghost_node_pool_ele( node_pool, child_idx ); - total_weight += child->weight; + children_weight += child->weight; child_idx = child->sibling_idx; } - if( FD_UNLIKELY( total_weight > parent->weight ) ) { - FD_LOG_WARNING(( "root %lu has total stake %lu but sum of children is %lu", parent->slot, parent->weight, total_weight )); - return false; + if( FD_UNLIKELY( parent->weight < children_weight ) ) { + FD_LOG_WARNING(( "[%s] invariant violation. %lu's weight: %lu < children's weight: %lu", __func__, parent->slot, parent->weight, children_weight )); + return -1; } - parent = fd_ghost_node_pool_ele( node_pool, parent->next ); } - return true; + return 0; } fd_ghost_node_t * -fd_ghost_insert( fd_ghost_t * ghost, ulong slot, ulong parent_slot ) { - FD_LOG_DEBUG(( "[ghost] node_insert: %lu. parent: %lu.", slot, parent_slot )); +fd_ghost_insert( fd_ghost_t * ghost, ulong parent_slot, ulong slot ) { + FD_LOG_DEBUG(( "[%s] slot: %lu. parent: %lu.", __func__, slot, parent_slot )); + + #if FD_GHOST_USE_HANDHOLDING + FD_TEST( ghost->magic == FD_GHOST_MAGIC ); + #endif -/* Caller promises slot >= SMR. */ fd_ghost_node_map_t * node_map = fd_ghost_node_map( ghost ); fd_ghost_node_t * node_pool = fd_ghost_node_pool( ghost ); - fd_ghost_node_t const * root = fd_ghost_root_node( ghost ); - -#if FD_GHOST_USE_HANDHOLDING - if( FD_UNLIKELY( slot < root->slot ) ) { - FD_LOG_ERR(( "slot %lu is older than ghost root %lu", slot, root->slot )); - } -#endif -/* Caller promises slot is not already in ghost. */ + ulong null_idx = fd_ghost_node_pool_idx_null( node_pool ); + fd_ghost_node_t * parent_ele = fd_ghost_node_map_ele_query( node_map, &parent_slot, NULL, node_pool ); + ulong parent_idx = fd_ghost_node_pool_idx( node_pool, parent_ele ); + fd_ghost_node_t const * root = fd_ghost_root( ghost ); -#if FD_GHOST_USE_HANDHOLDING - if( FD_UNLIKELY( fd_ghost_node_map_ele_query( node_map, - &slot, - NULL, - node_pool ) ) ) { - FD_LOG_ERR(( "slot %lu is already in ghost.", slot )); + #if FD_GHOST_USE_HANDHOLDING + if( FD_UNLIKELY( fd_ghost_query( ghost, slot ) ) ) { /* slot already in ghost */ + FD_LOG_WARNING(( "[%s] slot %lu already in ghost.", __func__, slot )); + return NULL; } -#endif - fd_ghost_node_t * parent = fd_ghost_node_map_ele_query( node_map, - &parent_slot, - NULL, - node_pool ); - ulong null_idx = fd_ghost_node_pool_idx_null( node_pool ); - ulong parent_idx = fd_ghost_node_map_idx_query( node_map, &parent_slot, null_idx, node_pool ); - -/* Caller promises parent_slot is already in ghost. */ + if( FD_UNLIKELY( !parent_ele ) ) { /* parent_slot not in ghost */ + FD_LOG_WARNING(( "[%s] missing `parent_slot` %lu.", __func__, parent_slot )); + return NULL; + } -#if FD_GHOST_USE_HANDHOLDING - if( FD_UNLIKELY( parent_idx == null_idx ) ) { - FD_LOG_ERR(( "[fd_ghost_node_insert] missing parent_slot %lu.", parent_slot )); + if( FD_UNLIKELY( !fd_ghost_node_pool_free( node_pool ) ) ) { /* ghost full */ + FD_LOG_WARNING(( "[%s] ghost full.", __func__ )); + return NULL; } -#endif - -/* Caller promises node pool has a free element. */ -#if FD_GHOST_USE_HANDHOLDING - if( FD_UNLIKELY( !fd_ghost_node_pool_free( node_pool ) ) ) { - FD_LOG_ERR(( "[ghost] node_pool full. check pruning logic." )); + if( FD_UNLIKELY( slot <= root->slot ) ) { /* slot must > root */ + FD_LOG_WARNING(( "[%s] slot %lu <= root %lu", __func__, slot, root->slot )); + return NULL; } -#endif + #endif + + fd_ghost_node_t * node_ele = fd_ghost_node_pool_ele_acquire( node_pool ); + ulong node_idx = fd_ghost_node_pool_idx( node_pool, node_ele ); - fd_ghost_node_t * node = fd_ghost_node_pool_ele_acquire( node_pool ); - memset( node, 0, sizeof( fd_ghost_node_t ) ); - node->slot = slot; - INIT_GHOST_FAMILY( node, node_pool ); + memset( node_ele, 0, sizeof(fd_ghost_node_t) ); + node_ele->slot = slot; + node_ele->next = null_idx; + node_ele->valid = 1; + node_ele->parent_idx = null_idx; + node_ele->child_idx = null_idx; + node_ele->sibling_idx = null_idx; /* Insert into the map for O(1) random access. */ - fd_ghost_node_map_ele_insert( node_map, node, node_pool ); - ulong node_idx = fd_ghost_node_map_idx_query( node_map, &slot, null_idx, node_pool ); + fd_ghost_node_map_ele_insert( node_map, node_ele, node_pool ); /* cannot fail */ /* Link node->parent. */ - node->parent_idx = parent_idx; + node_ele->parent_idx = parent_idx; /* Link parent->node and sibling->node. */ - if( FD_LIKELY( parent->child_idx == null_idx ) ) { - - /* No siblings, which means no forks. This is the likely case. */ + if( FD_LIKELY( parent_ele->child_idx == null_idx ) ) { + + /* No children yet so set as left-most child. */ - parent->child_idx = node_idx; + parent_ele->child_idx = node_idx; } else { - /* Iterate to right-most sibling. */ - fd_ghost_node_t * curr = fd_ghost_node_pool_ele( node_pool, parent->child_idx ); + /* Already have children so iterate to right-most sibling. */ + + fd_ghost_node_t * curr = fd_ghost_node_pool_ele( node_pool, parent_ele->child_idx ); while( curr->sibling_idx != null_idx ) { curr = fd_ghost_node_pool_ele( node_pool, curr->sibling_idx ); } @@ -266,13 +285,18 @@ fd_ghost_insert( fd_ghost_t * ghost, ulong slot, ulong parent_slot ) { /* Return newly-created node. */ - return node; + return node_ele; } fd_ghost_node_t const * -fd_ghost_head( fd_ghost_t const * ghost ) { +fd_ghost_head( fd_ghost_t const * ghost, fd_ghost_node_t const * node ) { + #if FD_GHOST_USE_HANDHOLDING + FD_TEST( ghost->magic == FD_GHOST_MAGIC ); + FD_TEST( node ); + #endif + fd_ghost_node_t * node_pool = fd_ghost_node_pool( ghost ); - fd_ghost_node_t const * head = fd_ghost_root_node( ghost ); + fd_ghost_node_t const * head = node; ulong null_idx = fd_ghost_node_pool_idx_null( node_pool ); while( head->child_idx != null_idx ) { @@ -280,8 +304,6 @@ fd_ghost_head( fd_ghost_t const * ghost ) { fd_ghost_node_t const * curr = head; while( curr ) { - /* clang-format off */ - head = fd_ptr_if( fd_int_if( /* if the weights are equal... */ @@ -297,8 +319,6 @@ fd_ghost_head( fd_ghost_t const * ghost ) { curr->weight > head->weight ), curr, head ); - /* clang-format on */ - curr = fd_ghost_node_pool_ele( node_pool, curr->sibling_idx ); } } @@ -311,15 +331,15 @@ fd_ghost_replay_vote( fd_ghost_t * ghost, ulong slot, fd_pubkey_t const * pubkey fd_ghost_node_map_t * node_map = fd_ghost_node_map( ghost ); fd_ghost_node_t * node_pool = fd_ghost_node_pool( ghost ); - fd_ghost_node_t const * root = fd_ghost_root_node( ghost ); + fd_ghost_node_t const * root = fd_ghost_root( ghost ); -#if FD_GHOST_USE_HANDHOLDING + #if FD_GHOST_USE_HANDHOLDING if( FD_UNLIKELY( slot < root->slot ) ) { FD_LOG_ERR(( "caller must only insert vote slots >= ghost root. vote: %lu, root: %lu", slot, root->slot )); } -#endif + #endif fd_ghost_node_t * node = fd_ghost_node_map_ele_query( node_map, &slot, @@ -380,13 +400,13 @@ fd_ghost_replay_vote( fd_ghost_t * ghost, ulong slot, fd_pubkey_t const * pubkey latest_vote->stake, latest_vote->slot )); - int cf = __builtin_usubl_overflow( node->stake, latest_vote->stake, &node->stake ); + int cf = __builtin_usubl_overflow( node->replay_stake, latest_vote->stake, &node->replay_stake ); if( FD_UNLIKELY( cf ) ) { FD_LOG_WARNING(( "[%s] sub overflow. node->stake %lu latest_vote->stake %lu", __func__, - node->stake, + node->replay_stake, latest_vote->stake )); - node->stake = 0; + node->replay_stake = 0; } fd_ghost_node_t * ancestor = node; @@ -429,11 +449,11 @@ fd_ghost_replay_vote( fd_ghost_t * ghost, ulong slot, fd_pubkey_t const * pubkey /* Propagate the vote stake up the ancestry, including updating the head. */ FD_LOG_DEBUG(( "[ghost] adding (%s, %lu, %lu)", FD_BASE58_ENC_32_ALLOCA( pubkey ), stake, latest_vote->slot )); - int cf = __builtin_uaddl_overflow( node->stake, latest_vote->stake, &node->stake ); + int cf = __builtin_uaddl_overflow( node->replay_stake, latest_vote->stake, &node->replay_stake ); if( FD_UNLIKELY( cf ) ) { FD_LOG_ERR(( "[%s] add overflow. node->stake %lu latest_vote->stake %lu", __func__, - node->stake, + node->replay_stake, latest_vote->stake )); } @@ -450,14 +470,14 @@ fd_ghost_replay_vote( fd_ghost_t * ghost, ulong slot, fd_pubkey_t const * pubkey } #if FD_GHOST_USE_HANDHOLDING - if( FD_UNLIKELY( node->stake > ghost->total_stake ) ) { + if( FD_UNLIKELY( node->replay_stake > ghost->total_stake ) ) { FD_LOG_ERR(( "[%s] invariant violation. node->stake > total stake." "slot: %lu, " "node->stake %lu, " "ghost->total_stake %lu", __func__, slot, - node->stake, + node->replay_stake, ghost->total_stake )); } #endif @@ -479,7 +499,7 @@ fd_ghost_rooted_vote( fd_ghost_t * ghost, ulong root, fd_pubkey_t const * pubkey fd_ghost_node_map_t * node_map = fd_ghost_node_map( ghost ); fd_ghost_node_t * node_pool = fd_ghost_node_pool( ghost ); - fd_ghost_node_t const * root_node = fd_ghost_root_node( ghost ); + fd_ghost_node_t const * root_node = fd_ghost_root( ghost ); #if FD_GHOST_USE_HANDHOLDING if( FD_UNLIKELY( root < root_node->slot ) ) { @@ -515,7 +535,7 @@ fd_ghost_node_t const * fd_ghost_publish( fd_ghost_t * ghost, ulong slot ) { fd_ghost_node_map_t * node_map = fd_ghost_node_map( ghost ); fd_ghost_node_t * node_pool = fd_ghost_node_pool( ghost ); - fd_ghost_node_t const * root_node = fd_ghost_root_node( ghost ); + fd_ghost_node_t const * root_node = fd_ghost_root( ghost ); ulong null_idx = fd_ghost_node_pool_idx_null( node_pool ); #if FD_GHOST_USE_HANDHOLDING @@ -640,58 +660,35 @@ fd_ghost_gca( fd_ghost_t const * ghost, ulong slot1, ulong slot2 ) { } int -fd_ghost_is_descendant( fd_ghost_t const * ghost, ulong slot, ulong ancestor_slot ) { - fd_ghost_node_t * node_pool = fd_ghost_node_pool( ghost ); - fd_ghost_node_t const * root_node = fd_ghost_root_node( ghost ); +fd_ghost_is_ancestor( fd_ghost_t const * ghost, ulong ancestor, ulong slot ) { + fd_ghost_node_t const * root = fd_ghost_root( ghost ); + fd_ghost_node_t const * curr = fd_ghost_query( ghost, slot ); - fd_ghost_node_t const * ancestor = fd_ghost_query( ghost, slot ); -#if FD_GHOST_USE_HANDHOLDING - - if( FD_UNLIKELY( ancestor_slot < root_node->slot ) ) { - FD_LOG_ERR(( "[%s] ancestor_slot %lu is older than ghost root %lu.", - __func__, - ancestor_slot, - root_node->slot )); + #if FD_GHOST_USE_HANDHOLDING + if( FD_UNLIKELY( ancestor < root->slot ) ) { + FD_LOG_WARNING(( "[%s] ancestor %lu too old. root %lu.", __func__, ancestor, root->slot )); + return 0; } - if( FD_UNLIKELY( !ancestor ) ) { - - /* Slot not found, so we won't find the ancestor. */ - - FD_LOG_WARNING(( "[%s] unable to find slot %lu in ghost.", __func__, slot )); + if( FD_UNLIKELY( !curr ) ) { + FD_LOG_WARNING(( "[%s] slot %lu not in ghost.", __func__, slot )); return 0; } -#endif + #endif - /* Look for ancestor_slot in the fork ancestry. + /* Look for `ancestor` in the fork ancestry. Stop looking when there is either no ancestry remaining or there is no reason to look further because we've searched past the - ancestor_slot. */ + `ancestor`. */ - while( FD_LIKELY( ancestor && ancestor->slot >= ancestor_slot ) ) { - if( FD_UNLIKELY( ancestor->slot == ancestor_slot ) ) return 1; /* optimize for not found */ - ancestor = fd_ghost_node_pool_ele( node_pool, ancestor->parent_idx ); + while( FD_LIKELY( curr && curr->slot >= ancestor ) ) { + if( FD_UNLIKELY( curr->slot == ancestor ) ) return 1; /* optimize for depth > 1 */ + curr = fd_ghost_node_pool_ele( fd_ghost_node_pool( ghost ), curr->parent_idx ); } return 0; /* not found */ } -fd_ghost_node_t const * -fd_ghost_root_node( fd_ghost_t const * ghost ) { - fd_ghost_node_t * node_pool = fd_ghost_node_pool( ghost ); - fd_ghost_node_t const * root = fd_ghost_node_pool_ele_const( node_pool, ghost->root_idx ); - - return root; -} - -fd_ghost_node_t const * -fd_ghost_child_node( fd_ghost_t const * ghost, fd_ghost_node_t const * parent ) { - fd_ghost_node_t * node_pool = fd_ghost_node_pool( ghost ); - fd_ghost_node_t const * child = fd_ghost_node_pool_ele_const( node_pool, parent->child_idx ); - - return child; -} - static void print( fd_ghost_t * ghost, fd_ghost_node_t const * node, int space, const char * prefix, ulong total ) { fd_ghost_node_t * node_pool = fd_ghost_node_pool( ghost ); @@ -729,20 +726,7 @@ print( fd_ghost_t * ghost, fd_ghost_node_t const * node, int space, const char * } void -fd_ghost_slot_print( fd_ghost_t * ghost, ulong slot, ulong depth ) { - fd_ghost_node_t * node_pool = fd_ghost_node_pool( ghost ); - ulong null_idx = fd_ghost_node_pool_idx_null( node_pool ); - - fd_ghost_node_t const * node = fd_ghost_query( ghost, slot ); - if( FD_UNLIKELY( !node ) ) { - FD_LOG_WARNING(( "[fd_ghost_print_node] NULL node." )); - return; - } - fd_ghost_node_t const * ancestor = node; - for( ulong i = 0; i < depth; i++ ) { - if( ancestor->parent_idx == null_idx ) break; - ancestor = fd_ghost_node_pool_ele( node_pool, ancestor->parent_idx ); - } - print( ghost, ancestor, 0, "", ghost->total_stake ); +fd_ghost_print( fd_ghost_t * ghost, fd_ghost_node_t const * node ) { + print( ghost, node, 0, "", ghost->total_stake ); printf( "\n\n" ); } diff --git a/src/choreo/ghost/fd_ghost.h b/src/choreo/ghost/fd_ghost.h index c5718f7b13..c26a37d3ef 100644 --- a/src/choreo/ghost/fd_ghost.h +++ b/src/choreo/ghost/fd_ghost.h @@ -6,17 +6,32 @@ Protocol details: - - LMD is an acronym for "latest message-driven". It denotes the - specific flavor of GHOST implementation, ie. only a - validator's latest vote counts. + - LMD is an acronym for "latest message-driven". It describes how + votes are counted when picking the best fork. In this scheme, only + a validator's latest vote counts. So if a validator votes for slot + 3 and then slot 5, the vote for 5 overwrites the vote for 3. - GHOST is an acronym for "greedy heaviest-observed subtree": - - greedy: pick the locally optimal subtree / fork based on our - current view (which may not be globally optimal). - - heaviest: pick based on the highest stake weight. - - observed: this is the validator's local view, and other - validators may have differing views. - - subtree: pick a subtree, not an individual node. + + greedy: for each depth of the tree, pick the locally optimal + child / subtree / fork. This will result in the global + optimal choice. + + heaviest: pick based on the highest stake weight. + + observed: this is the validator's local view, and other validators + may have differing views because they've observed + different votes. + + subtree: pick based on the weight of an entire subtree, not just + an individual node. For example, if slot 3 has 10 stake + and slot 5 has 5 stake, but slot 5 has two children 6 + and 7 that each have 3 stake, our weights are + + slot 3 subtree [3] = 10 + slot 5 subtree [5, 6, 7] = 11 (5+3+3) + + Therefore slot 5 would be the heaviest. In-memory representation: @@ -31,7 +46,6 @@ not prerequisite reading for understanding this implementation. */ #include "../fd_choreo_base.h" -#include /* FD_GHOST_USE_HANDHOLDING: Define this to non-zero at compile time to turn on additional runtime checks and logging. */ @@ -40,29 +54,27 @@ #define FD_GHOST_USE_HANDHOLDING 1 #endif -/* clang-format off */ - /* fd_ghost_node_t implements a left-child, right-sibling n-ary tree. - Each node maintains the index of its left-most child, its - immediate-right sibling, and its parent. The indices are directly - indexable into the node_pool */ + Each node maintains the `node_pool` index of its left-most child + (`child_idx`), its immediate-right sibling (`sibling_idx`), and its + parent (`parent_idx`). + + This tree structure is gaddr-safe and supports accesses and + operations from processes with separate local ghost joins. */ -typedef struct fd_ghost_node fd_ghost_node_t; struct __attribute__((aligned(128UL))) fd_ghost_node { ulong slot; /* slot this node is tracking, also the map key */ - ulong next; /* reserved for internal use by fd_pool and fd_map_chain */ - ulong weight; /* amount of stake (in lamports) that has voted for this slot or any of its descendants */ - ulong stake; /* amount of stake (in lamports) that has voted for this slot */ - ulong gossip_stake; /* amount of stake (in lamports) that has voted for this slot via gossip (sans replay overlap) */ - ulong rooted_stake; /* amount of stake (in lamports) that has rooted this slot */ - int eqvoc; /* flag there are equivocating blocks for this slot */ + ulong next; /* reserved for internal use by fd_pool, fd_map_chain and fd_ghost_publish */ + ulong weight; /* amount of stake that has voted (via replay) for this slot or any of its descendants */ + ulong replay_stake; /* amount of stake that has voted (via replay) for this slot */ + ulong gossip_stake; /* amount of stake that has voted (via gossip) for this slot */ + ulong rooted_stake; /* amount of stake that has rooted this slot */ + int valid; /* whether this node is valid for fork choice (fd_ghost_head) */ ulong parent_idx; /* index of the parent in the node pool */ - ulong child_idx; /* index of the left-most child in the node pool */ - ulong sibling_idx; /* index of the next sibling in the node pool */ + ulong child_idx; /* index of the left-child in the node pool */ + ulong sibling_idx; /* index of the right-sibling in the node pool */ }; - -#define FD_GHOST_EQV_SAFE ( 0.52 ) -#define FD_GHOST_OPT_CONF ( 2.0 / 3.0 ) +typedef struct fd_ghost_node fd_ghost_node_t; #define POOL_NAME fd_ghost_node_pool #define POOL_T fd_ghost_node_t @@ -107,7 +119,7 @@ typedef struct fd_ghost_vote fd_ghost_vote_t; the memory region. ---------------------- <--- fd_ghost_t * - | root | total_stake | + | metadata | ---------------------- | node_pool | ---------------------- @@ -119,20 +131,30 @@ typedef struct fd_ghost_vote fd_ghost_vote_t; ---------------------- */ -struct __attribute__((aligned(128UL))) fd_ghost { +#define FD_GHOST_ALIGN (128UL) +#define FD_GHOST_MAGIC (0xf17eda2ce794057UL) /* firedancer ghost */ + +struct __attribute__((aligned(FD_GHOST_ALIGN))) fd_ghost { /* Metadata */ - ulong root_idx; - ulong total_stake; - ulong ghost_gaddr; + ulong magic; /* ==FD_GHOST_MAGIC */ + ulong ghost_gaddr; /* wksp gaddr of this in the backing wksp, non-zero gaddr */ + ulong seed; /* seed for various hashing function used under the hood, arbitrary */ + ulong root_idx; /* node_pool idx of the root */ + ulong total_stake; /* total amount of stake */ + + /* The ghost node_pool is a memory pool of tree nodes from which one + is allocated for each slot. The node map is a map_para to support + fast, parallel O(1) querying of ghost nodes by slot. */ - /* Inline data structures */ + ulong node_pool_gaddr; + ulong node_map_gaddr; - ulong node_pool_gaddr; - ulong node_map_gaddr; - ulong vote_pool_gaddr; - ulong vote_map_gaddr; + /* FIXME remove */ + + ulong vote_pool_gaddr; + ulong vote_map_gaddr; }; typedef struct fd_ghost fd_ghost_t; @@ -165,7 +187,6 @@ fd_ghost_footprint( ulong node_max, ulong vote_max ) { fd_ghost_vote_map_align(), fd_ghost_vote_map_footprint( vote_max ) ), fd_ghost_align() ); } -/* clang-format on */ /* fd_ghost_new formats an unused memory region for use as a ghost. mem is a non-NULL pointer to this region in the local address space @@ -213,6 +234,11 @@ fd_ghost_init( fd_ghost_t * ghost, ulong root, ulong total_stake ); /* Accessors */ +/* fd_ghost_wksp returns the local join to the wksp backing the ghost. + The lifetime of the returned pointer is at least as long as the + lifetime of the local join. Assumes ghost is a current local + join. */ + FD_FN_PURE static inline fd_wksp_t * fd_ghost_wksp( fd_ghost_t const * ghost ) { return (fd_wksp_t *)( ( (ulong)ghost ) - ghost->ghost_gaddr ); @@ -228,13 +254,6 @@ fd_ghost_node_map( fd_ghost_t const * ghost ) { return fd_wksp_laddr_fast( fd_ghost_wksp( ghost ), ghost->node_map_gaddr ); } -FD_FN_PURE static inline fd_ghost_node_t const * -fd_ghost_query( fd_ghost_t const * ghost, ulong slot ) { - fd_ghost_node_map_t * node_map = fd_ghost_node_map( ghost ); - fd_ghost_node_t * node_pool = fd_ghost_node_pool( ghost ); - return fd_ghost_node_map_ele_query_const( node_map, &slot, NULL, node_pool ); -} - FD_FN_PURE static inline fd_ghost_vote_t * fd_ghost_vote_pool( fd_ghost_t const * ghost ) { return fd_wksp_laddr_fast( fd_ghost_wksp( ghost ), ghost->vote_pool_gaddr ); @@ -245,19 +264,76 @@ fd_ghost_vote_map( fd_ghost_t const * ghost ) { return fd_wksp_laddr_fast( fd_ghost_wksp( ghost ), ghost->vote_map_gaddr ); } -/* Operations */ +/* fd_ghost_root returns a pointer to the ghost root. Assumes ghost is + a current local join. */ -bool -fd_ghost_verify( fd_ghost_t const * ghost ); +FD_FN_PURE static inline fd_ghost_node_t const * +fd_ghost_root( fd_ghost_t const * ghost ) { + return fd_ghost_node_pool_ele_const( fd_ghost_node_pool( ghost ), ghost->root_idx ); +} + +/* fd_ghost_parent returns a pointer to the `parent` of `child`. + Assumes ghost is a current local join and child is a valid pointer + to a node_pool element inside ghost. */ + +FD_FN_PURE static inline fd_ghost_node_t const * +fd_ghost_parent( fd_ghost_t const * ghost, fd_ghost_node_t const * child ) { + return fd_ghost_node_pool_ele_const( fd_ghost_node_pool( ghost ), child->parent_idx ); +} -/* fd_ghost_node_insert inserts a new node with slot as the key into the - ghost. Assumes slot >= ghost->smr, slot is not already in ghost, - parent_slot is already in ghost, and the node pool has a free element - (if handholding is enabled, explicitly checks and errors). Returns - the inserted ghost node. */ +/* fd_ghost_child returns a pointer to the left-most child of `parent`. + Assumes ghost is a current local join and parent is a valid pointer + to a node_pool element inside ghost. */ + +FD_FN_PURE static inline fd_ghost_node_t const * +fd_ghost_child( fd_ghost_t const * ghost, fd_ghost_node_t const * parent ) { + return fd_ghost_node_pool_ele_const( fd_ghost_node_pool( ghost ), parent->child_idx ); +} + +/* fd_ghost_head greedily traverses the ghost beginning from `node`, + returning the ending leaf of the traversal (see top-level + documentation for traversal details). Assumes ghost is a current + local join and has been initialized with fd_ghost_init and is + therefore non-empty. */ + +FD_FN_PURE fd_ghost_node_t const * +fd_ghost_head( fd_ghost_t const * ghost, fd_ghost_node_t const * node ); + +/* fd_ghost_query returns the node keyed by `slot` or NULL if not + found. */ + +FD_FN_PURE static inline fd_ghost_node_t const * +fd_ghost_query( fd_ghost_t const * ghost, ulong slot ) { + fd_ghost_node_map_t * node_map = fd_ghost_node_map( ghost ); + fd_ghost_node_t * node_pool = fd_ghost_node_pool( ghost ); + return fd_ghost_node_map_ele_query_const( node_map, &slot, NULL, node_pool ); +} + +/* fd_ghost_gca returns the greatest common ancestor of slot1, slot2 in + ghost. Assumes slot1 or slot2 are present in ghost (warns and + returns NULL with handholding enabled). This is guaranteed to be + non-NULL if slot1 and slot2 are both present. */ + +FD_FN_PURE fd_ghost_node_t const * +fd_ghost_gca( fd_ghost_t const * ghost, ulong slot1, ulong slot2 ); + +/* fd_ghost_is_ancestor returns 1 if `ancestor` is `slot`'s ancestor, 0 + otherwise. Also returns 0 if either `ancestor` or `slot` are not in + ghost. */ + +FD_FN_PURE int +fd_ghost_is_ancestor( fd_ghost_t const * ghost, ulong ancestor, ulong slot ); + +/* Operations */ + +/* fd_ghost_insert inserts a new node keyed by `slot` into the ghost. + Assumes slot >= ghost->smr, slot is not already in ghost, parent_slot + is already in ghost, and the node pool has a free element (if + handholding is enabled, explicitly checks and errors). Returns the + inserted ghost node. */ fd_ghost_node_t * -fd_ghost_insert( fd_ghost_t * ghost, ulong slot, ulong parent_slot ); +fd_ghost_insert( fd_ghost_t * ghost, ulong parent_slot, ulong slot ); /* fd_ghost_replay_vote votes for slot, adding pubkey's stake to the `stake` field for slot and to the `weight` field for both slot and @@ -309,63 +385,36 @@ fd_ghost_rooted_vote( fd_ghost_t * ghost, ulong slot, fd_pubkey_t const * pubkey fd_ghost_node_t const * fd_ghost_publish( fd_ghost_t * ghost, ulong slot ); -/* Traversals */ - -/* fd_ghost_gca returns the greatest common ancestor of slot1, slot2 in - ghost. Assumes slot1 or slot2 are present in ghost (warns and - returns NULL with handholding enabled). This is guaranteed to be - non-NULL if slot1 and slot2 are both present. */ - -FD_FN_PURE fd_ghost_node_t const * -fd_ghost_gca( fd_ghost_t const * ghost, ulong slot1, ulong slot2 ); - -/* fd_ghost_head returns ghost's head. Assumes caller has called -fd_ghost_init and that the ghost is non-empty, ie. has a root. */ - -FD_FN_PURE fd_ghost_node_t const * -fd_ghost_head( fd_ghost_t const * ghost ); +/* Misc */ -/* fd_ghost_is_descendant returns 1 if slot descends from ancestor_slot, - 0 otherwise. Assumes slot is present in ghost (warns and returns 0 - early if handholding is on). Does not assume the same of - ancestor_slot. */ +/* fd_ghost_verify checks the ghost is not obviously corrupt, as well as + that ghost invariants are being preserved ie. the weight of every + node is >= the sum of weights of its direct children. Returns 0 if + verify succeeds, -1 otherwise. */ FD_FN_PURE int -fd_ghost_is_descendant( fd_ghost_t const * ghost, ulong slot, ulong ancestor_slot ); +fd_ghost_verify( fd_ghost_t const * ghost ); -fd_ghost_node_t const * -fd_ghost_root_node( fd_ghost_t const * ghost ); +/* fd_ghost_print pretty-prints a formatted ghost tree. Printing begins + from `node` (it will appear as the root in the print output). -fd_ghost_node_t const * -fd_ghost_child_node( fd_ghost_t const * ghost, fd_ghost_node_t const * parent ); + The most straightforward and commonly used printing pattern is: + `fd_ghost_print( ghost, fd_ghost_root( ghost ) )` -/* Misc */ + This would print ghost beginning from the root. -/* fd_ghost_slot_print pretty-prints a formatted ghost tree. slot - controls which slot to begin printing from (will appear as the root - in the print output). depth allows caller to specify additional - ancestors to walk back from slot to set as the root. + Alternatively, caller can print a more localized view, for example + starting from the grandparent of the most recently executed slot: - ghost->root->slot and 0 are valid defaults for the above, - respectively. In that case, this would print ghost beginning from - the root. See fd_ghost_print. + ``` + fd_ghost_node_t const * node = fd_ghost_query( slot ); + fd_ghost_print( ghost, fd_ghost_parent( fd_ghost_parent( node ) ) ) + ``` - Typical usage is to pass in the most recently executed slot, in which - that slot in a leaf in ghost, and pick an appropriate depth for - visualization (20 is a reasonable default). */ + Callers should add null-checks as appropriate in actual usage. */ void -fd_ghost_slot_print( fd_ghost_t * ghost, ulong slot, ulong depth ); - -/* fd_ghost_print pretty-prints a formatted ghost tree starting from the - root using fd_ghost_slot_print. */ - -static inline void -fd_ghost_print( fd_ghost_t * ghost ) { - fd_ghost_node_t * node_pool = fd_wksp_laddr_fast( fd_ghost_wksp( ghost ), ghost->node_pool_gaddr ); - fd_ghost_node_t * root = fd_ghost_node_pool_ele( node_pool, ghost->root_idx ); - fd_ghost_slot_print( ghost, root->slot, 0 ); -} +fd_ghost_print( fd_ghost_t * ghost, fd_ghost_node_t const * node ); FD_PROTOTYPES_END diff --git a/src/choreo/ghost/test_ghost.c b/src/choreo/ghost/test_ghost.c index 6a92ea7f80..7be9fb6bb7 100644 --- a/src/choreo/ghost/test_ghost.c +++ b/src/choreo/ghost/test_ghost.c @@ -1,11 +1,9 @@ #include "fd_ghost.h" -#include -#include #define INSERT( c, p ) \ slots[i] = c; \ parent_slots[i] = p; \ - fd_ghost_insert( ghost, slots[i], parent_slots[i] ); \ + fd_ghost_insert( ghost, parent_slots[i], slots[i] ); \ i++; fd_ghost_node_t * @@ -53,18 +51,18 @@ test_ghost_simple( fd_wksp_t * wksp ) { INSERT( 5, 3 ); INSERT( 6, 5 ); - fd_ghost_print( ghost ); + fd_ghost_print( ghost, fd_ghost_root( ghost ) ); fd_pubkey_t pk1 = { .key = { 1 } }; ulong key2 = 2; fd_ghost_replay_vote( ghost, key2, &pk1, 1 ); - fd_ghost_print( ghost ); + fd_ghost_print( ghost, fd_ghost_root( ghost ) ); ulong key3 = 3; fd_ghost_replay_vote( ghost, key3, &pk1, 1 ); - fd_ghost_print( ghost ); + fd_ghost_print( ghost, fd_ghost_root( ghost ) ); fd_wksp_free_laddr( mem ); } @@ -111,31 +109,40 @@ test_ghost_publish_left( fd_wksp_t * wksp ) { INSERT( 5, 3 ); INSERT( 6, 5 ); - FD_TEST( fd_ghost_verify( ghost ) ); + FD_TEST( !fd_ghost_verify( ghost ) ); fd_pubkey_t pk1 = { .key = { 1 } }; ulong key2 = 2; fd_ghost_replay_vote( ghost, key2, &pk1, 1 ); - fd_ghost_print( ghost ); + fd_ghost_print( ghost, fd_ghost_root( ghost ) ); - FD_TEST( fd_ghost_verify( ghost ) ); + FD_TEST( !fd_ghost_verify( ghost ) ); ulong key3 = 3; fd_ghost_replay_vote( ghost, key3, &pk1, 1 ); fd_ghost_node_t const * node2 = fd_ghost_query( ghost, key2 ); FD_TEST( node2 ); - FD_TEST( fd_ghost_verify( ghost ) ); + FD_TEST( !fd_ghost_verify( ghost ) ); - fd_ghost_print( ghost ); + fd_ghost_print( ghost, fd_ghost_root( ghost ) ); fd_ghost_publish( ghost, key2 ); - fd_ghost_node_t * root = fd_ghost_node_pool_ele( node_pool, ghost->root_idx ); + fd_ghost_node_t const * root = fd_ghost_root( ghost ); FD_TEST( root->slot == 2 ); - FD_TEST( fd_ghost_verify( ghost ) ); + + fd_ghost_node_map_t * node_map = fd_ghost_node_map( ghost ); + for( fd_ghost_node_map_iter_t iter = fd_ghost_node_map_iter_init( node_map, node_pool ); + !fd_ghost_node_map_iter_done( iter, node_map, node_pool ); + iter = fd_ghost_node_map_iter_next( iter, node_map, node_pool ) ) { + fd_ghost_node_t const * node = fd_ghost_node_map_iter_ele( iter, node_map, node_pool ); + FD_LOG_NOTICE(("slot: %lu", node->slot)); + } + + FD_TEST( !fd_ghost_verify( ghost ) ); FD_TEST( fd_ghost_node_pool_ele( node_pool, root->child_idx )->slot == 4 ); FD_TEST( fd_ghost_node_pool_free( node_pool ) == node_max - 2 ); - fd_ghost_print( ghost ); + fd_ghost_print( ghost, fd_ghost_root( ghost ) ); fd_wksp_free_laddr( mem ); } @@ -183,29 +190,29 @@ test_ghost_publish_right( fd_wksp_t * wksp ) { INSERT( 4, 2 ); INSERT( 5, 3 ); INSERT( 6, 5 ); - FD_TEST( fd_ghost_verify( ghost ) ); + FD_TEST( !fd_ghost_verify( ghost ) ); fd_pubkey_t pk1 = { .key = { 1 } }; ulong key2 = 2; fd_ghost_replay_vote( ghost, key2, &pk1, 1 ); - FD_TEST( fd_ghost_verify( ghost ) ); + FD_TEST( !fd_ghost_verify( ghost ) ); ulong key3 = 3; fd_ghost_replay_vote( ghost, key3, &pk1, 1 ); - FD_TEST( fd_ghost_verify( ghost ) ); + FD_TEST( !fd_ghost_verify( ghost ) ); fd_ghost_node_t const * node3 = fd_ghost_query( ghost, key3 ); FD_TEST( node3 ); - fd_ghost_print( ghost ); + fd_ghost_print( ghost, fd_ghost_root( ghost ) ); fd_ghost_publish( ghost, key3 ); - FD_TEST( fd_ghost_verify( ghost ) ); + FD_TEST( !fd_ghost_verify( ghost ) ); fd_ghost_node_t * root = fd_ghost_node_pool_ele( node_pool, ghost->root_idx ); FD_TEST( root->slot == 3 ); FD_TEST( fd_ghost_node_pool_ele( node_pool, root->child_idx )->slot == 5 ); - FD_TEST( fd_ghost_child_node( ghost, fd_ghost_child_node( ghost, root ) )->slot == 6 ); + FD_TEST( fd_ghost_child( ghost, fd_ghost_child( ghost, root ) )->slot == 6 ); FD_TEST( fd_ghost_node_pool_free( node_pool ) == node_max - 3 ); - fd_ghost_print( ghost ); + fd_ghost_print( ghost, fd_ghost_root( ghost ) ); fd_wksp_free_laddr( mem ); } @@ -233,9 +240,9 @@ test_ghost_gca( fd_wksp_t * wksp ) { INSERT( 4, 2 ); INSERT( 5, 3 ); INSERT( 6, 5 ); - FD_TEST( fd_ghost_verify( ghost ) ); + FD_TEST( !fd_ghost_verify( ghost ) ); - fd_ghost_print( ghost ); + fd_ghost_print( ghost, fd_ghost_root( ghost ) ); FD_TEST( fd_ghost_gca( ghost, 0, 0 )->slot == 0 ); @@ -311,10 +318,10 @@ test_ghost_print( fd_wksp_t * wksp ) { query = 268538761; node = query_mut( ghost, query ); node->weight = 10; - FD_TEST( fd_ghost_verify( ghost ) ); + FD_TEST( !fd_ghost_verify( ghost ) ); - fd_ghost_slot_print( ghost, query, 8 ); - fd_ghost_print( ghost ); + fd_ghost_node_t const * grandparent = fd_ghost_parent( ghost, fd_ghost_parent( ghost, fd_ghost_query( ghost, 268538760 ) ) ); + fd_ghost_print( ghost, grandparent ); fd_wksp_free_laddr( mem ); } @@ -351,24 +358,24 @@ test_ghost_head( fd_wksp_t * wksp ){ fd_pubkey_t pk1 = { .key = { 1 } }; ulong key11 = 11; fd_ghost_replay_vote( ghost, key11, &pk1, 50 ); - FD_TEST( fd_ghost_verify( ghost ) ); + FD_TEST( !fd_ghost_verify( ghost ) ); fd_pubkey_t pk2 = { .key = { 2 } }; ulong key12 = 12; fd_ghost_replay_vote( ghost, key12, &pk2, 100); - FD_TEST( fd_ghost_verify( ghost ) ); + FD_TEST( !fd_ghost_verify( ghost ) ); - fd_ghost_node_t const * head = fd_ghost_head( ghost ); + fd_ghost_node_t const * head = fd_ghost_head( ghost, fd_ghost_root( ghost ) ); FD_TEST( head->slot == 12 ); ulong key13 = 13; fd_ghost_replay_vote( ghost, key13, &pk1, 75); // different stake than it was inserted with... - FD_TEST( fd_ghost_verify( ghost ) ); + FD_TEST( !fd_ghost_verify( ghost ) ); - fd_ghost_node_t const * head2 = fd_ghost_head( ghost ); + fd_ghost_node_t const * head2 = fd_ghost_head( ghost, fd_ghost_root( ghost ) ); FD_TEST( head2->slot == 12 ); - fd_ghost_print( ghost ); + fd_ghost_print( ghost, fd_ghost_root( ghost ) ); fd_wksp_free_laddr( mem ); } @@ -376,7 +383,7 @@ test_ghost_head( fd_wksp_t * wksp ){ void test_ghost_vote_leaves( fd_wksp_t * wksp ){ ulong node_max = 8; ulong vote_max = 8; - int depth = 3; + int d = 3; void * mem = fd_wksp_alloc_laddr( wksp, fd_ghost_align(), @@ -390,20 +397,20 @@ void test_ghost_vote_leaves( fd_wksp_t * wksp ){ /* make a full binary tree */ for( ulong i = 1; i < node_max - 1; i++){ - fd_ghost_insert( ghost, i, (i - 1) / 2 ); + fd_ghost_insert( ghost, (i-1)/2, i ); } /* one validator changes votes along leaves */ - ulong first_leaf = (ulong) (pow(2, (depth - 1)) - 1); + ulong first_leaf = fd_ulong_pow2(d-1) - 1; for( ulong i = first_leaf; i < node_max - 1; i++){ fd_ghost_replay_vote( ghost, i, &pk, 10); } - fd_ghost_print( ghost ); + fd_ghost_print( ghost, fd_ghost_root( ghost ) ); - ulong path[depth]; + ulong path[d]; ulong leaf = node_max - 2; - for( int i = depth - 1; i >= 0; i--){ + for( int i = d - 1; i >= 0; i--){ path[i] = leaf; leaf = (leaf - 1) / 2; } @@ -412,8 +419,8 @@ void test_ghost_vote_leaves( fd_wksp_t * wksp ){ int j = 0; for( ulong i = 0; i < node_max - 1; i++){ fd_ghost_node_t const * node = fd_ghost_query( ghost, i ); - if ( i == node_max - 2) FD_TEST( node->stake == 10 ); - else FD_TEST( node->stake == 0 ); + if ( i == node_max - 2) FD_TEST( node->replay_stake == 10 ); + else FD_TEST( node->replay_stake == 0 ); if ( i == path[j] ){ // if on fork FD_TEST( node->weight == 10 ); @@ -433,16 +440,16 @@ void test_ghost_vote_leaves( fd_wksp_t * wksp ){ for( ulong i = 0; i < node_max - 1; i++){ fd_ghost_node_t const * node = fd_ghost_query( ghost, i ); if ( i >= first_leaf){ - FD_TEST( node->stake == 10 ); + FD_TEST( node->replay_stake == 10 ); FD_TEST( node->weight == 10 ); } else { - FD_TEST( node->stake == 0 ); + FD_TEST( node->replay_stake == 0 ); FD_TEST( node->weight > 10); } } - FD_TEST( fd_ghost_verify( ghost ) ); - fd_ghost_print( ghost ); + FD_TEST( !fd_ghost_verify( ghost ) ); + fd_ghost_print( ghost, fd_ghost_root( ghost ) ); } void @@ -460,20 +467,20 @@ test_ghost_head_full_tree( fd_wksp_t * wksp ){ FD_LOG_NOTICE(( "ghost node max: %lu", fd_ghost_node_pool_max( fd_ghost_node_pool( ghost ) ) )); for ( ulong i = 1; i < node_max - 1; i++ ) { - fd_ghost_insert( ghost, i, (i - 1)/2); + fd_ghost_insert( ghost, (i-1)/2, i ); fd_pubkey_t pk = { .key = { ( uchar )i } }; fd_ghost_replay_vote( ghost, i, &pk, i); } for ( ulong i = 0; i < node_max - 1; i++ ) { fd_ghost_node_t const * node = fd_ghost_query( ghost, i ); - FD_TEST( node->stake == i ); + FD_TEST( node->replay_stake == i ); } - FD_TEST( fd_ghost_verify( ghost ) ); + FD_TEST( !fd_ghost_verify( ghost ) ); - fd_ghost_print( ghost ); - fd_ghost_node_t const * head = fd_ghost_head( ghost ); + fd_ghost_print( ghost, fd_ghost_root( ghost ) ); + fd_ghost_node_t const * head = fd_ghost_head( ghost, fd_ghost_root( ghost ) ); FD_LOG_NOTICE(( "head slot %lu", head->slot )); @@ -483,12 +490,12 @@ test_ghost_head_full_tree( fd_wksp_t * wksp ){ // add one more node - fd_ghost_insert( ghost, node_max - 1, (node_max - 2)/2 ); + fd_ghost_insert( ghost, (node_max-2)/2, node_max - 1 ); fd_pubkey_t pk = { .key = { (uchar)(node_max - 1) } }; fd_ghost_replay_vote( ghost, node_max - 1, &pk, node_max - 1); - FD_TEST( fd_ghost_verify( ghost ) ); - head = fd_ghost_head( ghost ); + FD_TEST( !fd_ghost_verify( ghost ) ); + head = fd_ghost_head( ghost, fd_ghost_root( ghost ) ); FD_TEST( head->slot == 14 ); // adding one more node would fail. @@ -507,7 +514,7 @@ test_rooted_vote( fd_wksp_t * wksp ){ fd_ghost_init( ghost, 0, 120 ); - fd_ghost_insert( ghost, 1, 0); + fd_ghost_insert( ghost, 0, 1); fd_pubkey_t pk = { .key = { 1 } }; fd_ghost_replay_vote( ghost, 1, &pk, 20); @@ -515,11 +522,11 @@ test_rooted_vote( fd_wksp_t * wksp ){ fd_ghost_rooted_vote( ghost, 1, &pk2, 10 ); fd_ghost_node_t const * node = fd_ghost_query( ghost, 1 ); - FD_TEST( node->stake == 20 ); + FD_TEST( node->replay_stake == 20 ); FD_TEST( node->weight == 20 ); FD_TEST( node->rooted_stake == 10 ); - fd_ghost_verify( ghost ); + FD_TEST( !fd_ghost_verify( ghost ) ); } int diff --git a/src/choreo/tower/fd_tower.c b/src/choreo/tower/fd_tower.c index f80b01e99c..d41adb5bb7 100644 --- a/src/choreo/tower/fd_tower.c +++ b/src/choreo/tower/fd_tower.c @@ -1,5 +1,5 @@ #include "fd_tower.h" -#include "../../flamenco/runtime/program/fd_vote_program.h" +#include "../voter/fd_voter.h" #define THRESHOLD_DEPTH ( 8 ) #define THRESHOLD_PCT ( 2.0 / 3.0 ) @@ -66,41 +66,8 @@ print( fd_tower_vote_t * tower_votes, ulong root ) { printf( "\n" ); } -static inline ulong -simulate_vote( fd_tower_vote_t const * votes, ulong slot ) { - ulong cnt = fd_tower_votes_cnt( votes ); - while( cnt ) { - - /* Return early if we can't pop the top tower vote, even if votes - below it are expired. */ - - if( FD_LIKELY( lockout_expiration_slot( fd_tower_votes_peek_index_const( votes, cnt - 1 ) ) >= - slot ) ) { - break; - } - cnt--; - } - return cnt; -} - -static void -tower_votes_from_landed_votes( fd_tower_vote_t * tower_votes, - fd_landed_vote_t const * landed_votes ) { - for( deq_fd_landed_vote_t_iter_t iter = deq_fd_landed_vote_t_iter_init( landed_votes ); - !deq_fd_landed_vote_t_iter_done( landed_votes, iter ); - iter = deq_fd_landed_vote_t_iter_next( landed_votes, iter ) ) { - fd_landed_vote_t const * landed_vote = deq_fd_landed_vote_t_iter_ele_const( landed_votes, - iter ); - fd_tower_votes_push_tail( tower_votes, - ( fd_tower_vote_t ){ - .slot = landed_vote->lockout.slot, - .conf = landed_vote->lockout.confirmation_count } ); - } -} - -/* Public API */ +/* Constructors */ -/* clang-format off */ void * fd_tower_new( void * shmem ) { if( FD_UNLIKELY( !shmem ) ) { @@ -134,9 +101,7 @@ fd_tower_new( void * shmem ) { return shmem; } -/* clang-format on */ -/* clang-format off */ fd_tower_t * fd_tower_join( void * shtower ) { @@ -164,7 +129,6 @@ fd_tower_join( void * shtower ) { return (fd_tower_t *)shtower; } -/* clang-format on */ void * fd_tower_leave( fd_tower_t const * tower ) { @@ -194,12 +158,11 @@ fd_tower_delete( void * tower ) { } void -fd_tower_init( fd_tower_t * tower, - fd_pubkey_t const * vote_acc_addr, - fd_acc_mgr_t * acc_mgr, - fd_exec_epoch_ctx_t const * epoch_ctx, - fd_fork_t const * fork, - ulong * smr ) { +fd_tower_init( fd_tower_t * tower, + fd_pubkey_t const * vote_acc_addr, + fd_funk_t * funk, + fd_funk_txn_t const * txn, + ulong * smr ) { if( FD_UNLIKELY( !tower ) ) { FD_LOG_WARNING(( "NULL tower" )); @@ -208,71 +171,78 @@ fd_tower_init( fd_tower_t * tower, /* Restore our tower using the vote account state. */ - FD_SCRATCH_SCOPE_BEGIN { - fd_vote_state_versioned_t vote_state_versioned = { 0 }; - - fd_vote_state_t * vote_state = fd_tower_vote_state_query( tower, - vote_acc_addr, - acc_mgr, - fork, - fd_scratch_virtual(), - &vote_state_versioned ); - if( FD_LIKELY( vote_state ) ) { - fd_tower_from_vote_state( tower, vote_state ); - FD_LOG_NOTICE(( "[%s] loading vote state for vote acc: %s", __func__, FD_BASE58_ENC_32_ALLOCA( vote_acc_addr ) )); - } else { - FD_LOG_WARNING(( "[%s] didn't find existing vote state for vote acc: %s", - __func__, - FD_BASE58_ENC_32_ALLOCA( vote_acc_addr ) )); - } + fd_funk_rec_key_t key = { 0 }; + fd_memcpy( key.c, vote_acc_addr, sizeof( fd_pubkey_t ) ); + key.c[FD_FUNK_REC_KEY_FOOTPRINT - 1] = FD_FUNK_KEY_TYPE_ACC; + fd_voter_state_t const * state = fd_voter_state( funk, txn, &key ); + if ( FD_UNLIKELY( !state ) ) { + FD_LOG_WARNING(( "[%s] didn't find existing vote state for vote acc: %s", + __func__, + FD_BASE58_ENC_32_ALLOCA( vote_acc_addr ) )); } - FD_SCRATCH_SCOPE_END; + fd_voter_state_tower( state, tower ); - /* Init vote_accs and total_stake. */ + /* Set the SMR pointer. */ - fd_tower_epoch_update( tower, epoch_ctx ); + tower->smr = smr; +} - /* Init the smr. */ +/* simulate_vote simulates a vote on the vote tower for slot, + returning the new height (cnt) for all the votes that would have been + popped. */ - tower->smr = smr; +static inline ulong +simulate_vote( fd_tower_vote_t const * votes, ulong slot ) { + ulong cnt = fd_tower_votes_cnt( votes ); + while( cnt ) { + + /* Return early if we can't pop the top tower vote, even if votes + below it are expired. */ + + if( FD_LIKELY( lockout_expiration_slot( fd_tower_votes_peek_index_const( votes, cnt - 1 ) ) >= slot ) ) { + break; + } + cnt--; + } + return cnt; } int -fd_tower_lockout_check( fd_tower_t const * tower, - fd_fork_t const * fork, - fd_ghost_t const * ghost ) { +fd_tower_lockout_check( fd_tower_t const * tower, fd_ghost_t const * ghost, ulong slot ) { /* Simulate a vote to pop off all the votes that have been expired at the top of the tower. */ - ulong cnt = simulate_vote( tower->votes, fork->slot ); + ulong cnt = simulate_vote( tower->votes, slot ); /* By definition, all votes in the tower must be for the same fork, so - check if the top vote of the tower after simulating is on the same - fork as the fork we want to vote for (ie. fork->slot is a - descendant of top vote slot). If the top vote slot is too old (ie. - older than ghost->root), we just assume it is on the same fork. */ - - fd_tower_vote_t const * top_vote = fd_tower_votes_peek_index_const( tower->votes, cnt - 1 ); - fd_ghost_node_t const * root = fd_ghost_root_node( ghost ); - - int lockout_check = top_vote->slot < root->slot || - fd_ghost_is_descendant( ghost, fork->slot, top_vote->slot ); + check if the previous vote (ie. the last vote in the tower) is on + the same fork as the fork we want to vote for. We do this using + ghost by checking if the previous vote slot is an ancestor of the + `slot`. If the previous vote slot is too old (ie. older than + ghost->root), then we don't have ancestry information anymore and + we just assume it is on the same fork. + + FIXME discuss if it is safe to assume that? */ + + fd_tower_vote_t const * prev_vote = fd_tower_votes_peek_index_const( tower->votes, cnt - 1 ); + fd_ghost_node_t const * root = fd_ghost_root( ghost ); + + int lockout_check = prev_vote->slot < root->slot || + fd_ghost_is_ancestor( ghost, prev_vote->slot, slot ); FD_LOG_NOTICE(( "[fd_tower_lockout_check] ok? %d. top: (slot: %lu, conf: %lu). switch: %lu.", lockout_check, - top_vote->slot, - top_vote->conf, - fork->slot )); + prev_vote->slot, + prev_vote->conf, + slot )); return lockout_check; } int -fd_tower_switch_check( fd_tower_t const * tower, - fd_fork_t const * fork, - fd_ghost_t const * ghost ) { +fd_tower_switch_check( fd_tower_t const * tower, fd_ghost_t const * ghost, ulong slot ) { fd_tower_vote_t const * latest_vote = fd_tower_votes_peek_tail_const( tower->votes ); - fd_ghost_node_t const * root = fd_ghost_root_node( ghost ); + fd_ghost_node_t const * root = fd_ghost_root( ghost ); if( FD_UNLIKELY( latest_vote->slot < root->slot ) ) { @@ -286,10 +256,9 @@ fd_tower_switch_check( fd_tower_t const * tower, return 1; } - /* Assumption: fd_tower_switch_check is only called if - latest_vote->slot and fork->slot are on different forks (determined - by is_descendant), so they must not fall on the same ancestry path - back to the gca. + /* fd_tower_switch_check is only called if latest_vote->slot and + fork->slot are on different forks (determined by is_descendant), so + they must not fall on the same ancestry path back to the gca. INVALID: @@ -308,8 +277,13 @@ fd_tower_switch_check( fd_tower_t const * tower, a b */ + + #if FD_TOWER_USE_HANDHOLDING + FD_TEST( !fd_ghost_is_ancestor( ghost, latest_vote->slot, slot ) ); + #endif + fd_ghost_node_t * node_pool = fd_ghost_node_pool( ghost ); - fd_ghost_node_t const * gca = fd_ghost_gca( ghost, latest_vote->slot, fork->slot ); + fd_ghost_node_t const * gca = fd_ghost_gca( ghost, latest_vote->slot, slot ); ulong gca_idx = fd_ghost_node_map_idx_query( fd_ghost_node_map( ghost ), &gca->slot, ULONG_MAX, fd_ghost_node_pool( ghost ) ); /* gca_child is our latest_vote slot's ancestor that is also a direct @@ -322,7 +296,7 @@ fd_tower_switch_check( fd_tower_t const * tower, } ulong switch_stake = 0; - fd_ghost_node_t const * child = fd_ghost_child_node( ghost, gca ); + fd_ghost_node_t const * child = fd_ghost_child( ghost, gca ); while ( FD_LIKELY( child ) ) { if ( FD_LIKELY ( child != gca_child ) ) { switch_stake += child->weight; @@ -334,20 +308,21 @@ fd_tower_switch_check( fd_tower_t const * tower, FD_LOG_NOTICE(( "[fd_tower_switch_check] ok? %d. top: %lu. switch: %lu. switch stake: %.0lf%%.", switch_pct > SWITCH_PCT, fd_tower_votes_peek_tail_const( tower->votes )->slot, - fork->slot, + slot, switch_pct * 100.0 )); return switch_pct > SWITCH_PCT; } int -fd_tower_threshold_check( fd_tower_t const * tower, - fd_fork_t const * fork, - fd_acc_mgr_t * acc_mgr ) { +fd_tower_threshold_check( fd_tower_t const * tower, + fd_funk_t * funk, + fd_funk_txn_t const * txn, + ulong slot ) { /* First, simulate a vote, popping off everything that would be expired by voting for the current slot. */ - ulong cnt = simulate_vote( tower->votes, fork->slot ); + ulong cnt = simulate_vote( tower->votes, slot ); /* Return early if our tower is not at least THRESHOLD_DEPTH deep after simulating. */ @@ -372,46 +347,38 @@ fd_tower_threshold_check( fd_tower_t const * tower, iter = fd_tower_vote_accs_iter_next( tower->vote_accs, iter ) ) { fd_tower_vote_acc_t * vote_acc = fd_tower_vote_accs_iter_ele( tower->vote_accs, iter ); + fd_funk_rec_key_t key = { 0 }; + fd_memcpy( key.c, vote_acc->addr, sizeof( fd_pubkey_t ) ); + key.c[FD_FUNK_REC_KEY_FOOTPRINT - 1] = FD_FUNK_KEY_TYPE_ACC; + fd_voter_state_t const * state = fd_voter_state( funk, txn, &key ); + if( FD_UNLIKELY( !state ) ) { + FD_LOG_WARNING(( "[%s] failed to load vote acc addr %s. skipping.", + __func__, + FD_BASE58_ENC_32_ALLOCA( vote_acc->addr ) )); + continue; + } - FD_SCRATCH_SCOPE_BEGIN { - - /* FIXME */ - fd_vote_state_versioned_t vote_state_versioned = { 0 }; - - fd_vote_state_t * vote_state = fd_tower_vote_state_query( tower, - vote_acc->addr, - acc_mgr, - fork, - fd_scratch_virtual(), - &vote_state_versioned ); - if( FD_UNLIKELY( !vote_state ) ) { - FD_LOG_WARNING(( "[%s] failed to load vote acc addr %s. skipping.", - __func__, - FD_BASE58_ENC_32_ALLOCA( vote_acc->addr ) )); - continue; - } - - fd_landed_vote_t * landed_votes = vote_state->votes; - - /* If the vote account has an empty tower, continue. */ + ulong vote = fd_voter_state_vote( state ); - if( FD_UNLIKELY( deq_fd_landed_vote_t_empty( landed_votes ) ) ) continue; + /* If this voter has not voted, continue. */ - /* Convert the landed_votes into tower's vote_slots interface. */ + if( FD_UNLIKELY( vote == FD_SLOT_NULL ) ) continue; - void * mem = fd_scratch_alloc( fd_tower_votes_align(), fd_tower_votes_footprint() ); - fd_tower_vote_t * their_tower_votes = fd_tower_votes_join( fd_tower_votes_new( mem ) ); - tower_votes_from_landed_votes( their_tower_votes, landed_votes ); + /* Convert the landed_votes into tower's vote_slots interface. */ - ulong cnt = simulate_vote( their_tower_votes, fork->slot ); + FD_SCRATCH_SCOPE_BEGIN { + void * mem = fd_scratch_alloc( fd_tower_align(), fd_tower_footprint() ); + fd_tower_t * their_tower = fd_tower_join( fd_tower_new( mem ) ); + fd_voter_state_tower( state, their_tower ); + ulong cnt = simulate_vote( their_tower->votes, slot ); /* Continue if their tower is empty after simulating. */ if( FD_UNLIKELY( !cnt ) ) continue; - /* Get their latest vote slot.*/ + /* Get their latest vote slot. */ - fd_tower_vote_t const * vote_slot = fd_tower_votes_peek_index( their_tower_votes, cnt - 1 ); + fd_tower_vote_t const * vote_slot = fd_tower_votes_peek_index( their_tower->votes, cnt - 1 ); /* Count their stake towards the threshold check if their latest vote slot >= our threshold slot. @@ -427,8 +394,7 @@ fd_tower_threshold_check( fd_tower_t const * tower, if( FD_LIKELY( vote_slot->slot >= threshold_slot ) ) { threshold_stake += vote_acc->stake; } - } - FD_SCRATCH_SCOPE_END; + } FD_SCRATCH_SCOPE_END; } double threshold_pct = (double)threshold_stake / (double)tower->total_stake; @@ -444,7 +410,7 @@ fd_fork_t const * fd_tower_best_fork( FD_PARAM_UNUSED fd_tower_t const * tower, fd_forks_t const * forks, fd_ghost_t const * ghost ) { - fd_ghost_node_t const * head = fd_ghost_head( ghost ); + fd_ghost_node_t const * head = fd_ghost_head( ghost, fd_ghost_root( ghost ) ); /* Search for the fork head in the frontier. */ @@ -454,7 +420,7 @@ fd_tower_best_fork( FD_PARAM_UNUSED fd_tower_t const * tower, if( FD_UNLIKELY( !best ) ) { /* If the best fork is not in the frontier, then we must have pruned - it or improperly re-used its fork and we're now in a bad state. */ + it or corrupted memory and we're now in a bad state. */ /* TODO eqvoc */ @@ -477,12 +443,12 @@ fd_tower_reset_fork( fd_tower_t const * tower, return fd_tower_best_fork( tower, forks, ghost ); } - fd_tower_vote_t const * latest_vote = fd_tower_votes_peek_tail_const( tower->votes ); + fd_tower_vote_t const * vote = fd_tower_votes_peek_tail_const( tower->votes ); /* In general our reset fork is our last vote fork, but there are 2 cases in which that doesn't apply: - 1. If our latest vote slot is older than SMR, we know we don't have + 1. If our last vote slot is older than SMR, we know we don't have ancestry information about our latest vote slot anymore, so we build off the best fork. @@ -492,43 +458,32 @@ fd_tower_reset_fork( fd_tower_t const * tower, different fork. So build off the best fork instead. See the top-level documentation in fd_tower.h for more context. */ - fd_ghost_node_t const * root = fd_ghost_root_node( ghost ); - if( FD_UNLIKELY( latest_vote->slot < root->slot || - !fd_ghost_is_descendant( ghost, latest_vote->slot, root->slot ) ) ) { + + fd_ghost_node_t const * root = fd_ghost_root( ghost ); + if( FD_UNLIKELY( vote->slot < root->slot || + !fd_ghost_is_ancestor( ghost, root->slot, vote->slot ) ) ) { return fd_tower_best_fork( tower, forks, ghost ); } - fd_fork_frontier_t const * frontier = forks->frontier; - fd_fork_t const * pool = forks->pool; + /* Find the ghost node keyed by our last vote slot. This must always + be in the ghost, because the ghost root should always be both on + the same fork and less than or equal to our tower root. */ - for( fd_fork_frontier_iter_t iter = fd_fork_frontier_iter_init( frontier, pool ); - !fd_fork_frontier_iter_done( iter, frontier, pool ); - iter = fd_fork_frontier_iter_next( iter, frontier, pool ) ) { - fd_fork_t const * fork = fd_fork_frontier_iter_ele_const( iter, frontier, pool ); - ulong slot = fd_ulong_if( fork->lock, fork->slot_ctx.slot_bank.prev_slot, fork->slot ); - if( FD_LIKELY( fd_ghost_is_descendant( ghost, slot, latest_vote->slot ) ) ) return fork; + fd_ghost_node_t const * vote_node = fd_ghost_query( ghost, vote->slot ); + if( FD_UNLIKELY( !vote_node ) ) { + FD_LOG_WARNING(( "[%s] invariant violation: missing vote slot %lu in ghost", __func__, vote->slot )); + return NULL; } - /* If we've reached here, we're in a bad state. Log some diagnostics. */ - - for( fd_fork_frontier_iter_t iter = fd_fork_frontier_iter_init( frontier, pool ); - !fd_fork_frontier_iter_done( iter, frontier, pool ); - iter = fd_fork_frontier_iter_next( iter, frontier, pool ) ) { - fd_fork_t const * fork = fd_fork_frontier_iter_ele_const( iter, frontier, pool ); - ulong slot = fd_ulong_if( fork->lock, fork->slot_ctx.slot_bank.prev_slot, fork->slot ); - - FD_LOG_WARNING(( "\n\n[%s] unable to find altest vote slot in frontier!\nfork lock? %d\nfork slot %lu\nfork prev slot %lu\nlatest vote slot %lu\n" - "descendant slot %lu\ndescends? %d", - __func__, - fork->lock, - fork->slot, - fork->slot_ctx.slot_bank.prev_slot, - latest_vote->slot, - slot, - fd_ghost_is_descendant( ghost, slot, latest_vote->slot ) )); - } + /* Given the last vote as a starting pointing, find the best (head) + slot in ghost. */ - FD_LOG_ERR(( "invariant violation: could not find our latest vote slot in frontier even though there is a valid ancestry in ghost." )); + fd_ghost_node_t const * reset_head = fd_ghost_head( ghost, vote_node ); + fd_fork_t const * reset_fork = fd_forks_query_const( forks, reset_head->slot ); + #if FD_TOWER_USE_HANDHOLDING + if ( FD_UNLIKELY( !reset_fork ) ) FD_LOG_WARNING(( "[%s] invariant violation: missing reset head %lu in forks.", __func__, reset_head->slot )); + #endif + return reset_fork; } fd_fork_t const * @@ -562,19 +517,20 @@ fd_tower_vote_fork( fd_tower_t * tower, 2. If our latest vote slot is older than SMR, we know we don't have ancestry information to determine whether we're locked out or can switch, so we similarly build off the best fork. */ - fd_ghost_node_t const * root = fd_ghost_root_node( ghost ); - if( FD_UNLIKELY( !fd_ghost_is_descendant( ghost, latest_vote->slot, root->slot ) ) ) { + + fd_ghost_node_t const * root = fd_ghost_root( ghost ); + if( FD_UNLIKELY( !fd_ghost_is_ancestor( ghost, root->slot, latest_vote->slot ) ) ) { return fd_tower_best_fork( tower, forks, ghost ); } /* Optimize for when there is just one fork (most of the time), which means best fork. */ - if( FD_LIKELY( fd_ghost_is_descendant( ghost, best->slot, latest_vote->slot ) ) ) { + if( FD_LIKELY( fd_ghost_is_ancestor( ghost, latest_vote->slot, best->slot ) ) ) { /* The best fork is on the same fork and we can vote for best_fork->slot if we pass the threshold check. */ - if( FD_LIKELY( fd_tower_threshold_check( tower, best, acc_mgr ) ) ) { + if( FD_LIKELY( fd_tower_threshold_check( tower, acc_mgr->funk, best->slot_ctx.funk_txn, best->slot ) ) ) { FD_LOG_NOTICE(( "[fd_tower_vote_fork_select] success (threshold). best: %lu. vote: " "(slot: %lu conf: %lu)", best->slot, @@ -594,8 +550,8 @@ fd_tower_vote_fork( fd_tower_t * tower, /* The best fork is on a different fork, so try to switch if we pass lockout and switch threshold. */ - if( FD_UNLIKELY( fd_tower_lockout_check( tower, best, ghost ) && - fd_tower_switch_check( tower, best, ghost ) ) ) { + if( FD_UNLIKELY( fd_tower_lockout_check( tower, ghost, best->slot ) && + fd_tower_switch_check( tower, ghost, best->slot ) ) ) { FD_LOG_NOTICE(( "[fd_tower_vote_fork_select] success (lockout switch). best: %lu. vote: " "(slot: %lu conf: %lu)", best->slot, @@ -682,164 +638,119 @@ fd_tower_epoch_update( fd_tower_t * tower, fd_exec_epoch_ctx_t const * epoch_ctx } void -fd_tower_fork_update( fd_tower_t const * tower, - fd_fork_t const * fork, - fd_acc_mgr_t * acc_mgr, - fd_blockstore_t * blockstore, - fd_ghost_t * ghost ) { - - /* Get the parent key. Every slot except the root must have a parent. */ - - fd_blockstore_start_read( blockstore ); - ulong parent_slot = fd_blockstore_parent_slot_query( blockstore, fork->slot ); -#if FD_TOWER_USE_HANDHOLDING - /* we must have a parent slot and bank hash, given we just executed - its child. if not, likely a bug in blockstore pruning. */ - if( FD_UNLIKELY( parent_slot == FD_SLOT_NULL ) ) { - FD_LOG_ERR(( "missing parent slot for curr slot %lu", fork->slot )); - }; -#endif - fd_blockstore_end_read( blockstore ); - - /* Insert the new fork head into ghost. */ - - fd_ghost_node_t * ghost_node = fd_ghost_insert( ghost, fork->slot, parent_slot ); - fd_ghost_node_t const * root = fd_ghost_root_node( ghost ); - -#if FD_TOWER_USE_HANDHOLDING - if( FD_UNLIKELY( !ghost_node ) ) { - FD_LOG_ERR(( "failed to insert ghost node %lu", fork->slot )); - } -#endif +fd_tower_fork_update( fd_tower_t const * tower, + fd_blockstore_t * blockstore, + fd_ghost_t * ghost, + fd_funk_t * funk, + fd_funk_txn_t const * txn ) { + for( fd_tower_vote_accs_iter_t iter = fd_tower_vote_accs_iter_init_rev( tower->vote_accs ); !fd_tower_vote_accs_iter_done_rev( tower->vote_accs, iter ); iter = fd_tower_vote_accs_iter_prev( tower->vote_accs, iter ) ) { fd_tower_vote_acc_t * vote_acc = fd_tower_vote_accs_iter_ele( tower->vote_accs, iter ); - FD_SCRATCH_SCOPE_BEGIN { + /* TODO we can optimize this funk query to only check through the + last slot on this fork this function was called on. currently + rec_query_global traverses all the way back to the root. */ - /* FIXME */ - fd_vote_state_versioned_t vote_state_versioned = { 0 }; - - fd_vote_state_t * vote_state = fd_tower_vote_state_query( tower, - vote_acc->addr, - acc_mgr, - fork, - fd_scratch_virtual(), - &vote_state_versioned ); - if( FD_UNLIKELY( !vote_state ) ) { - FD_LOG_WARNING(( "[%s] failed to load vote acc addr %s. skipping.", - __func__, - FD_BASE58_ENC_32_ALLOCA( vote_acc->addr ) )); - continue; - } + fd_funk_rec_key_t key = { 0 }; + fd_memcpy( key.c, vote_acc->addr, sizeof( fd_pubkey_t ) ); + key.c[FD_FUNK_REC_KEY_FOOTPRINT - 1] = FD_FUNK_KEY_TYPE_ACC; - fd_landed_vote_t * landed_votes = vote_state->votes; + fd_voter_state_t const * state = fd_voter_state( funk, txn, &key ); - /* If the vote account has an empty tower, continue. */ + ulong vote = fd_voter_state_vote( state ); - if( FD_UNLIKELY( deq_fd_landed_vote_t_empty( landed_votes ) ) ) continue; + /* Only process votes for slots >= root. Ghost requires vote slot + to already exist in the ghost tree. */ - /* Get the vote account's latest vote. */ + if( FD_UNLIKELY( vote != FD_SLOT_NULL && vote >= fd_ghost_root( ghost )->slot ) ) { + FD_LOG_NOTICE(("voting for %lu", vote)); + fd_ghost_node_t const * node = fd_ghost_replay_vote( ghost, vote, vote_acc->addr, vote_acc->stake ); - ulong vote_slot = deq_fd_landed_vote_t_peek_tail_const( landed_votes )->lockout.slot; + /* Check if it has crossed the equivocation safety and optimistic confirmation thresholds. */ - if( FD_UNLIKELY( vote_slot < root->slot ) ) continue; + fd_blockstore_start_write( blockstore ); + fd_block_map_t * block_map_entry = fd_blockstore_block_map_query( blockstore, vote ); - /* Only process votes for slots >= root. Ghost requires vote slot - to already exist in the ghost tree. */ + int eqvocsafe = fd_uchar_extract_bit( block_map_entry->flags, FD_BLOCK_FLAG_EQVOCSAFE ); + if( FD_UNLIKELY( !eqvocsafe ) ) { + double pct = (double)node->replay_stake / (double)ghost->total_stake; + if( FD_UNLIKELY( pct > FD_EQVOCSAFE_PCT ) ) { + FD_LOG_NOTICE( ( "eqvocsafe %lu", block_map_entry->slot ) ); + block_map_entry->flags = fd_uchar_set_bit( block_map_entry->flags, FD_BLOCK_FLAG_EQVOCSAFE ); + blockstore->hcs = fd_ulong_max( blockstore->hcs, block_map_entry->slot ); + } + } - if( FD_UNLIKELY( vote_slot >= root->slot ) ) { - fd_ghost_node_t const * node = fd_ghost_replay_vote( ghost, vote_slot, &vote_state->node_pubkey, vote_acc->stake ); + int confirmed = fd_uchar_extract_bit( block_map_entry->flags, FD_BLOCK_FLAG_CONFIRMED ); + if( FD_UNLIKELY( !confirmed ) ) { + double pct = (double)node->replay_stake / (double)ghost->total_stake; + if( FD_UNLIKELY( pct > FD_CONFIRMED_PCT ) ) { + FD_LOG_NOTICE( ( "confirming %lu", block_map_entry->slot ) ); + block_map_entry->flags = fd_uchar_set_bit( block_map_entry->flags, FD_BLOCK_FLAG_CONFIRMED ); + blockstore->hcs = fd_ulong_max( blockstore->hcs, block_map_entry->slot ); + } + } - /* Check if it has crossed the equivocation safety and optimistic confirmation thresholds. */ + fd_blockstore_end_write( blockstore ); + } - fd_blockstore_start_write( blockstore ); - fd_block_map_t * block_map_entry = fd_blockstore_block_map_query( blockstore, vote_slot ); + ulong root = fd_voter_state_root( state ); - int eqvocsafe = fd_uchar_extract_bit( block_map_entry->flags, FD_BLOCK_FLAG_EQVOCSAFE ); - if( FD_UNLIKELY( !eqvocsafe ) ) { - double pct = (double)node->stake / (double)ghost->total_stake; - if( FD_UNLIKELY( pct > FD_EQVOCSAFE_PCT ) ) { - FD_LOG_NOTICE( ( "equivocation safe %lu", block_map_entry->slot ) ); - block_map_entry->flags = fd_uchar_set_bit( block_map_entry->flags, FD_BLOCK_FLAG_EQVOCSAFE ); - blockstore->hcs = fd_ulong_max( blockstore->hcs, block_map_entry->slot ); - } - } + /* Check if this voter's root >= ghost root. We can't process + other voters' roots that precede the ghost root. */ - int confirmed = fd_uchar_extract_bit( block_map_entry->flags, FD_BLOCK_FLAG_CONFIRMED ); - if( FD_UNLIKELY( !confirmed ) ) { - double pct = (double)node->stake / (double)ghost->total_stake; - if( FD_UNLIKELY( pct > FD_CONFIRMED_PCT ) ) { - FD_LOG_NOTICE( ( "confirming %lu", block_map_entry->slot ) ); - block_map_entry->flags = fd_uchar_set_bit( block_map_entry->flags, FD_BLOCK_FLAG_CONFIRMED ); - blockstore->hcs = fd_ulong_max( blockstore->hcs, block_map_entry->slot ); - } - } + if( FD_UNLIKELY( root >= fd_ghost_root( ghost )->slot ) ) { + fd_ghost_node_t const * node = fd_ghost_rooted_vote( ghost, root, vote_acc->addr, vote_acc->stake ); - fd_blockstore_end_write( blockstore ); - } + /* Check if it has crossed finalized threshold. */ - /* Check if this voter's root >= ghost root. We can't process - other voters' roots that precede the ghost root. */ - - if( FD_UNLIKELY( vote_state->root_slot >= root->slot ) ) { - fd_ghost_node_t const * node = fd_ghost_rooted_vote( ghost, vote_state->root_slot, &vote_state->node_pubkey, vote_acc->stake ); - - /* Check if it has crossed finalized threshold. */ - - fd_blockstore_start_write( blockstore ); - fd_block_map_t * block_map_entry = fd_blockstore_block_map_query( blockstore, vote_state->root_slot ); - int finalized = fd_uchar_extract_bit( block_map_entry->flags, FD_BLOCK_FLAG_FINALIZED ); - if( FD_UNLIKELY( !finalized ) ) { - double pct = (double)node->rooted_stake / (double)ghost->total_stake; - if( FD_UNLIKELY( pct > FD_FINALIZED_PCT ) ) { - ulong smr = block_map_entry->slot; - FD_LOG_NOTICE(( "finalizing %lu", block_map_entry->slot )); - fd_block_map_t * ancestor = block_map_entry; - while( ancestor ) { - ancestor->flags = fd_uchar_set_bit( ancestor->flags, FD_BLOCK_FLAG_FINALIZED ); - ancestor = fd_blockstore_block_map_query( blockstore, ancestor->parent_slot ); - } + fd_blockstore_start_write( blockstore ); + fd_block_map_t * block_map_entry = fd_blockstore_block_map_query( blockstore, root ); + int finalized = fd_uchar_extract_bit( block_map_entry->flags, FD_BLOCK_FLAG_FINALIZED ); + if( FD_UNLIKELY( !finalized ) ) { + double pct = (double)node->rooted_stake / (double)ghost->total_stake; + if( FD_UNLIKELY( pct > FD_FINALIZED_PCT ) ) { + ulong smr = block_map_entry->slot; + FD_LOG_NOTICE(( "finalizing %lu", block_map_entry->slot )); + fd_block_map_t * ancestor = block_map_entry; + while( ancestor ) { + ancestor->flags = fd_uchar_set_bit( ancestor->flags, FD_BLOCK_FLAG_FINALIZED ); + ancestor = fd_blockstore_block_map_query( blockstore, ancestor->parent_slot ); + } #if FD_TOWER_USE_HANDHOLDING - if( FD_UNLIKELY( smr <= fd_fseq_query( tower->smr ) ) ) { - FD_LOG_ERR(( "invariant violation. newly observed SMR %lu <= existing fseq SMR %lu.", - smr, - fd_fseq_query( tower->smr ) )); - } -#endif - fd_fseq_update( tower->smr, smr ); + if( FD_UNLIKELY( smr <= fd_fseq_query( tower->smr ) ) ) { + FD_LOG_ERR(( "invariant violation. newly observed SMR %lu <= existing fseq SMR %lu.", + smr, + fd_fseq_query( tower->smr ) )); } +#endif + fd_fseq_update( tower->smr, smr ); } - fd_blockstore_end_write( blockstore ); } + fd_blockstore_end_write( blockstore ); } - FD_SCRATCH_SCOPE_END; } } -ulong -fd_tower_simulate_vote( fd_tower_t const * tower, ulong slot ) { - return simulate_vote( tower->votes, slot ); -} - void fd_tower_vote( fd_tower_t const * tower, ulong slot ) { - FD_LOG_NOTICE(( "[fd_tower_vote] voting for slot %lu", slot )); + FD_LOG_DEBUG(( "[fd_tower_vote] voting for slot %lu", slot )); + +#if FD_TOWER_USE_HANDHOLDING /* Check we're not voting for the exact same slot as our latest tower vote. This can happen when there are forks. */ fd_tower_vote_t * latest_vote = fd_tower_votes_peek_tail( tower->votes ); if( FD_UNLIKELY( latest_vote && latest_vote->slot == slot ) ) { - FD_LOG_NOTICE(( "[fd_tower_vote] already voted for slot %lu", slot )); + FD_LOG_WARNING(( "[fd_tower_vote] already voted for slot %lu", slot )); return; } -#if FD_TOWER_USE_HANDHOLDING - - /* Check we aren't voting for a slot earlier than the latest tower + /* Check we aren't voting for a slot earlier than the last tower vote. This should not happen and indicates a bug, because on the same vote fork the slot should be monotonically non-decreasing. */ @@ -849,20 +760,13 @@ fd_tower_vote( fd_tower_t const * tower, ulong slot ) { fd_tower_vote_t const * vote = fd_tower_votes_iter_ele_const( tower->votes, iter ); if( FD_UNLIKELY( slot == vote->slot ) ) { fd_tower_print( tower ); - FD_LOG_ERR(( "[fd_tower_vote] double-voting for old slot %lu (new vote: %lu)", - slot, - vote->slot )); + FD_LOG_ERR(( "[fd_tower_vote] double-voting for old slot %lu (new vote: %lu)", slot, vote->slot )); } } #endif - /* First, simulate a vote for slot. We do this purely for - implementation convenience and code reuse. - - As the name of this function indicates, we are not just - simulating and in fact voting for this fork by pushing this a new - vote onto the tower. */ + /* Use simulate_vote to determine how many expired votes to pop. */ ulong cnt = simulate_vote( tower->votes, slot ); @@ -872,7 +776,8 @@ fd_tower_vote( fd_tower_t const * tower, ulong slot ) { fd_tower_votes_pop_tail( tower->votes ); } - /* Increase confirmations (double lockouts) in consecutive votes. */ + /* Increment confirmations (double lockouts) for consecutive + confirmations in prior votes. */ ulong prev_conf = 0; for( fd_tower_votes_iter_t iter = fd_tower_votes_iter_init_rev( tower->votes ); @@ -890,6 +795,11 @@ fd_tower_vote( fd_tower_t const * tower, ulong slot ) { fd_tower_votes_push_tail( tower->votes, ( fd_tower_vote_t ){ .slot = slot, .conf = 1 } ); } +ulong +fd_tower_simulate_vote( fd_tower_t const * tower, ulong slot ) { + return simulate_vote( tower->votes, slot ); +} + void fd_tower_print( fd_tower_t const * tower ) { print( tower->votes, tower->root ); @@ -920,118 +830,6 @@ fd_tower_vote_state_cmp( fd_tower_t const * tower, fd_vote_state_t * vote_state return fd_int_if( local == cluster, 0, fd_int_if( local > cluster, 1, -1 ) ); } -fd_vote_state_t * -fd_tower_vote_state_query( FD_PARAM_UNUSED fd_tower_t const * tower, - fd_pubkey_t const * vote_acc_addr, - fd_acc_mgr_t * acc_mgr, - fd_fork_t const * fork, - fd_valloc_t valloc, - fd_vote_state_versioned_t * versioned ) { - int rc; - - FD_BORROWED_ACCOUNT_DECL( vote_acc ); - rc = fd_acc_mgr_view( acc_mgr, fork->slot_ctx.funk_txn, vote_acc_addr, vote_acc ); - if( FD_UNLIKELY( rc == FD_ACC_MGR_ERR_UNKNOWN_ACCOUNT ) ) { - FD_LOG_WARNING(( "[%s] fd_acc_mgr_view could not find vote account %s. error: %d", - __func__, - FD_BASE58_ENC_32_ALLOCA( vote_acc_addr ), - rc )); - return NULL; - } else if( FD_UNLIKELY( rc != FD_ACC_MGR_SUCCESS ) ) { - FD_LOG_ERR(( "[%s] fd_acc_mgr_view failed on vote account %s. error: %d", - __func__, - FD_BASE58_ENC_32_ALLOCA( vote_acc_addr ), - rc )); - } - - rc = fd_vote_get_state( vote_acc, valloc, versioned ); - if( FD_UNLIKELY( rc != FD_ACC_MGR_SUCCESS ) ) { - FD_LOG_ERR(( "[%s] fd_vote_get_state failed on vote account %s. error: %d", - __func__, - FD_BASE58_ENC_32_ALLOCA( vote_acc_addr ), - rc )); - } - - fd_vote_convert_to_current( versioned, valloc ); - - return &versioned->inner.current; -} - -void -fd_tower_from_vote_state( fd_tower_t * tower, fd_vote_state_t * vote_state ) { -#if FD_TOWER_USE_HANDHOLDING - if( FD_UNLIKELY( tower->root ) ) { - FD_LOG_WARNING(( "[%s] overwriting existing tower root %lu.", __func__, tower->root )); - } - - if( FD_UNLIKELY( !fd_tower_votes_empty( tower->votes ) ) ) { - FD_LOG_WARNING(( "[%s] overwriting existing tower votes.", __func__ )); - } - - if( FD_UNLIKELY( !vote_state->has_root_slot ) ) { - FD_LOG_WARNING(( "[%s] vote_state is missing root.", __func__ )); - } - - if( FD_UNLIKELY( deq_fd_landed_vote_t_empty( vote_state->votes ) ) ) { - FD_LOG_WARNING(( "[%s] vote_state is empty.", __func__ )); - } - -#endif - - tower_votes_from_landed_votes( tower->votes, vote_state->votes ); - tower->root = vote_state->root_slot; -} - -// ulong i = fd_tower_votes_cnt( tower->votes ); -// while ( fd_tower_vote(const fd_tower_t *tower, ulong slot) ) - -// /* If the local view of our tower is a strict subset or divergent from -// the cluster view, then we need to sync with the cluster ie. replace -// our local state with the cluster state. */ - -// int sync = 0; -// ulong i = 0; -// for( deq_fd_landed_vote_t_iter_t iter = deq_fd_landed_vote_t_iter_init( vote_state->votes ); -// !deq_fd_landed_vote_t_iter_done( vote_state->votes, iter ); -// iter = deq_fd_landed_vote_t_iter_next( vote_state->votes, iter ) ) { -// fd_landed_vote_t const * landed_vote = -// deq_fd_landed_vote_t_iter_ele_const( vote_state->votes, iter ); -// fd_tower_vote_t const * tower_vote = fd_tower_votes_peek_index_const( tower->votes, i++ ); - -// if( FD_UNLIKELY( !tower_vote ) ) { - -// /* Local tower is shorter, need sync*/ - -// sync = 1; -// } - -// if( FD_UNLIKELY( tower_vote->slot != landed_vote->lockout.slot || -// tower_vote->conf != landed_vote->lockout.confirmation_count ) ) { - -// /* Local tower diverges, need sync */ - -// sync = 1; -// } -// } - -// /* Note we don't sync if the local view of the tower is taller. -// Syncing can lead to a problem where we can otherwise get "stuck" in -// a cluster-sync loop when our votes don't land. For example, if our -// tower is currently [1 3 5], and we vote [1 3 5 7]. That vote -// doesn't land, so we re-sync and as a result vote [1 3 5 9]. -// Instead we would prefer [1 3 5 7 9] for the second vote. */ - -// if( FD_UNLIKELY( sync ) ) { - -// FD_LOG_WARNING(("syncing with cluster")); - -// /* Sync local with cluster. */ - -// fd_tower_votes_remove_all( tower->votes ); -// tower_votes_from_landed_votes( tower->votes, vote_state->votes ); -// tower->root = vote_state->root_slot; -// } - void fd_tower_to_tower_sync( fd_tower_t const * tower, fd_hash_t const * bank_hash, diff --git a/src/choreo/tower/fd_tower.h b/src/choreo/tower/fd_tower.h index f9ac58ffbf..6b04b20e2d 100644 --- a/src/choreo/tower/fd_tower.h +++ b/src/choreo/tower/fd_tower.h @@ -337,10 +337,6 @@ #include "../forks/fd_forks.h" #include "../ghost/fd_ghost.h" -#define FD_TOWER_EQV_SAFE ( 0.52 ) -#define FD_TOWER_OPT_CONF ( 2.0 / 3.0 ) -#define FD_TOWER_VOTE_MAX ( 32UL ) - /* FD_TOWER_USE_HANDHOLDING: Define this to non-zero at compile time to turn on additional runtime checks and logging. */ @@ -348,6 +344,8 @@ #define FD_TOWER_USE_HANDHOLDING 1 #endif +#define FD_TOWER_VOTE_MAX (32UL) + struct fd_tower_vote { ulong slot; /* vote slot */ ulong conf; /* confirmation count */ @@ -376,6 +374,8 @@ typedef struct fd_tower_vote_acc fd_tower_vote_acc_t; /* clang-format off */ struct __attribute__((aligned(128UL))) fd_tower { + /* Owned memory */ + /* The votes currently in the tower, ordered from latest to earliest vote slot (lowest to highest confirmation count). */ @@ -470,22 +470,23 @@ fd_tower_delete( void * tower ); In general, this should be called by the same process that formatted tower's memory, ie. the caller of fd_tower_new. */ void -fd_tower_init( fd_tower_t * tower, - fd_pubkey_t const * vote_acc_addr, - fd_acc_mgr_t * acc_mgr, - fd_exec_epoch_ctx_t const * epoch_ctx, - fd_fork_t const * fork, - ulong * smr ); +fd_tower_init( fd_tower_t * tower, + fd_pubkey_t const * vote_acc_addr, + fd_funk_t * funk, + fd_funk_txn_t const * txn, + ulong * smr ); /* fd_tower_lockout_check checks if we are locked out from voting for - fork. Returns 1 if we can vote for fork without violating lockout, 0 - otherwise. + `slot`. Returns 1 if we can vote for `slot` without violating + lockout, 0 otherwise. After voting for a slot n, we are locked out for 2^k slots, where k is the confirmation count of that vote. Once locked out, we cannot vote for a different fork until that previously-voted fork expires at slot n+2^k. This implies the earliest slot in which we can switch - from the previously-voted fork is (n+2^k)+1. + from the previously-voted fork is (n+2^k)+1. We use `ghost` to + determine whether `slot` is on the same or different fork as previous + vote slots. In the case of the tower, every vote has its own expiration slot depending on confirmations. The confirmation count is the max number @@ -540,8 +541,8 @@ fd_tower_init( fd_tower_t * tower, int fd_tower_lockout_check( fd_tower_t const * tower, - fd_fork_t const * fork, - fd_ghost_t const * ghost ); + fd_ghost_t const * ghost, + ulong slot ); /* fd_tower_switch_check checks if we can switch to fork. Returns 1 if we can switch, 0 otherwise. @@ -569,32 +570,32 @@ fd_tower_lockout_check( fd_tower_t const * tower, return switch stake >= FD_TOWER_SWITCH_PCT ``` - The switch check is used to safeguard optimistic confirmation. - Invariant: FD_TOWER_OPT_CONF_PCT + FD_TOWER_SWITCH_PCT >= 1. */ + Specifically: FD_TOWER_OPT_CONF_PCT + FD_TOWER_SWITCH_PCT >= 1. */ int -fd_tower_switch_check( fd_tower_t const * tower, fd_fork_t const * fork, fd_ghost_t const * ghost ); +fd_tower_switch_check( fd_tower_t const * tower, + fd_ghost_t const * ghost, + ulong slot ); /* fd_tower_threshold_check checks if we pass the threshold required to - continue voting along the same fork as our last vote. Returns 1 if - we pass the threshold check, 0 otherwise. + vote for `slot`. This is only relevant after voting for (and + confirming) the same fork ie. the tower is FD_TOWER_THRESHOLD_DEPTH + deep. Returns 1 if we pass the threshold check, 0 otherwise. The following psuedocode describes the algorithm: ``` - for all vote accounts on the current fork + for all vote accounts in the current epoch - simulate that the validator has voted on the current slot (the - fork head) + simulate that the validator has voted for `slot` pop all votes expired by that simulated vote - if validator's latest tower vote after expiry >= our threshold + if the validator's latest tower vote after expiry >= our threshold slot ie. our vote from FD_TOWER_THRESHOLD_DEPTH back (after - simulating a vote on our own tower the same way) - - add validator's stake to threshold_stake. + simulating a vote on our own tower the same way), then add + validator's stake to threshold_stake. return threshold_stake >= FD_TOWER_THRESHOLD_PCT ``` @@ -604,9 +605,10 @@ fd_tower_switch_check( fd_tower_t const * tower, fd_fork_t const * fork, fd_ghos long time from counting towards the threshold stake. */ int -fd_tower_threshold_check( fd_tower_t const * tower, - fd_fork_t const * fork, - fd_acc_mgr_t * acc_mgr ); +fd_tower_threshold_check( fd_tower_t const * tower, + fd_funk_t * funk, + fd_funk_txn_t const * txn, + ulong slot ); /* fd_tower_best_fork picks the best fork, where best is defined as the fork head containing the highest stake-weight in its ancestry. @@ -629,9 +631,9 @@ fd_tower_best_fork( fd_tower_t const * tower, fd_forks_t const * forks, fd_ghost fd_fork_t const * fd_tower_reset_fork( fd_tower_t const * tower, fd_forks_t const * forks, fd_ghost_t const * ghost ); -/* fd_tower_vote_fork picks which frontier fork to vote on. Returns NULL - if we cannot vote because we are locked out, do not meet switch - threshold, or fail the threshold check. +/* fd_tower_vote_fork picks which fork in the frontier in `forks` to + vote on. Returns NULL if we cannot vote because we are locked out, + do not meet switch threshold, or fail the threshold check. Modifies the tower to record the vote slot of the fork we select. */ @@ -655,11 +657,17 @@ fd_tower_epoch_update( fd_tower_t * tower, fd_exec_epoch_ctx_t const * epoch_ctx fork->slot, not before. */ void -fd_tower_fork_update( fd_tower_t const * tower, - fd_fork_t const * fork, - fd_acc_mgr_t * acc_mgr, - fd_blockstore_t * blockstore, - fd_ghost_t * ghost ); +fd_tower_fork_update( fd_tower_t const * tower, + fd_blockstore_t * blockstore, + fd_ghost_t * ghost, + fd_funk_t * funk, + fd_funk_txn_t const * txn ); + +/* fd_tower_vote votes for slot. Assumes caller has already performed + all the tower checks to ensure this is a valid vote. */ + +void +fd_tower_vote( fd_tower_t const * tower, ulong slot ); /* fd_tower_simulate_vote simulates a vote on the vote tower for slot, returning the new height (cnt) for all the votes that would have been @@ -668,12 +676,6 @@ fd_tower_fork_update( fd_tower_t const * tower, ulong fd_tower_simulate_vote( fd_tower_t const * tower, ulong slot ); -/* fd_tower_vote votes for slot. Assumes caller has already performed - all the tower checks to ensure this is a valid vote. */ - -void -fd_tower_vote( fd_tower_t const * tower, ulong slot ); - /* fd_tower_is_max_lockout returns 1 if the bottom vote of the tower has reached max lockout, 0 otherwise. Max lockout is equivalent to 1 << FD_TOWER_VOTE_MAX (equivalently, confirmation count is @@ -686,8 +688,14 @@ fd_tower_is_max_lockout( fd_tower_t const * tower ) { } /* fd_tower_publish publishes the tower. Returns the new root. Assumes - caller has already checked that tower has reached max lockout (see - fd_tower_is_max_lockout). */ + caller has already checked that tower is at max lockout (see + fd_tower_is_max_lockout). + + smr is a non-NULL pointer to an fseq that always contains the highest + observed smr. + + IMPORTANT! Caller should not read or modify this value outside the + fseq API. */ static inline ulong fd_tower_publish( fd_tower_t * tower ) { @@ -696,7 +704,7 @@ fd_tower_publish( fd_tower_t * tower ) { #endif ulong root = fd_tower_votes_pop_head( tower->votes ).slot; - FD_LOG_NOTICE( ( "[%s] root %lu", __func__, tower->root ) ); + FD_LOG_DEBUG( ( "[%s] root %lu", __func__, tower->root ) ); tower->root = root; return root; } diff --git a/src/choreo/tower/test_tower.c b/src/choreo/tower/test_tower.c index ec9f31ee52..b055e1707a 100644 --- a/src/choreo/tower/test_tower.c +++ b/src/choreo/tower/test_tower.c @@ -1,73 +1,61 @@ #include "fd_tower.h" -int -main( int argc, char ** argv ) { - fd_boot( &argc, &argv ); - - /* Initialize the test workspace */ - char const * _page_sz = fd_env_strip_cmdline_cstr ( &argc, &argv, "--page-sz", NULL, "gigantic" ); - ulong page_cnt = fd_env_strip_cmdline_ulong( &argc, &argv, "--page-cnt", NULL, 1UL ); - ulong numa_idx = fd_env_strip_cmdline_ulong( &argc, &argv, "--numa-idx", NULL, fd_shmem_numa_idx( 0 ) ); - - FD_LOG_NOTICE(( "Creating workspace with --page-cnt %lu --page-sz %s pages on --numa-idx %lu", page_cnt, _page_sz, numa_idx )); - - ulong page_sz = fd_cstr_to_shmem_page_sz( _page_sz ); - if( FD_UNLIKELY( !page_sz ) ) FD_LOG_ERR(( "unsupported --page-sz" )); - fd_wksp_t * wksp = fd_wksp_new_anonymous( page_sz, page_cnt, fd_shmem_cpu_idx( numa_idx ), "wksp", 0UL ); - FD_TEST( wksp ); +uchar mem[16384] __attribute__((aligned(alignof(fd_tower_t)))); - /* Create a new tower and join it */ - void *mem = fd_wksp_alloc_laddr( wksp, fd_tower_align(), fd_tower_footprint(), 1UL ); - fd_tower_t *tower = fd_tower_join( fd_tower_new( mem ) ); - FD_TEST_CUSTOM(tower, "Failed to join the tower"); +void +test_tower_vote( void ) { + fd_tower_t * tower = fd_tower_join( fd_tower_new( mem ) ); + FD_TEST( tower ); /* Add some votes to the tower - (0, 31) expiration = 0 + 1<<31 (some big number) - (1, 30) expiration = 1 + 1<<30 (some big number) - (2, 29) expiration = 2 + 1<<29 (some big number) + + (0, 31) expiration = 0 + 1<<31 + (1, 30) expiration = 1 + 1<<30 + (2, 29) expiration = 2 + 1<<29 .. (28, 3) expiration = 28 + 1<<3 = 36 (29, 2) expiration = 29 + 1<<2 = 33 (30, 1) expiration = 30 + 1<<1 = 32 */ - for (ulong i = 0; i < 31; i++) - { + + for( ulong i = 0; i < 31; i++ ) { fd_tower_vote( tower, i ); FD_TEST( fd_tower_votes_cnt( tower->votes ) == i + 1 ); } - for (ulong i = 0; i < 31; i++) - { - fd_tower_vote_t expected_vote = { .slot = i, .conf = 31-i }; - fd_tower_vote_t *actual_vote = fd_tower_votes_peek_index( tower->votes, i ); + for( ulong i = 0; i < 31; i++ ) { + fd_tower_vote_t expected_vote = { .slot = i, .conf = 31 - i }; + fd_tower_vote_t * actual_vote = fd_tower_votes_peek_index( tower->votes, i ); FD_TEST( expected_vote.slot == actual_vote->slot ); FD_TEST( expected_vote.conf == actual_vote->conf ); } /* CASE 1: NEW VOTE WHICH REPLACES EXPIRED VOTE */ - /* Check expiration - A vote for 33 should make the vote for 30 expire. - A full tower has 31 votes. One expired vote => 30 remaining.*/ + /* Test expiration + + A vote for 33 should make the vote for 30 expire. + A full tower has 31 votes. One expired vote => 30 remaining. */ + ulong new_vote_expiry = 33; - ulong vote_cnt = fd_tower_simulate_vote( tower, new_vote_expiry ); + ulong vote_cnt = fd_tower_simulate_vote( tower, new_vote_expiry ); FD_TEST( vote_cnt == 30 ); - /* Check slots 1 through 30 are unchanged after voting */ + /* Test slots 1 through 30 are unchanged after voting */ + fd_tower_vote( tower, new_vote_expiry ); - for (ulong i = 0; i < 30; i++) - { - fd_tower_vote_t expected_vote = { .slot = i, .conf = 31-i }; - fd_tower_vote_t *actual_vote = fd_tower_votes_peek_index( tower->votes, i ); + for( ulong i = 0; i < 30; i++ ) { + fd_tower_vote_t expected_vote = { .slot = i, .conf = 31 - i }; + fd_tower_vote_t * actual_vote = fd_tower_votes_peek_index( tower->votes, i ); FD_TEST( expected_vote.slot == actual_vote->slot ); FD_TEST( expected_vote.conf == actual_vote->conf ); } /* Check new vote */ - fd_tower_vote_t expected_vote = { .slot = new_vote_expiry, .conf = 1 }; - fd_tower_vote_t *actual_vote = fd_tower_votes_peek_index( tower->votes, 30 ); + + fd_tower_vote_t expected_vote = { .slot = new_vote_expiry, .conf = 1 }; + fd_tower_vote_t * actual_vote = fd_tower_votes_peek_index( tower->votes, 30 ); FD_TEST( expected_vote.slot == actual_vote->slot ); FD_TEST( expected_vote.conf == actual_vote->conf ); - /* CASE 2: NEW VOTE WHICH PRODUCES NEW ROOT */ ulong new_vote_root = 34; @@ -75,30 +63,35 @@ main( int argc, char ** argv ) { FD_TEST( fd_tower_is_max_lockout( tower ) ); /* Check root */ + ulong expected_root = 0; - ulong actual_root = fd_tower_publish( tower ); - FD_LOG_NOTICE(( "actual root %lu; expected root %lu", actual_root, expected_root )); + ulong actual_root = fd_tower_publish( tower ); FD_TEST( actual_root == expected_root ); - /* Check all existing votes moved up by one, with one additional confirmation */ - for (ulong i = 0; i < 29 /* one of the original slots was rooted */; i++) - { - fd_tower_vote_t expected_vote = { .slot = i+1, .conf = 31-i }; - fd_tower_vote_t *actual_vote = fd_tower_votes_peek_index( tower->votes, i ); - FD_LOG_INFO(( "evs %lu; avs %lu", expected_vote.slot, actual_vote->slot )); + /* Check all existing votes were repositioned one index lower and one + confirmation higher. */ + + for( ulong i = 0; i < 29 /* one of the original slots was rooted */; i++ ) { + fd_tower_vote_t expected_vote = { .slot = i + 1, .conf = 31 - i }; + fd_tower_vote_t * actual_vote = fd_tower_votes_peek_index( tower->votes, i ); FD_TEST( expected_vote.slot == actual_vote->slot ); FD_TEST( expected_vote.conf == actual_vote->conf ); } - /* Check new vote */ - fd_tower_vote_t expected_vote_root = { .slot = new_vote_root, .conf = 1 }; - fd_tower_vote_t *actual_vote_root = fd_tower_votes_peek_index( tower->votes, 30 ); + /* Check new vote in the tower. */ + + fd_tower_vote_t expected_vote_root = { .slot = new_vote_root, .conf = 1 }; + fd_tower_vote_t * actual_vote_root = fd_tower_votes_peek_index( tower->votes, 30 ); FD_TEST( expected_vote_root.slot == actual_vote_root->slot ); FD_TEST( expected_vote_root.conf == actual_vote_root->conf ); - fd_tower_delete( tower ); - fd_tower_leave( tower ); + fd_tower_delete( fd_tower_leave( tower ) ); +} +int +main( int argc, char ** argv ) { + fd_boot( &argc, &argv ); + test_tower_vote(); fd_halt(); return 0; } diff --git a/src/choreo/voter/Local.mk b/src/choreo/voter/Local.mk index bc161c511e..e26d3d2c91 100644 --- a/src/choreo/voter/Local.mk +++ b/src/choreo/voter/Local.mk @@ -2,7 +2,7 @@ ifdef FD_HAS_INT128 $(call add-hdrs,fd_voter.h) $(call add-objs,fd_voter,fd_choreo) ifdef FD_HAS_HOSTED -$(call make-bin,fd_voter_ctl,fd_voter_ctl,fd_choreo fd_flamenco fd_ballet fd_util) -$(call make-unit-test,test_voter,test_voter,fd_choreo fd_flamenco fd_ballet fd_util) +$(call make-bin,fd_voter_ctl,fd_voter_ctl,fd_choreo fd_flamenco fd_funk fd_ballet fd_util) +$(call make-unit-test,test_voter,test_voter,fd_choreo fd_flamenco fd_funk fd_ballet fd_util) endif endif diff --git a/src/choreo/voter/fd_voter.c b/src/choreo/voter/fd_voter.c index 306049a3a1..7c8f954e50 100644 --- a/src/choreo/voter/fd_voter.c +++ b/src/choreo/voter/fd_voter.c @@ -2,68 +2,39 @@ #include -void * -fd_voter_new( void * shmem ) { - if( FD_UNLIKELY( !shmem ) ) { - FD_LOG_WARNING( ( "NULL mem" ) ); +fd_voter_state_t const * +fd_voter_state( fd_funk_t * funk, fd_funk_txn_t const * txn, fd_funk_rec_key_t const * key ) { + fd_funk_rec_t const * rec = fd_funk_rec_query_global( funk, txn, key, NULL ); + if( FD_UNLIKELY( !rec || !!( rec->flags & FD_FUNK_REC_FLAG_ERASE ) ) ) { return NULL; } - - if( FD_UNLIKELY( !fd_ulong_is_aligned( (ulong)shmem, fd_voter_align() ) ) ) { - FD_LOG_WARNING( ( "misaligned mem" ) ); - return NULL; - } - - ulong footprint = fd_voter_footprint(); - if( FD_UNLIKELY( !footprint ) ) { - FD_LOG_WARNING( ( "bad mem" ) ); - return NULL; - } - - return shmem; -} - -fd_voter_t * -fd_voter_join( void * shvoter ) { - - if( FD_UNLIKELY( !shvoter ) ) { - FD_LOG_WARNING( ( "NULL voter" ) ); - return NULL; - } - - if( FD_UNLIKELY( !fd_ulong_is_aligned( (ulong)shvoter, fd_voter_align() ) ) ) { - FD_LOG_WARNING( ( "misaligned voter" ) ); - return NULL; - } - - return (fd_voter_t *)shvoter; -} - -void * -fd_voter_leave( fd_voter_t const * voter ) { - - if( FD_UNLIKELY( !voter ) ) { - FD_LOG_WARNING( ( "NULL voter" ) ); - return NULL; - } - - return (void *)voter; + fd_account_meta_t const * meta = fd_funk_val_const( rec, fd_funk_wksp(funk) ); + FD_TEST( meta->magic == FD_ACCOUNT_META_MAGIC ); + fd_voter_state_t const * state = fd_type_pun_const( (uchar const *)meta + meta->hlen ); + #if FD_TOWER_USE_HANDHOLDING + FD_TEST( state->discriminant <= fd_vote_state_versioned_enum_current ); + #endif + return state; } -void * -fd_voter_delete( void * voter ) { - - if( FD_UNLIKELY( !voter ) ) { - FD_LOG_WARNING( ( "NULL voter" ) ); - return NULL; +void +fd_voter_state_tower( fd_voter_state_t const * state, fd_tower_t * tower ) { + #if FD_TOWER_USE_HANDHOLDING + if( FD_UNLIKELY( fd_tower_votes_cnt( tower->votes ) ) ) FD_LOG_ERR(( "[%s] cannot write to non-empty tower", __func__ )); + if( FD_UNLIKELY( tower->root != 0 && tower->root != FD_SLOT_NULL ) ) FD_LOG_ERR(( "[%s] cannot write to tower with a root", __func__ )); + #endif + + fd_tower_vote_t vote = { 0 }; + ulong vote_sz = sizeof(ulong) /* slot */ + sizeof(uint); /* conf */ + for( ulong i = 0; i < fd_voter_state_cnt( state ); i++ ) { + if( FD_UNLIKELY( state->discriminant == fd_vote_state_versioned_enum_v0_23_5 ) ) { + memcpy( (uchar *)&vote, (uchar *)&state->v0_23_5.tower.votes[i], vote_sz ); + } else { + memcpy( (uchar *)&vote, (uchar *)&state->tower.votes[i] + sizeof(uchar) /* latency */, vote_sz ); + } + fd_tower_votes_push_tail( tower->votes, vote ); } - - if( FD_UNLIKELY( !fd_ulong_is_aligned( (ulong)voter, fd_voter_align() ) ) ) { - FD_LOG_WARNING( ( "misaligned voter" ) ); - return NULL; - } - - return voter; + tower->root = fd_voter_state_root( state ); } ulong @@ -72,7 +43,7 @@ fd_voter_txn_generate( fd_voter_t const * voter, fd_hash_t const * recent_blockhash, uchar txn_meta_out[static FD_TXN_MAX_SZ], uchar txn_out[static FD_TXN_MTU] ) { - FD_LOG_NOTICE(( "[%s]: vote acc addr %s", __func__, FD_BASE58_ENC_32_ALLOCA( &voter->vote_acc_addr ) )); + FD_LOG_NOTICE(( "[%s]: vote acc addr %s", __func__, FD_BASE58_ENC_32_ALLOCA( &voter->addr ) )); int same_addr = !memcmp( &voter->validator_identity, &voter->vote_authority, @@ -90,7 +61,7 @@ fd_voter_txn_generate( fd_voter_t const * voter, accts.acct_cnt = 3; accts.signers_w = &voter->validator_identity; accts.signers_r = NULL; - accts.non_signers_w = &voter->vote_acc_addr; + accts.non_signers_w = &voter->addr; accts.non_signers_r = &fd_solana_vote_program_id; FD_TEST( fd_txn_base_generate( txn_meta_out, txn_out, @@ -111,7 +82,7 @@ fd_voter_txn_generate( fd_voter_t const * voter, accts.acct_cnt = 4; accts.signers_w = &voter->validator_identity; accts.signers_r = &voter->vote_authority; - accts.non_signers_w = &voter->vote_acc_addr; + accts.non_signers_w = &voter->addr; accts.non_signers_r = &fd_solana_vote_program_id; FD_TEST( fd_txn_base_generate( txn_meta_out, txn_out, diff --git a/src/choreo/voter/fd_voter.h b/src/choreo/voter/fd_voter.h index 7e629b85a6..e2e924df5b 100644 --- a/src/choreo/voter/fd_voter.h +++ b/src/choreo/voter/fd_voter.h @@ -6,59 +6,156 @@ #include "../../flamenco/txn/fd_txn_generate.h" #include "../fd_choreo_base.h" #include "../forks/fd_forks.h" +#include "../tower/fd_tower.h" struct fd_voter { - fd_pubkey_t vote_acc_addr; + ulong key; /* map key */ + ulong next; /* reserved for use by fd_pool_para.c */ + ulong hash; /* reserved for use by fd_map_para.c */ + + union { + fd_pubkey_t addr; /* vote account address */ + fd_funk_rec_key_t rec_key; /* funk record key to query above */ + }; + + ulong stake; fd_pubkey_t validator_identity; fd_pubkey_t vote_authority; }; typedef struct fd_voter fd_voter_t; -/* fd_voter_{align,footprint} return the required alignment and - footprint of a memory region suitable for use as voter. align is - double cache line to mitigate false sharing. */ - -FD_FN_CONST static inline ulong -fd_voter_align( void ) { - return alignof( fd_voter_t ); +/* fd_voter_state is a struct representation of the bincode-serialized + layout of a voter's state (known in Agave parlance as "VoteState"). + This struct is then used to support zero-copy access of members. + + The voter's state is versioned, and the serialized formats differ + depending on this. Currently, the only version that differs from the + others that is relevant here is v0.23.5. Thus v0.23.5 has its own + dedicated struct definition with a different set of fields that + precede the votes than the other versions. Furthermore, v0.23.5 + contains votes of type `fd_vote_lockout_t` vs. the other versions + which are of type `fd_landed_vote_t`. The only difference between + `fd_vote_lockout_t` and `fd_landed_vote_t` is there is an additional + uchar field `latency`, so that is we include an offset of 0 or 1 + depending on which vote state type it is. + + The layout begins with a set of fields providing important metadata + about the voter. Immediately following these fields is the tower + itself. The tower layout begins with the number of votes currently in + the tower ie. `cnt`. Then the votes themselves follow. The format of + the votes varies depending on the version. Finally the layout + concludes with the tower's root slot. + + -------- + metadata <- variable. depends on vote state version + -------- + cnt <- 8 bytes. bincode-serialized u64 + -------- + votes <- variable. depends on vote state version and cnt + -------- + root <- 5 or 1 byte(s). bincode-serialized Option + -------- +*/ + +struct fd_voter_state { + uint discriminant; + union { + struct __attribute__((packed)) { + fd_pubkey_t node_pubkey; + fd_pubkey_t authorized_voter; + ulong authorized_voter_epoch; + uchar prior_voters[ (32 * 56 + sizeof(ulong)) /* serialized bincode sz */ ]; + fd_pubkey_t authorized_withdrawer; + uchar commission; + struct __attribute__((packed)) fd_voter_state_tower_v0_23_5 { + ulong cnt; + struct __attribute__((packed)) fd_voter_state_tower_vote_v0_23_5 { + ulong slot; + uint conf; + } votes[32]; /* only first `cnt` elements are valid */ + } tower; + } v0_23_5; + + struct __attribute__((packed)) { + fd_pubkey_t node_pubkey; + fd_pubkey_t authorized_withdrawer; + uchar commission; + struct __attribute__((packed)) fd_voter_state_tower { + ulong cnt; + struct __attribute__((packed)) fd_voter_state_tower_vote { + uchar latency; + ulong slot; + uint conf; + } votes[32]; /* only first `cnt` elements are valid */ + } tower; + }; + + /* The tower's root (a bincode-serialized Option) follows + votes. Because the preceding votes are variable-length in + serialized form, we cannot encode the root directly inside the + struct. */ + }; +}; +typedef struct fd_voter_state_tower_vote_v0_23_5 fd_voter_state_tower_vote_v0_23_5_t; +typedef struct fd_voter_state_tower_vote fd_voter_state_tower_vote_t; +typedef struct fd_voter_state_tower_v0_23_5 fd_voter_state_tower_v0_23_5_t; +typedef struct fd_voter_state_tower fd_voter_state_tower_t; +typedef struct fd_voter_state fd_voter_state_t; + +/* fd_voter_state queries funk for the record in the provided `txn` and + `key`. Returns a pointer to the start of the voter's state. Assumes + `key` is a vote account address and the record is a voter's state + (fd_voter_state_t). U.B. if `key` does not point to a valid vote + account. */ + +fd_voter_state_t const * +fd_voter_state( fd_funk_t * funk, fd_funk_txn_t const * txn, fd_funk_rec_key_t const * key ); + +/* fd_voter_state_cnt returns the number of votes in the voter's tower. + Assumes `state` is a valid fd_voter_state_t. */ + +FD_FN_PURE static inline ulong +fd_voter_state_cnt( fd_voter_state_t const * state ) { + if( FD_UNLIKELY( state->discriminant == fd_vote_state_versioned_enum_v0_23_5 ) ) { + return state->v0_23_5.tower.cnt; + } + return state->tower.cnt; } -FD_FN_CONST static inline ulong -fd_voter_footprint( void ) { - return sizeof( fd_voter_t ); +/* fd_voter_state_vote returns the voter's most recent vote (ie. the + last vote of the tower in the voter's state). Assumes `state` is a + valid fd_voter_state_t. */ + +FD_FN_PURE static inline ulong +fd_voter_state_vote( fd_voter_state_t const * state ) { + if( FD_UNLIKELY( !fd_voter_state_cnt( state ) ) ) return FD_SLOT_NULL; + if( FD_UNLIKELY( state->discriminant == fd_vote_state_versioned_enum_v0_23_5 ) ) { + return state->v0_23_5.tower.votes[state->tower.cnt - 1].slot; + } + return state->tower.votes[state->tower.cnt - 1].slot; } -/* fd_voter_new formats an unused memory region for use as a voter. mem - is a non-NULL pointer to this region in the local address space with - the required footprint and alignment. */ - -void * -fd_voter_new( void * mem ); - -/* fd_voter_join joins the caller to the voter. voter points to the - first byte of the memory region backing the voter in the caller's - address space. - - Returns a pointer in the local address space to voter on success. */ - -fd_voter_t * -fd_voter_join( void * voter ); - -/* fd_voter_leave leaves a current local join. Returns a pointer to the - underlying shared memory region on success and NULL on failure (logs - details). Reasons for failure include voter is NULL. */ - -void * -fd_voter_leave( fd_voter_t const * voter ); +/* fd_voter_state_root returns the voter's tower root. Assumes `state` + is a valid fd_voter_state_t. */ + +FD_FN_PURE static inline ulong +fd_voter_state_root( fd_voter_state_t const * state ) { + uchar * root = fd_ptr_if( + state->discriminant == fd_vote_state_versioned_enum_v0_23_5, + (uchar *)&state->v0_23_5.tower.votes + sizeof(fd_voter_state_tower_vote_v0_23_5_t) * state->v0_23_5.tower.cnt, + (uchar *)&state->tower.votes + sizeof(fd_voter_state_tower_vote_t) * state->tower.cnt + ); + uchar is_some = *(uchar *)root; /* whether the Option is a Some type */ + if( FD_UNLIKELY( !is_some ) ) return 0; /* the implicit root is the genesis slot */ + return *(ulong *)(root+sizeof(uchar)); +} -/* fd_voter_delete unformats a memory region used as a voter. Assumes - only the local process is joined to the region. Returns a pointer to - the underlying shared memory region or NULL if used obviously in - error (e.g. voter is obviously not a voter ... logs details). The - ownership of the memory region is transferred to the caller. */ +/* fd_voter_state_tower writes the saved tower inside `state` to the + caller-provided `tower`. Assumes `tower` is a valid join of an + fd_tower that is currently empty. */ -void * -fd_voter_delete( void * voter ); +void +fd_voter_state_tower( fd_voter_state_t const * state, fd_tower_t * tower ); /* fd_voter_txn_generate generates a vote txn using the TowerSync ix. */ diff --git a/src/choreo/voter/test_voter.c b/src/choreo/voter/test_voter.c index 7f186e6779..06caf2b589 100644 --- a/src/choreo/voter/test_voter.c +++ b/src/choreo/voter/test_voter.c @@ -69,7 +69,7 @@ main( int argc, char ** argv ) { uchar txn_meta_buf[FD_TXN_MAX_SZ]; uchar txn_buf[FD_TXN_MTU]; - fd_voter_t voter = { .vote_acc_addr = vote_acct_addr, + fd_voter_t voter = { .addr = vote_acct_addr, .validator_identity = *validator_identity, .vote_authority = *validator_identity }; diff --git a/src/disco/consensus/test_gossip_echo_vote.c b/src/disco/consensus/test_gossip_echo_vote.c index d4416d9deb..69d56efe9a 100644 --- a/src/disco/consensus/test_gossip_echo_vote.c +++ b/src/disco/consensus/test_gossip_echo_vote.c @@ -308,7 +308,7 @@ gossip_deliver_fun( fd_crds_data_t * data, void * arg ) { fd_pubkey_t const * vote_authority_pubkey = (fd_pubkey_t const *)fd_type_pun_const( arg_->vote_authority_keypair + 32UL ); fd_pubkey_t const * validator_identity_pubkey = (fd_pubkey_t const *)fd_type_pun_const( arg_->validator_identity_keypair + 32UL ); fd_voter_t voter = { - .vote_acc_addr = *arg_->vote_acct_addr, + .addr = *arg_->vote_acct_addr, .vote_authority = *vote_authority_pubkey, .validator_identity = *validator_identity_pubkey }; diff --git a/src/disco/store/fd_epoch_forks.c b/src/disco/store/fd_epoch_forks.c index ddce95c3cc..d68cf5b6e2 100644 --- a/src/disco/store/fd_epoch_forks.c +++ b/src/disco/store/fd_epoch_forks.c @@ -89,7 +89,7 @@ fd_epoch_forks_get_epoch_ctx( fd_epoch_forks_t * epoch_forks, fd_ghost_t * ghost /* check if this fork has a parent in the entries list, and isn't the parent itself. */ ulong slot = (opt_prev_slot == NULL) ? curr_slot : *opt_prev_slot; - if( elem->parent_slot != slot && elem->parent_slot >= fd_ghost_root_node(ghost)->slot && fd_ghost_is_descendant( ghost, slot, elem->parent_slot ) ) { + if( elem->parent_slot != slot && elem->parent_slot >= fd_ghost_root(ghost)->slot && fd_ghost_is_ancestor( ghost, elem->parent_slot, slot ) ) { if( elem->parent_slot > max_parent_root ) { max_parent_root = elem->parent_slot; max_idx = i; diff --git a/src/funk/fd_funk_val.h b/src/funk/fd_funk_val.h index da153c3c6b..3abd05c0e8 100644 --- a/src/funk/fd_funk_val.h +++ b/src/funk/fd_funk_val.h @@ -39,7 +39,7 @@ fd_funk_val_max( fd_funk_rec_t const * rec ) { /* Assumes pointer in caller's ad covers the case where rec has been marked ERASE). max 0 implies val NULL and vice versa. Assumes no concurrent operations on rec. */ -FD_FN_PURE static inline void * /* Lifetime is the lesser of rec or the value size is modified */ +FD_FN_PURE static inline void * /* Lifetime is the lesser of rec or the value size is modified */ fd_funk_val( fd_funk_rec_t const * rec, /* Assumes pointer in caller's address space to a live funk record */ fd_wksp_t const * wksp ) { /* ==fd_funk_wksp( funk ) where funk is a current local join */ ulong val_gaddr = rec->val_gaddr;