LCOV - code coverage report
Current view: top level - choreo/hfork - fd_hfork.c (source / functions) Hit Total Coverage
Test: cov.lcov Lines: 174 230 75.7 %
Date: 2026-02-13 06:06:24 Functions: 10 10 100.0 %

          Line data    Source code
       1             : #include "fd_hfork.h"
       2             : #include "fd_hfork_private.h"
       3             : 
       4             : static void
       5             : check( fd_hfork_t *  hfork,
       6             :        ulong         total_stake,
       7             :        candidate_t * candidate,
       8             :        int           dead,
       9           3 :        fd_hash_t *   our_bank_hash ) {
      10             : 
      11           3 :   if( FD_LIKELY( candidate->checked ) ) return; /* already checked this bank hash against our own */
      12           3 :   double pct = (double)candidate->stake * 100.0 / (double)total_stake;
      13           3 :   if( FD_LIKELY( pct < 52.0 ) ) return; /* not enough stake to compare */
      14             : 
      15           3 :   if( FD_UNLIKELY( dead ) ) {
      16           0 :     char msg[ 4096UL ];
      17           0 :     FD_BASE58_ENCODE_32_BYTES( candidate->key.block_id.uc, _block_id );
      18           0 :     FD_TEST( fd_cstr_printf_check( msg, sizeof( msg ), NULL,
      19           0 :                                   "HARD FORK DETECTED: our validator has marked slot %lu with block ID `%s` dead, but %lu validators with %.1f of stake have voted on it",
      20           0 :                                   candidate->slot,
      21           0 :                                   _block_id,
      22           0 :                                   candidate->cnt,
      23           0 :                                   pct ) );
      24             : 
      25           0 :     if( FD_UNLIKELY( hfork->fatal ) ) FD_LOG_ERR    (( "%s", msg ));
      26           0 :     else                              FD_LOG_WARNING(( "%s", msg ));
      27           3 :   } else if( FD_UNLIKELY( 0!=memcmp( our_bank_hash, &candidate->key.bank_hash, 32UL ) ) ) {
      28           0 :     char msg[ 4096UL ];
      29           0 :     FD_BASE58_ENCODE_32_BYTES( our_bank_hash->uc, _our_bank_hash );
      30           0 :     FD_BASE58_ENCODE_32_BYTES( candidate->key.block_id.uc, _block_id );
      31           0 :     FD_BASE58_ENCODE_32_BYTES( candidate->key.bank_hash.uc, _bank_hash );
      32           0 :     FD_TEST( fd_cstr_printf_check( msg, sizeof( msg ), NULL,
      33           0 :                                   "HARD FORK DETECTED: our validator has produced bank hash `%s` for slot %lu with block ID `%s`, but %lu validators with %.1f of stake have voted on a different bank hash `%s` for the same slot",
      34           0 :                                   _our_bank_hash,
      35           0 :                                   candidate->slot,
      36           0 :                                   _block_id,
      37           0 :                                   candidate->cnt,
      38           0 :                                   pct,
      39           0 :                                   _bank_hash ) );
      40             : 
      41           0 :     if( FD_UNLIKELY( hfork->fatal ) ) FD_LOG_ERR    (( "%s", msg ));
      42           0 :     else                              FD_LOG_WARNING(( "%s", msg ));
      43           0 :   }
      44           3 :   candidate->checked = 1;
      45           3 : }
      46             : 
      47             : ulong
      48          33 : fd_hfork_align( void ) {
      49          33 :   return 128UL;
      50          33 : }
      51             : 
      52             : ulong
      53             : fd_hfork_footprint( ulong max_live_slots,
      54           6 :                     ulong max_vote_accounts ) {
      55           6 :   ulong fork_max   = max_live_slots * max_vote_accounts;
      56           6 :   int   lg_blk_max = fd_ulong_find_msb( fd_ulong_pow2_up( fork_max ) ) + 1;
      57           6 :   int   lg_vtr_max = fd_ulong_find_msb( fd_ulong_pow2_up( max_vote_accounts ) ) + 1;
      58             : 
      59           6 :   ulong l = FD_LAYOUT_INIT;
      60           6 :   l = FD_LAYOUT_APPEND( l, alignof(fd_hfork_t),   sizeof(fd_hfork_t)                    );
      61           6 :   l = FD_LAYOUT_APPEND( l, blk_map_align(),       blk_map_footprint( lg_blk_max )       );
      62           6 :   l = FD_LAYOUT_APPEND( l, vtr_map_align(),       vtr_map_footprint( lg_vtr_max )       );
      63           6 :   l = FD_LAYOUT_APPEND( l, candidate_map_align(), candidate_map_footprint( lg_blk_max ) );
      64           6 :   l = FD_LAYOUT_APPEND( l, bank_hash_pool_align(), bank_hash_pool_footprint( fork_max ) );
      65          54 :   for( ulong i = 0UL; i < fd_ulong_pow2( lg_vtr_max ); i++ ) {
      66          48 :     l = FD_LAYOUT_APPEND( l, votes_align(), votes_footprint( max_live_slots ) );
      67          48 :   }
      68           6 :   return FD_LAYOUT_FINI( l, fd_hfork_align() );
      69           6 : }
      70             : 
      71             : void *
      72             : fd_hfork_new( void * shmem,
      73             :               ulong  max_live_slots,
      74             :               ulong  max_vote_accounts,
      75             :               ulong  seed,
      76           3 :               int    fatal ) {
      77           3 :   (void)seed; /* TODO map seed */
      78             : 
      79           3 :   if( FD_UNLIKELY( !shmem ) ) {
      80           0 :     FD_LOG_WARNING(( "NULL mem" ));
      81           0 :     return NULL;
      82           0 :   }
      83             : 
      84           3 :   if( FD_UNLIKELY( !fd_ulong_is_aligned( (ulong)shmem, fd_hfork_align() ) ) ) {
      85           0 :     FD_LOG_WARNING(( "misaligned mem" ));
      86           0 :     return NULL;
      87           0 :   }
      88             : 
      89           3 :   ulong footprint = fd_hfork_footprint( max_live_slots, max_vote_accounts );
      90           3 :   if( FD_UNLIKELY( !footprint ) ) {
      91           0 :     FD_LOG_WARNING(( "bad max_live_slots (%lu) or max_vote_accounts (%lu)", max_live_slots, max_vote_accounts ));
      92           0 :     return NULL;
      93           0 :   }
      94             : 
      95           3 :   fd_memset( shmem, 0, footprint );
      96             : 
      97           3 :   ulong fork_max   = max_live_slots * max_vote_accounts;
      98           3 :   int   lg_blk_max = fd_ulong_find_msb( fd_ulong_pow2_up( fork_max ) ) + 1;
      99           3 :   int   lg_vtr_max = fd_ulong_find_msb( fd_ulong_pow2_up( max_vote_accounts ) ) + 1;
     100             : 
     101           3 :   FD_SCRATCH_ALLOC_INIT( l, shmem );
     102           3 :   fd_hfork_t * hfork          = FD_SCRATCH_ALLOC_APPEND( l, fd_hfork_align(),       sizeof( fd_hfork_t )                        );
     103           3 :   void *       blk_map        = FD_SCRATCH_ALLOC_APPEND( l, blk_map_align(),        blk_map_footprint( lg_blk_max )             );
     104           3 :   void *       vtr_map        = FD_SCRATCH_ALLOC_APPEND( l, vtr_map_align(),        vtr_map_footprint( lg_vtr_max )             );
     105           3 :   void *       candidate_map  = FD_SCRATCH_ALLOC_APPEND( l, candidate_map_align(),  candidate_map_footprint( lg_blk_max )       );
     106           3 :   void *       bank_hash_pool = FD_SCRATCH_ALLOC_APPEND( l, bank_hash_pool_align(), bank_hash_pool_footprint( fork_max )        );
     107             : 
     108           0 :   hfork->blk_map        = blk_map_new( blk_map, lg_blk_max, 0UL );             /* FIXME seed */
     109           3 :   hfork->vtr_map        = vtr_map_new( vtr_map, lg_vtr_max, 0UL );             /* FIXME seed */
     110           3 :   hfork->candidate_map  = candidate_map_new( candidate_map, lg_blk_max, 0UL ); /* FIXME seed */
     111           3 :   hfork->bank_hash_pool = bank_hash_pool_new( bank_hash_pool, fork_max );
     112             : 
     113           3 :   vtr_t * vtr_map_ = vtr_map_join( hfork->vtr_map );
     114          27 :   for( ulong i = 0UL; i < fd_ulong_pow2( lg_vtr_max ); i++ ) {
     115          24 :     void * votes      = FD_SCRATCH_ALLOC_APPEND( l, votes_align(), votes_footprint( max_live_slots ) );
     116           0 :     vtr_map_[i].votes = votes_new( votes, max_live_slots );
     117          24 :   }
     118           3 :   FD_TEST( FD_SCRATCH_ALLOC_FINI( l, fd_hfork_align() ) == (ulong)shmem + footprint );
     119           3 :   hfork->fatal = fatal;
     120           3 :   return shmem;
     121           3 : }
     122             : 
     123             : fd_hfork_t *
     124           3 : fd_hfork_join( void * shhfork ) {
     125           3 :   fd_hfork_t * hfork = (fd_hfork_t *)shhfork;
     126             : 
     127           3 :   if( FD_UNLIKELY( !hfork ) ) {
     128           0 :     FD_LOG_WARNING(( "NULL hfork" ));
     129           0 :     return NULL;
     130           0 :   }
     131             : 
     132           3 :   if( FD_UNLIKELY( !fd_ulong_is_aligned((ulong)hfork, fd_hfork_align() ) ) ) {
     133           0 :     FD_LOG_WARNING(( "misaligned hfork" ));
     134           0 :     return NULL;
     135           0 :   }
     136             : 
     137           3 :   hfork->blk_map        = blk_map_join( hfork->blk_map );
     138           3 :   hfork->vtr_map        = vtr_map_join( hfork->vtr_map );
     139           3 :   hfork->candidate_map  = candidate_map_join( hfork->candidate_map );
     140           3 :   hfork->bank_hash_pool = bank_hash_pool_join( hfork->bank_hash_pool );
     141          27 :   for( ulong i = 0UL; i < vtr_map_slot_cnt( hfork->vtr_map ); i++ ) {
     142          24 :     hfork->vtr_map[i].votes = votes_join( hfork->vtr_map[i].votes );
     143          24 :   }
     144             : 
     145           3 :   return hfork;
     146           3 : }
     147             : 
     148             : void *
     149           3 : fd_hfork_leave( fd_hfork_t const * hfork ) {
     150             : 
     151           3 :   if( FD_UNLIKELY( !hfork ) ) {
     152           0 :     FD_LOG_WARNING(( "NULL hfork" ));
     153           0 :     return NULL;
     154           0 :   }
     155             : 
     156           3 :   return (void *)hfork;
     157           3 : }
     158             : 
     159             : void *
     160           3 : fd_hfork_delete( void * hfork ) {
     161             : 
     162           3 :   if( FD_UNLIKELY( !hfork ) ) {
     163           0 :     FD_LOG_WARNING(( "NULL hfork" ));
     164           0 :     return NULL;
     165           0 :   }
     166             : 
     167           3 :   if( FD_UNLIKELY( !fd_ulong_is_aligned((ulong)hfork, fd_hfork_align() ) ) ) {
     168           0 :     FD_LOG_WARNING(( "misaligned hfork" ));
     169           0 :     return NULL;
     170           0 :   }
     171             : 
     172           3 :   return hfork;
     173           3 : }
     174             : 
     175             : void
     176          21 : remove( blk_t * blk, fd_hash_t * bank_hash, bank_hash_t * pool ) {
     177          21 :   bank_hash_t * prev = NULL;
     178          21 :   bank_hash_t * curr = blk->bank_hashes;
     179          27 :   while( FD_LIKELY( curr ) ) {
     180          27 :     if( FD_LIKELY( 0==memcmp( &curr->bank_hash, bank_hash, 32UL ) ) ) break;
     181           6 :     prev = curr;
     182           6 :     curr = bank_hash_pool_ele( pool, curr->next );
     183           6 :   }
     184          21 :   FD_TEST( curr ); /* assumes bank_hash in blk->bank_hashes */
     185             : 
     186             :   /* In most cases, there is only one bank_hash per blk, so it will be
     187             :      the first element in blk->bank_hashes and prev will be NULL. */
     188             : 
     189          21 :   if( FD_LIKELY( !prev ) ) blk->bank_hashes = bank_hash_pool_ele( pool, curr->next );
     190           6 :   else                     prev->next       = curr->next;
     191          21 :   bank_hash_pool_ele_release( pool, curr );
     192          21 : }
     193             : 
     194             : void
     195             : fd_hfork_count_vote( fd_hfork_t *         hfork,
     196             :                      fd_hfork_metrics_t * metrics,
     197             :                      fd_hash_t const *    vote_acc,
     198             :                      fd_hash_t const *    block_id,
     199             :                      fd_hash_t const *    bank_hash,
     200             :                      ulong                slot,
     201             :                      ulong                stake,
     202          48 :                      ulong                total_stake ) {
     203             : 
     204             :   /* Get the vtr. */
     205             : 
     206          48 :   vtr_t * vtr = vtr_map_query( hfork->vtr_map, *vote_acc, NULL );
     207          48 :   if( FD_UNLIKELY( !vtr ) ) {
     208          12 :     FD_TEST( vtr_map_key_cnt( hfork->vtr_map ) < vtr_map_key_max( hfork->vtr_map ) );
     209          12 :     vtr = vtr_map_insert( hfork->vtr_map, *vote_acc );
     210          12 :   }
     211             : 
     212             :   /* Only process newer votes (by vote slot) from a given voter. */
     213             : 
     214          48 :   if( FD_UNLIKELY( !votes_empty( vtr->votes ) && votes_peek_tail_const( vtr->votes )->slot >= slot ) ) return;
     215             : 
     216             :   /* Evict the candidate's oldest vote (by vote slot). */
     217             : 
     218          48 :   if( FD_UNLIKELY( votes_full( vtr->votes ) ) ) {
     219          24 :     vote_t          vote      = votes_pop_head( vtr->votes );
     220          24 :     candidate_key_t key       = { .block_id = vote.block_id, .bank_hash = vote.bank_hash };
     221          24 :     candidate_t *   candidate = candidate_map_query( hfork->candidate_map, key, NULL );
     222          24 :     candidate->stake -= vote.stake;
     223          24 :     candidate->cnt--;
     224          24 :     if( FD_UNLIKELY( candidate->cnt==0 ) ) {
     225          21 :       candidate_map_remove( hfork->candidate_map, candidate );
     226          21 :       blk_t * blk = blk_map_query( hfork->blk_map, vote.block_id, NULL );
     227          21 :       FD_TEST( blk ); /* asumes if this is in candidate_map, it must also be in blk_map */
     228          21 :       remove( blk, &vote.bank_hash, hfork->bank_hash_pool );
     229          21 :       if( FD_UNLIKELY( !blk->bank_hashes ) ) {
     230          12 :         blk_map_remove( hfork->blk_map, blk );
     231          12 :         if( FD_UNLIKELY( blk->forked ) ) {
     232           9 :           metrics->active--;
     233           9 :           metrics->pruned++;
     234           9 :         }
     235          12 :       }
     236          21 :     }
     237          24 :   }
     238             : 
     239             :   /* Push the vote onto the vtr. */
     240             : 
     241          48 :   vote_t vote = { .block_id = *block_id, .bank_hash = *bank_hash, .slot = slot, .stake = stake };
     242          48 :   vtr->votes  = votes_push_tail( vtr->votes, vote );
     243             : 
     244             :   /* Update the hard fork candidate for this block id. */
     245             : 
     246          48 :   candidate_key_t key       = { .block_id = *block_id, .bank_hash = *bank_hash };
     247          48 :   candidate_t *   candidate = candidate_map_query( hfork->candidate_map, key, NULL );
     248          48 :   if( FD_UNLIKELY( !candidate ) ) {
     249          27 :     candidate        = candidate_map_insert( hfork->candidate_map, key );
     250          27 :     candidate->slot  = slot;
     251          27 :     candidate->stake = 0UL;
     252          27 :     candidate->cnt   = 0UL;
     253          27 :   }
     254          48 :   candidate->cnt++;
     255          48 :   candidate->stake += stake;
     256             : 
     257             :   /* Update the list of bank hashes for this block_id. */
     258             : 
     259          48 :   blk_t * blk = blk_map_query( hfork->blk_map, *block_id, NULL );
     260          48 :   if( FD_UNLIKELY( !blk ) ) {
     261          18 :     FD_TEST( blk_map_key_cnt( hfork->blk_map ) < blk_map_key_max( hfork->blk_map ) ); /* invariant violation: blk_map full */
     262          18 :     blk              = blk_map_insert( hfork->blk_map, *block_id );
     263          18 :     blk->bank_hashes = NULL;
     264          18 :     blk->replayed    = 0;
     265          18 :     blk->dead        = 0;
     266          18 :   }
     267          48 :   int           found = 0;
     268          48 :   ulong         cnt   = 0;
     269          48 :   bank_hash_t * prev  = NULL;
     270          48 :   bank_hash_t * curr  = blk->bank_hashes;
     271          87 :   while( FD_LIKELY( curr ) ) {
     272          39 :     if( FD_LIKELY( 0==memcmp( curr, bank_hash, 32UL ) ) ) found = 1;
     273          39 :     prev = curr;
     274          39 :     curr = bank_hash_pool_ele( hfork->bank_hash_pool, curr->next );
     275          39 :     cnt++;
     276          39 :   }
     277          48 :   if( FD_UNLIKELY( !found ) ) {
     278          27 :     FD_TEST( bank_hash_pool_free( hfork->bank_hash_pool ) );
     279          27 :     bank_hash_t * ele = bank_hash_pool_ele_acquire( hfork->bank_hash_pool );
     280          27 :     ele->bank_hash    = *bank_hash;
     281          27 :     ele->next         = bank_hash_pool_idx_null( hfork->bank_hash_pool );
     282          27 :     if( FD_LIKELY( !prev ) ) blk->bank_hashes = ele;
     283           9 :     else {
     284           9 :       prev->next  = bank_hash_pool_idx( hfork->bank_hash_pool, ele );
     285           9 :       blk->forked = 1;
     286           9 :       metrics->seen++;
     287           9 :       metrics->active++;
     288           9 :     }
     289          27 :     cnt++;
     290          27 :   }
     291          48 :   metrics->max_width = fd_ulong_max( metrics->max_width, cnt );
     292             : 
     293             :   /* Check for hard forks. */
     294             : 
     295          48 :   if( FD_LIKELY( blk->replayed ) ) check( hfork, total_stake, candidate, blk->dead, &blk->our_bank_hash );
     296          48 : }
     297             : 
     298             : void
     299             : fd_hfork_record_our_bank_hash( fd_hfork_t * hfork,
     300             :                                fd_hash_t  * block_id,
     301             :                                fd_hash_t  * bank_hash,
     302           3 :                                ulong        total_stake ) {
     303           3 :   blk_t * blk = blk_map_query( hfork->blk_map, *block_id, NULL );
     304           3 :   if( FD_UNLIKELY( !blk ) ) {
     305           0 :     blk              = blk_map_insert( hfork->blk_map, *block_id );
     306           0 :     blk->replayed    = 1;
     307           0 :     blk->bank_hashes = NULL;
     308           0 :   }
     309           3 :   if( FD_LIKELY( bank_hash ) ) { blk->dead = 0; blk->our_bank_hash = *bank_hash; }
     310           0 :   else                           blk->dead = 1;
     311             : 
     312           3 :   bank_hash_t * curr  = blk->bank_hashes;
     313           6 :   while( FD_LIKELY( curr ) ) {
     314           3 :     candidate_key_t key       = { .block_id = *block_id, .bank_hash = curr->bank_hash };
     315           3 :     candidate_t *   candidate = candidate_map_query( hfork->candidate_map, key, NULL );
     316           3 :     if( FD_LIKELY( candidate ) ) check( hfork, total_stake, candidate, blk->dead, &blk->our_bank_hash );
     317           3 :     curr = bank_hash_pool_ele( hfork->bank_hash_pool, curr->next );
     318           3 :   }
     319           3 : }

Generated by: LCOV version 1.14