LCOV - code coverage report
Current view: top level - flamenco/leaders - fd_leaders_base.h (source / functions) Hit Total Coverage
Test: cov.lcov Lines: 18 32 56.2 %
Date: 2026-03-25 05:50:20 Functions: 10 1116 0.9 %

          Line data    Source code
       1             : #ifndef HEADER_fd_src_flamenco_leaders_fd_leaders_base_h
       2             : #define HEADER_fd_src_flamenco_leaders_fd_leaders_base_h
       3             : 
       4             : #include "../types/fd_types_custom.h"
       5             : #include "../features/fd_features.h"
       6             : 
       7      242361 : #define MAX_SHRED_DESTS              40200UL /* 200 * 201 - 1 (exclude self) */
       8           0 : #define MAX_SLOTS_PER_EPOCH          432000UL
       9          57 : #define MAX_STAKED_LEADERS           108000UL
      10          36 : #define MAX_COMPRESSED_STAKE_WEIGHTS (MAX_STAKED_LEADERS*2UL+1UL)
      11             : 
      12             : /* Follows message structure in fd_stake_ci_stake_msg_init.
      13             :    Frankendancer only */
      14             : struct fd_stake_weight_msg_t {
      15             :   ulong             epoch;             /* Epoch for which the stake weights are valid */
      16             :   ulong             staked_vote_cnt;   /* Number of staked nodes */
      17             :   ulong             staked_id_cnt;     /* Number of staked nodes */
      18             :   ulong             start_slot;        /* Start slot of the epoch */
      19             :   ulong             slot_cnt;          /* Number of slots in the epoch */
      20             :   ulong             excluded_id_stake; /* Total stake that is excluded for shred dests */
      21             :   ulong             vote_keyed_lsched; /* 1=use vote-keyed leader schedule, 0=use old leader schedule */
      22             : };
      23             : typedef struct fd_stake_weight_msg_t fd_stake_weight_msg_t;
      24             : 
      25         684 : #define FD_STAKE_CI_STAKE_MSG_HEADER_SZ (sizeof(fd_stake_weight_msg_t))
      26         306 : #define FD_STAKE_CI_STAKE_MSG_RECORD_SZ (sizeof(fd_vote_stake_weight_t))
      27           0 : #define FD_STAKE_CI_ID_WEIGHT_RECORD_SZ (sizeof(fd_stake_weight_t))
      28           0 : #define FD_STAKE_CI_STAKE_MSG_SZ (FD_STAKE_CI_STAKE_MSG_HEADER_SZ + MAX_COMPRESSED_STAKE_WEIGHTS * FD_STAKE_CI_STAKE_MSG_RECORD_SZ + MAX_SHRED_DESTS * FD_STAKE_CI_ID_WEIGHT_RECORD_SZ)
      29             : 
      30           0 : #define FD_STAKE_OUT_MTU FD_STAKE_CI_STAKE_MSG_SZ
      31             : 
      32             : static inline ulong fd_stake_weight_msg_sz( ulong staked_vote_cnt,
      33           0 :                                             ulong staked_id_cnt ) {
      34           0 :   return FD_STAKE_CI_STAKE_MSG_HEADER_SZ + staked_vote_cnt * FD_STAKE_CI_STAKE_MSG_RECORD_SZ + staked_id_cnt * FD_STAKE_CI_ID_WEIGHT_RECORD_SZ;
      35           0 : }
      36             : 
      37             : static inline fd_vote_stake_weight_t *
      38         378 : fd_stake_weight_msg_stake_weights( fd_stake_weight_msg_t const * stake_weight_msg ) {
      39         378 :   return (fd_vote_stake_weight_t *)fd_type_pun( (uchar *)stake_weight_msg + FD_STAKE_CI_STAKE_MSG_HEADER_SZ );
      40         378 : }
      41             : 
      42             : static inline fd_stake_weight_t *
      43         306 : fd_stake_weight_msg_id_weights( fd_stake_weight_msg_t const * stake_weight_msg ) {
      44         306 :   return (fd_stake_weight_t *)fd_type_pun( (uchar *)stake_weight_msg + FD_STAKE_CI_STAKE_MSG_HEADER_SZ + stake_weight_msg->staked_vote_cnt * FD_STAKE_CI_STAKE_MSG_RECORD_SZ );
      45         306 : }
      46             : 
      47             : /* Firedancer only */
      48             : struct fd_epoch_info_msg_t {
      49             :   ulong                  epoch;             /* Epoch for which the info is valid */
      50             :   ulong                  staked_vote_cnt;   /* Number of staked nodes */
      51             :   ulong                  staked_id_cnt;     /* Number of staked nodes */
      52             :   ulong                  start_slot;        /* Start slot of the epoch */
      53             :   ulong                  slot_cnt;          /* Number of slots in the epoch */
      54             :   ulong                  excluded_id_stake; /* Total stake that is excluded for shred dests */
      55             :   ulong                  vote_keyed_lsched; /* Whether vote account keyed leader schedule is active */
      56             :   fd_epoch_schedule_t    epoch_schedule;    /* Epoch schedule */
      57             :   fd_features_t          features;          /* Feature activation slots */
      58             : };
      59             : typedef struct fd_epoch_info_msg_t fd_epoch_info_msg_t;
      60             : 
      61             : /* Leader schedule calculation is done based on a weighted sample of
      62             :    a sorted list of stake weights (see fd_leaders and fd_wsample).
      63             :    Because there are many consumers of the stake weights from the Replay
      64             :    tile, a naive approach will make the link size very large:
      65             :    (72 bytes per weight * 40M vote accounts) = ~2.9GB.
      66             : 
      67             :    In order to do something more clever consider some protocol/message
      68             :    invariants:
      69             :    1. the set of stake weights that is passed around is sorted based on
      70             :       stake and then tiebroken on lexicographic order of the vote key.
      71             :    2. There are 108,000 leaders per epoch in the worst case (432000
      72             :       slots per epoch / 4 leaders per slot).
      73             :    3. Stake weighted sample is done to select leaders.
      74             : 
      75             :    From this we know that there can be up to 108k leaders from a much
      76             :    larger set of stake weights.  All other pubkeys can be ignored since
      77             :    we know that they won't be selected.  In the worst case there are
      78             :    40M - 108,000 vote accounts that won't be selected.  In the weighted
      79             :    sample, two adjacent, non-selected pubkeys can be combined into one
      80             :    aggregated stake weight.  If we take this idea to its limit we can
      81             :    combine all adjacent non-selected pubkeys into aggregated weights.
      82             :    The worst case number of these non-selected pubkeys is 108001 (if
      83             :    the non-selected keys are perfectly interleaved with the selected
      84             :    keys with the first and last key also being non-selected).  In
      85             :    practice, this bound is probably tighter because the higher staked
      86             :    nodes are almost guaranteed to be selected.
      87             : 
      88             :    Regardless, this allows us to compress the set of stake weights into
      89             :    a much smaller, bounded set.  The worst case number of compressed
      90             :    stake weights is 108000 + 108001 = 216001 keys.  The aggregated
      91             :    weights will be stored as FD_DUMMY_ACCOUNT.  The consumer is
      92             :    responsible for post-processing the aggregated weights to make sure
      93             :    they aren't inserted into any downstream data structures. */
      94             : 
      95         360 : #define FD_EPOCH_INFO_MSG_HEADER_SZ (sizeof(fd_epoch_info_msg_t))
      96           0 : #define FD_EPOCH_INFO_MAX_MSG_SZ    (FD_EPOCH_INFO_MSG_HEADER_SZ + MAX_COMPRESSED_STAKE_WEIGHTS * sizeof(fd_vote_stake_weight_t) + MAX_SHRED_DESTS * sizeof(fd_stake_weight_t))
      97           0 : #define FD_EPOCH_OUT_MTU            FD_EPOCH_INFO_MAX_MSG_SZ
      98             : 
      99             : static inline ulong fd_epoch_info_msg_sz( ulong vote_cnt,
     100           0 :                                           ulong id_weight_cnt ) {
     101           0 :   return FD_EPOCH_INFO_MSG_HEADER_SZ +
     102           0 :          (vote_cnt * sizeof(fd_vote_stake_weight_t)) +
     103           0 :          (id_weight_cnt * sizeof(fd_stake_weight_t));
     104           0 : }
     105             : 
     106             : static inline fd_vote_stake_weight_t *
     107         180 : fd_epoch_info_msg_stake_weights( fd_epoch_info_msg_t const * epoch_info_msg ) {
     108         180 :   return (fd_vote_stake_weight_t *)fd_type_pun( (uchar *)epoch_info_msg + FD_EPOCH_INFO_MSG_HEADER_SZ );
     109         180 : }
     110             : 
     111             : static inline fd_stake_weight_t *
     112         180 : fd_epoch_info_msg_id_weights( fd_epoch_info_msg_t const * epoch_info_msg ) {
     113         180 :   return (fd_stake_weight_t *)fd_type_pun( (uchar *)epoch_info_msg + FD_EPOCH_INFO_MSG_HEADER_SZ + epoch_info_msg->staked_vote_cnt * sizeof(fd_vote_stake_weight_t) );
     114         180 : }
     115             : 
     116             : /* compute_id_weights_from_vote_weights() translates vote-based
     117             :    stake weights into (older) identity-based stake weights.
     118             : 
     119             :    Before SIMD-0180, the leader schedule was generated starting from
     120             :    a list [(id, stake)] where `id` is the validator identity and
     121             :    `stake` its aggregated stake, and the same list was used to build
     122             :    the Turbine tree.
     123             : 
     124             :    After SIMD-0180, the leader schedule is generated by vote
     125             :    accounts, i.e. starting from a list [(vote, id, stake)] instead.
     126             :    This makes it easier to send rewards to the expected vote account.
     127             :    Notably, turbine tree doesn't change with SIMD-0180, so the old
     128             :    list [(id, stake)] is still necessary.
     129             : 
     130             :    Realistically, there should be a 1:1 relationship between id and
     131             :    vote, but unfortunately the on chain state allows for a 1:N
     132             :    relationship (1 id could be associated to N vote accounts).
     133             :    At the time of writing, testnet has one such example.
     134             :    id: DtSguGSHVrXdqZU1mKWKocsAjrXMhaC7YJic5xxN1Uom
     135             :    votes:
     136             :    - https://solscan.io/account/BbtyLT1ntMFbbXtsJRCZnYjpe7d7TUtyZeGKzod3eNsN?cluster=testnet
     137             :    - https://solscan.io/account/FFr8Gyjy3Wjeqv6oD4RjbwqD1mVfKycAFxQdASYAfR75?cluster=testnet
     138             : 
     139             :    Even when there is a 1:1 relationship, the order of the 2 lists
     140             :    can be different because validators with the same stake could
     141             :    be ordered differently by vote vs id.
     142             : 
     143             :    Last consideration, this operation is done only once per epoch, twice
     144             :    at startup.
     145             : 
     146             :    The current implementation uses sort in place to avoid extra memory
     147             :    for a map or tree. */
     148             :    ulong
     149             :    compute_id_weights_from_vote_weights( fd_stake_weight_t *            stake_weight,
     150             :                                          fd_vote_stake_weight_t const * vote_stake_weight,
     151             :                                          ulong                          staked_cnt );
     152             : 
     153             : #endif /* HEADER_fd_src_flamenco_leaders_fd_leaders_base_h */

Generated by: LCOV version 1.14