LCOV - code coverage report
Current view: top level - disco/shred - fd_stake_ci.c (source / functions) Hit Total Coverage
Test: cov.lcov Lines: 185 193 95.9 %
Date: 2025-03-20 12:08:36 Functions: 14 14 100.0 %

          Line data    Source code
       1             : #include "fd_stake_ci.h"
       2             : #include "../../util/net/fd_ip4.h" /* Just for debug */
       3             : 
       4             : #define SORT_NAME sort_pubkey
       5     3472326 : #define SORT_KEY_T fd_shred_dest_weighted_t
       6     5510922 : #define SORT_BEFORE(a,b) (memcmp( (a).pubkey.uc, (b).pubkey.uc, 32UL )<0)
       7             : #include "../../util/tmpl/fd_sort.c"
       8             : 
       9             : /* We don't have or need real contact info for the local validator, but
      10             :    we want to be able to distinguish it from staked nodes with no
      11             :    contact info. */
      12         147 : #define SELF_DUMMY_IP 1U
      13             : 
      14             : void *
      15             : fd_stake_ci_new( void             * mem,
      16          36 :                 fd_pubkey_t const * identity_key ) {
      17          36 :   fd_stake_ci_t * info = (fd_stake_ci_t *)mem;
      18             : 
      19          36 :   fd_stake_weight_t dummy_stakes[ 1 ] = {{ .key = {{0}}, .stake = 1UL }};
      20          36 :   fd_shred_dest_weighted_t dummy_dests[ 1 ] = {{ .pubkey = *identity_key, .ip4 = SELF_DUMMY_IP }};
      21             : 
      22             :   /* Initialize first 2 to satisfy invariants */
      23          36 :   info->stake_weight[ 0 ] = dummy_stakes[ 0 ];
      24          36 :   info->shred_dest  [ 0 ] = dummy_dests [ 0 ];
      25         108 :   for( ulong i=0UL; i<2UL; i++ ) {
      26          72 :     fd_per_epoch_info_t * ei = info->epoch_info + i;
      27          72 :     ei->epoch          = i;
      28          72 :     ei->start_slot     = 0UL;
      29          72 :     ei->slot_cnt       = 0UL;
      30          72 :     ei->excluded_stake = 0UL;
      31             : 
      32          72 :     ei->lsched = fd_epoch_leaders_join( fd_epoch_leaders_new( ei->_lsched, 0UL, 0UL, 1UL, 1UL,    info->stake_weight,       0UL ) );
      33          72 :     ei->sdest  = fd_shred_dest_join   ( fd_shred_dest_new   ( ei->_sdest,  info->shred_dest, 1UL, ei->lsched, identity_key, 0UL ) );
      34          72 :   }
      35          36 :   info->identity_key[ 0 ] = *identity_key;
      36             : 
      37          36 :   return (void *)info;
      38          36 : }
      39             : 
      40          36 : fd_stake_ci_t * fd_stake_ci_join( void * mem ) { return (fd_stake_ci_t *)mem; }
      41             : 
      42          33 : void * fd_stake_ci_leave ( fd_stake_ci_t * info ) { return (void *)info; }
      43          33 : void * fd_stake_ci_delete( void          * mem  ) { return mem;          }
      44             : 
      45             : 
      46             : void
      47             : fd_stake_ci_stake_msg_init( fd_stake_ci_t * info,
      48          93 :                             uchar const   * new_message ) {
      49          93 :   ulong const * hdr = fd_type_pun_const( new_message );
      50             : 
      51          93 :   ulong epoch               = hdr[ 0 ];
      52          93 :   ulong staked_cnt          = hdr[ 1 ];
      53          93 :   ulong start_slot          = hdr[ 2 ];
      54          93 :   ulong slot_cnt            = hdr[ 3 ];
      55          93 :   ulong excluded_stake      = hdr[ 4 ];
      56             : 
      57          93 :   if( FD_UNLIKELY( staked_cnt > MAX_SHRED_DESTS ) )
      58           0 :     FD_LOG_ERR(( "The stakes -> Firedancer splice sent a malformed update with %lu stakes in it,"
      59          93 :                  " but the maximum allowed is %lu", staked_cnt, MAX_SHRED_DESTS ));
      60             : 
      61          93 :   info->scratch->epoch          = epoch;
      62          93 :   info->scratch->start_slot     = start_slot;
      63          93 :   info->scratch->slot_cnt       = slot_cnt;
      64          93 :   info->scratch->staked_cnt     = staked_cnt;
      65          93 :   info->scratch->excluded_stake = excluded_stake;
      66             : 
      67          93 :   fd_memcpy( info->stake_weight, hdr+5UL, sizeof(fd_stake_weight_t)*staked_cnt );
      68          93 : }
      69             : 
      70             : static inline void
      71         177 : log_summary( char const * msg, fd_stake_ci_t * info ) {
      72             : #if 0
      73             :   fd_per_epoch_info_t const * ei = info->epoch_info;
      74             :   FD_LOG_NOTICE(( "Dumping stake contact information because %s", msg ));
      75             :   for( ulong i=0UL; i<2UL; i++ ) {
      76             :     FD_LOG_NOTICE(( "  Dumping shred destination details for epoch %lu, slots [%lu, %lu)", ei[i].epoch, ei[i].start_slot, ei[i].start_slot+ei[i].slot_cnt ));
      77             :     fd_shred_dest_t * sdest = ei[i].sdest;
      78             :     for( fd_shred_dest_idx_t j=0; j<(fd_shred_dest_idx_t)fd_shred_dest_cnt_all( sdest ); j++ ) {
      79             :       fd_shred_dest_weighted_t * dest = fd_shred_dest_idx_to_dest( sdest, j );
      80             :       FD_LOG_NOTICE(( "    %16lx  %20lu " FD_IP4_ADDR_FMT " %hu ", *(ulong *)dest->pubkey.uc, dest->stake_lamports, FD_IP4_ADDR_FMT_ARGS( dest->ip4 ), dest->port ));
      81             :     }
      82             :   }
      83             : #else
      84         177 :   (void)msg;
      85         177 :   (void)info;
      86         177 : #endif
      87         177 : }
      88             : 
      89             : #define SET_NAME unhit_set
      90           0 : #define SET_MAX  MAX_SHRED_DESTS
      91             : #include "../../util/tmpl/fd_set.c"
      92             : 
      93             : void
      94          90 : fd_stake_ci_stake_msg_fini( fd_stake_ci_t * info ) {
      95             :   /* The grossness here is a sign our abstractions are wrong and need to
      96             :      be fixed instead of just patched.  We need to generate weighted
      97             :      shred destinations using a combination of the new stake information
      98             :      and whatever contact info we previously knew. */
      99          90 :   ulong epoch                  = info->scratch->epoch;
     100          90 :   ulong staked_cnt             = info->scratch->staked_cnt;
     101             : 
     102             :   /* Just take the first one arbitrarily because they both have the same
     103             :      contact info, other than possibly some staked nodes with no contact
     104             :      info. */
     105          90 :   fd_shred_dest_t * existing_sdest    = info->epoch_info->sdest;
     106          90 :   ulong             existing_dest_cnt = fd_shred_dest_cnt_all( existing_sdest );
     107             : 
     108             :   /* Keep track of the destinations in existing_sdest that are not
     109             :      staked in this new epoch, i.e. the ones we don't hit in the loop
     110             :      below. */
     111          90 :   unhit_set_t _unhit[ unhit_set_word_cnt ];
     112             :   /* This memsets to 0, right before we memset to 1, and is probably
     113             :      unnecessary, but using it without joining seems like a hack. */
     114          90 :   unhit_set_t * unhit = unhit_set_join( unhit_set_new( _unhit ) );
     115          90 :   unhit_set_full( unhit );
     116             : 
     117      482715 :   for( ulong i=0UL; i<staked_cnt; i++ ) {
     118      482625 :     fd_shred_dest_idx_t old_idx = fd_shred_dest_pubkey_to_idx( existing_sdest, &(info->stake_weight[ i ].key) );
     119      482625 :     fd_shred_dest_weighted_t * in_prev = fd_shred_dest_idx_to_dest( existing_sdest, old_idx );
     120      482625 :     info->shred_dest[ i ] = *in_prev;
     121      482625 :     if( FD_UNLIKELY( old_idx==FD_SHRED_DEST_NO_DEST ) ) {
     122             :       /* We got the generic empty entry, so fixup the pubkey */
     123      120711 :       info->shred_dest[ i ].pubkey = info->stake_weight[ i ].key;
     124      361914 :     } else {
     125      361914 :       unhit_set_remove( unhit, old_idx );
     126      361914 :     }
     127      482625 :     info->shred_dest[ i ].stake_lamports = info->stake_weight[ i ].stake;
     128      482625 :   }
     129             : 
     130          90 :   int any_destaked = 0;
     131          90 :   ulong j = staked_cnt;
     132         279 :   for( ulong idx=unhit_set_iter_init( unhit ); (idx<existing_dest_cnt) & (!unhit_set_iter_done( idx )) & (j<MAX_SHRED_DESTS);
     133         189 :              idx=unhit_set_iter_next( unhit, idx ) ) {
     134         189 :     fd_shred_dest_weighted_t * in_prev = fd_shred_dest_idx_to_dest( existing_sdest, (fd_shred_dest_idx_t)idx );
     135         189 :     if( FD_LIKELY( in_prev->ip4 ) ) {
     136         147 :       info->shred_dest[ j ] = *in_prev;
     137         147 :       any_destaked |= (in_prev->stake_lamports > 0UL);
     138         147 :       info->shred_dest[ j ].stake_lamports = 0UL;
     139         147 :       j++;
     140         147 :     }
     141         189 :   }
     142             : 
     143          90 :   unhit_set_delete( unhit_set_leave( unhit ) );
     144             : 
     145          90 :   if( FD_UNLIKELY( any_destaked ) ) {
     146             :     /* The unstaked list might be a little out of order because the
     147             :        destinations that were previously staked will be at the start of
     148             :        the unstaked list, sorted by their previous stake, instead of
     149             :        where they should be.  If there weren't any destaked, then the
     150             :        only unstaked nodes come from the previous list, which we know
     151             :        was in order, perhaps skipping some, which doesn't ruin the
     152             :        order. */
     153          18 :     sort_pubkey_inplace( info->shred_dest + staked_cnt, j - staked_cnt );
     154          18 :   }
     155             : 
     156             :   /* Now we have a plausible shred_dest list. */
     157             : 
     158             :   /* Clear the existing info */
     159          90 :   fd_per_epoch_info_t * new_ei = info->epoch_info + (epoch % 2UL);
     160          90 :   fd_shred_dest_delete   ( fd_shred_dest_leave   ( new_ei->sdest  ) );
     161          90 :   fd_epoch_leaders_delete( fd_epoch_leaders_leave( new_ei->lsched ) );
     162             : 
     163             :   /* And create the new one */
     164          90 :   ulong excluded_stake = info->scratch->excluded_stake;
     165             : 
     166          90 :   new_ei->epoch          = epoch;
     167          90 :   new_ei->start_slot     = info->scratch->start_slot;
     168          90 :   new_ei->slot_cnt       = info->scratch->slot_cnt;
     169          90 :   new_ei->excluded_stake = excluded_stake;
     170             : 
     171          90 :   new_ei->lsched = fd_epoch_leaders_join( fd_epoch_leaders_new( new_ei->_lsched, epoch, new_ei->start_slot, new_ei->slot_cnt,
     172          90 :                                                                 staked_cnt, info->stake_weight,     excluded_stake ) );
     173          90 :   new_ei->sdest  = fd_shred_dest_join   ( fd_shred_dest_new   ( new_ei->_sdest, info->shred_dest, j,
     174          90 :                                                                 new_ei->lsched, info->identity_key, excluded_stake ) );
     175          90 :   log_summary( "stake update", info );
     176          90 : }
     177             : 
     178          90 : fd_shred_dest_weighted_t * fd_stake_ci_dest_add_init( fd_stake_ci_t * info ) { return info->shred_dest; }
     179             : 
     180             : static inline void
     181             : fd_stake_ci_dest_add_fini_impl( fd_stake_ci_t       * info,
     182             :                                 ulong                 cnt,
     183         174 :                                 fd_per_epoch_info_t * ei ) {
     184             :   /* Initially we start with one list containing S+U staked and unstaked
     185             :      destinations jumbled together.  In order to update sdest, we need
     186             :      to convert the list to S' staked destinations (taken from the
     187             :      existing sdest, though possibly updated) followed by U unstaked
     188             :      destinations.
     189             : 
     190             :      It's possible to do this in place, but at a cost of additional
     191             :      complexity (similar to memcpy vs memmove).  Rather than do that, we
     192             :      build the combined list in shred_dest_temp. */
     193             : 
     194         174 :   ulong found_unstaked_cnt = 0UL;
     195         174 :   int   any_new_unstaked   = 0;
     196             : 
     197         174 :   ulong const staked_cnt = fd_shred_dest_cnt_staked( ei->sdest );
     198         174 :   ulong j = staked_cnt;
     199             : 
     200     3859698 :   for( ulong i=0UL; i<cnt; i++ ) {
     201     3859524 :     fd_shred_dest_idx_t idx = fd_shred_dest_pubkey_to_idx( ei->sdest, &(info->shred_dest[ i ].pubkey) );
     202     3859524 :     fd_shred_dest_weighted_t * dest = fd_shred_dest_idx_to_dest( ei->sdest, idx );
     203     3859524 :     if( FD_UNLIKELY( (dest->stake_lamports==0UL)&(j<MAX_SHRED_DESTS) ) ) {
     204             :       /* Copy this destination to the unstaked part of the new list.
     205             :          This also handles the new unstaked case */
     206      482760 :       info->shred_dest_temp[ j ] = info->shred_dest[ i ];
     207      482760 :       info->shred_dest_temp[ j ].stake_lamports = 0UL;
     208      482760 :       j++;
     209      482760 :     }
     210             : 
     211     3859524 :     if( FD_LIKELY( idx!=FD_SHRED_DEST_NO_DEST ) ) {
     212     3738693 :       dest->ip4  = info->shred_dest[ i ].ip4;
     213     3738693 :       dest->port = info->shred_dest[ i ].port;
     214     3738693 :     }
     215             : 
     216     3859524 :     any_new_unstaked   |= (idx==FD_SHRED_DEST_NO_DEST);
     217     3859524 :     found_unstaked_cnt += (ulong)((idx!=FD_SHRED_DEST_NO_DEST) & (dest->stake_lamports==0UL));
     218     3859524 :   }
     219             : 
     220         174 :   if( FD_LIKELY( !any_new_unstaked && found_unstaked_cnt==fd_shred_dest_cnt_unstaked( ei->sdest ) ) ) {
     221             :     /* Because any_new_unstaked==0, the set of unstaked nodes in this
     222             :        update is fully contained in the set of unstaked nodes in the
     223             :        sdest.  Then additionally, because the sets are the same size,
     224             :        they must actually be equal.  In this case, we've already updated
     225             :        the existing shred_dest_weighted with the newest contact info we
     226             :        have, so there's nothing else to do. */
     227          48 :     return;
     228          48 :   }
     229             : 
     230             :   /* Otherwise something more significant changed and we need to
     231             :      regenerate the sdest.  At this point, elements [staked_cnt, j) now
     232             :      contain all the current unstaked destinations. */
     233             : 
     234             :   /* Copy staked nodes to [0, staked_cnt). We've already applied the
     235             :      updated contact info to these. */
     236     1929831 :   for( ulong i=0UL; i<staked_cnt; i++ )
     237     1929705 :     info->shred_dest_temp[ i ] = *fd_shred_dest_idx_to_dest( ei->sdest, (fd_shred_dest_idx_t)i );
     238             : 
     239             :   /* The staked nodes are sorted properly because we use the index from
     240             :      sdest.  We need to sort the unstaked nodes by pubkey though. */
     241         126 :   sort_pubkey_inplace( info->shred_dest_temp + staked_cnt, j - staked_cnt );
     242             : 
     243         126 :   fd_shred_dest_delete( fd_shred_dest_leave( ei->sdest ) );
     244             : 
     245         126 :   ei->sdest  = fd_shred_dest_join( fd_shred_dest_new( ei->_sdest, info->shred_dest_temp, j, ei->lsched, info->identity_key,
     246         126 :                                                       ei->excluded_stake ) );
     247             : 
     248         126 :   if( FD_UNLIKELY( ei->sdest==NULL ) ) {
     249             :     /* Happens if the identity key is not present, which can only happen
     250             :        if the current validator's stake is not in the top 40,200.  We
     251             :        could initialize ei->sdest to a dummy value, but having the wrong
     252             :        stake weights could lead to potentially slashable issues
     253             :        elsewhere (e.g. we might product a block when we're not actually
     254             :        leader).  We're just going to terminate in this case. */
     255           0 :     FD_LOG_ERR(( "Too many validators have higher stake than this validator.  Cannot continue." ));
     256           0 :   }
     257         126 : }
     258             : 
     259             : 
     260             : void
     261             : fd_stake_ci_dest_add_fini( fd_stake_ci_t * info,
     262          87 :                            ulong           cnt ) {
     263             :   /* The Rust side uses tvu_peers which typically excludes the local
     264             :      validator.  In some cases, after a set-identity, it might still
     265             :      include the local validator though.  If it doesn't include it, we
     266             :      need to add the local validator back. */
     267          87 :   FD_TEST( cnt<MAX_SHRED_DESTS );
     268          87 :   ulong i=0UL;
     269     1929762 :   for(; i<cnt; i++ ) if( FD_UNLIKELY( 0==memcmp( info->shred_dest[ i ].pubkey.uc, info->identity_key, 32UL ) ) ) break;
     270             : 
     271          87 :   if( FD_LIKELY( i==cnt ) ) {
     272          87 :     fd_shred_dest_weighted_t self_dests = { .pubkey = info->identity_key[ 0 ], .ip4 = SELF_DUMMY_IP };
     273          87 :     info->shred_dest[ cnt++ ] = self_dests;
     274          87 :   } else {
     275           0 :     info->shred_dest[ i ].ip4 = SELF_DUMMY_IP;
     276           0 :   }
     277             : 
     278             :   /* Update both of them */
     279          87 :   fd_stake_ci_dest_add_fini_impl( info, cnt, info->epoch_info + 0UL );
     280          87 :   fd_stake_ci_dest_add_fini_impl( info, cnt, info->epoch_info + 1UL );
     281             : 
     282          87 :   log_summary( "dest update", info );
     283          87 : }
     284             : 
     285             : 
     286             : /* Returns a value in [0, 2) if found, and ULONG_MAX if not */
     287             : static inline ulong
     288             : fd_stake_ci_get_idx_for_slot( fd_stake_ci_t const * info,
     289        1179 :                               ulong                 slot ) {
     290        1179 :   fd_per_epoch_info_t const * ei = info->epoch_info;
     291        1179 :   ulong idx = ULONG_MAX;
     292        3537 :   for( ulong i=0UL; i<2UL; i++ ) idx = fd_ulong_if( (ei[i].start_slot<=slot) & (slot-ei[i].start_slot<ei[i].slot_cnt), i, idx );
     293        1179 :   return idx;
     294        1179 : }
     295             : 
     296             : 
     297             : void
     298             : fd_stake_ci_set_identity( fd_stake_ci_t *     info,
     299          12 :                           fd_pubkey_t const * identity_key ) {
     300             :   /* None of the stakes are changing, so we just need to regenerate the
     301             :      sdests, sightly adjusting the destination IP addresses.  The only
     302             :      corner case is if the new identity is not present. */
     303          36 :   for( ulong i=0UL; i<2UL; i++ ) {
     304          24 :     fd_per_epoch_info_t * ei = info->epoch_info+i;
     305             : 
     306          24 :     fd_shred_dest_idx_t old_idx = fd_shred_dest_pubkey_to_idx( ei->sdest, info->identity_key );
     307          24 :     fd_shred_dest_idx_t new_idx = fd_shred_dest_pubkey_to_idx( ei->sdest, identity_key       );
     308             : 
     309          24 :     FD_TEST( old_idx!=FD_SHRED_DEST_NO_DEST );
     310             : 
     311          24 :     if( FD_LIKELY( new_idx!=FD_SHRED_DEST_NO_DEST ) ) {
     312          18 :       fd_shred_dest_idx_to_dest( ei->sdest, old_idx )->ip4 = 0U;
     313          18 :       fd_shred_dest_idx_to_dest( ei->sdest, new_idx )->ip4 = SELF_DUMMY_IP;
     314             : 
     315          18 :       fd_shred_dest_update_source( ei->sdest, new_idx );
     316          18 :     } else {
     317           6 :       ulong staked_cnt   = fd_shred_dest_cnt_staked  ( ei->sdest );
     318           6 :       ulong unstaked_cnt = fd_shred_dest_cnt_unstaked( ei->sdest );
     319           6 :       if( FD_UNLIKELY( staked_cnt+unstaked_cnt==MAX_SHRED_DESTS ) ) {
     320           0 :         FD_LOG_ERR(( "too many validators in shred table to add a new validator with set-identity" ));
     321           0 :       }
     322             :       /* We'll add identity_key as a new unstaked validator.  First copy
     323             :          all the staked ones, then place the new validator in the spot
     324             :          where it belongs according to lexicographic order. */
     325           6 :       ulong j=0UL;
     326          24 :       for(; j<staked_cnt; j++ ) info->shred_dest_temp[ j ] = *fd_shred_dest_idx_to_dest( ei->sdest, (fd_shred_dest_idx_t)j );
     327          33 :       for(; j<staked_cnt+unstaked_cnt; j++ ) {
     328          33 :         fd_shred_dest_weighted_t * wj = fd_shred_dest_idx_to_dest( ei->sdest, (fd_shred_dest_idx_t)j );
     329          33 :         if( FD_UNLIKELY( (memcmp( wj->pubkey.uc, identity_key->uc, 32UL )>=0) ) ) break;
     330          27 :         info->shred_dest_temp[ j ] = *wj;
     331          27 :       }
     332             : 
     333           6 :       info->shred_dest_temp[ j ].pubkey         = *identity_key;
     334           6 :       info->shred_dest_temp[ j ].stake_lamports = 0UL;
     335           6 :       info->shred_dest_temp[ j ].ip4            = SELF_DUMMY_IP;
     336             : 
     337          12 :       for(; j<staked_cnt+unstaked_cnt; j++ ) info->shred_dest_temp[ j+1UL ] = *fd_shred_dest_idx_to_dest( ei->sdest, (fd_shred_dest_idx_t)j );
     338             : 
     339           6 :       fd_shred_dest_delete( fd_shred_dest_leave( ei->sdest ) );
     340             : 
     341           6 :       ei->sdest  = fd_shred_dest_join( fd_shred_dest_new( ei->_sdest, info->shred_dest_temp, j+1UL, ei->lsched, identity_key,
     342           6 :                                                           ei->excluded_stake ) );
     343           6 :       FD_TEST( ei->sdest );
     344           6 :     }
     345             : 
     346          24 :   }
     347          12 :   *info->identity_key = *identity_key;
     348          12 : }
     349             : 
     350             : 
     351             : fd_shred_dest_t *
     352             : fd_stake_ci_get_sdest_for_slot( fd_stake_ci_t const * info,
     353         591 :                                 ulong                 slot ) {
     354         591 :   ulong idx = fd_stake_ci_get_idx_for_slot( info, slot );
     355         591 :   return idx!=ULONG_MAX ? info->epoch_info[ idx ].sdest : NULL;
     356         591 : }
     357             : 
     358             : fd_epoch_leaders_t *
     359             : fd_stake_ci_get_lsched_for_slot( fd_stake_ci_t const * info,
     360         588 :                                  ulong                 slot ) {
     361         588 :   ulong idx = fd_stake_ci_get_idx_for_slot( info, slot );
     362         588 :   return idx!=ULONG_MAX ? info->epoch_info[ idx ].lsched : NULL;
     363         588 : }

Generated by: LCOV version 1.14