LCOV - code coverage report
Current view: top level - flamenco/runtime/sysvar - fd_sysvar_clock.c (source / functions) Hit Total Coverage
Test: cov.lcov Lines: 159 168 94.6 %
Date: 2026-09-17 04:28:31 Functions: 8 8 100.0 %

          Line data    Source code
       1             : #include "fd_sysvar_clock.h"
       2             : #include "fd_sysvar_epoch_schedule.h"
       3             : #include "../fd_runtime_stack.h"
       4             : #include "../fd_system_ids.h"
       5             : #include "../sysvar/fd_sysvar.h"
       6             : 
       7             : /* Syvar Clock Possible Values:
       8             :   slot:
       9             :   [0, ULONG_MAX]
      10             : 
      11             :   epoch:
      12             :   [0, slot/432000UL]
      13             : 
      14             :   epoch_start_timestamp:
      15             :   [0, ULONG_MAX]
      16             : 
      17             :   unix_timestamp:
      18             :   This value is bounded by the slot distance from the
      19             :   epoch_start_timestamp.
      20             :   The protocol allows for a maximum drift (either fast or slow) from the
      21             :   start of the epoch's timestamp.  The expected time is called the PoH
      22             :   offset.  This offset is calculated by (epoch_start_timestamp + slots
      23             :   since epoch * slot_duration). The drift is then bounded by the
      24             :   max_allowable_drift_{slow,fast}.  The stake weighted offset can be
      25             :   150% more than the PoH offset and 25% less than the PoH offset.
      26             :   So, the bounds for the unix_timestamp can be calculated by:
      27             :   upper bound = epoch_start_timestamp + (slots since epoch * slot_duration) * 2.5
      28             :   lower bound = epoch_start_timestamp + (slots since epoch * slot_duration) * 0.75
      29             : 
      30             :   leader_schedule_epoch:
      31             :   This is the value of the epoch used for the leader schedule.  It is
      32             :   computed based on the values of the epoch schedule (first_normal_slot,
      33             :   leader_schedule_slot_offset, slots_per_epoch).  It is always equal to
      34             :   ((slot - first_normal_slot) + leader_schedule_slot_offset) / schedule->slots_per_epoch
      35             : */
      36             : 
      37             : /* https://github.com/anza-xyz/agave/blob/v2.3.7/runtime/src/stake_weighted_timestamp.rs#L14 */
      38        4077 : #define MAX_ALLOWABLE_DRIFT_FAST_PERCENT ( 25U )
      39             : 
      40             : /* https://github.com/anza-xyz/agave/blob/v2.3.7/runtime/src/stake_weighted_timestamp.rs#L15 */
      41        4077 : #define MAX_ALLOWABLE_DRIFT_SLOW_PERCENT ( 150U )
      42             : 
      43             : /* Do all intermediate calculations at nanosecond precision, to mirror
      44             :    Solana's behavior. */
      45       15702 : #define NS_IN_S ((long)1e9)
      46             : 
      47             : /* https://github.com/anza-xyz/agave/blob/v2.3.7/runtime/src/bank.rs#L2110-L2117 */
      48             : static inline long
      49           6 : unix_timestamp_from_genesis( fd_bank_t * bank ) {
      50             :   /* TODO: genesis_creation_time needs to be a long in the bank. */
      51           6 :   return fd_long_sat_add(
      52           6 :       (long)bank->f.genesis_creation_time,
      53           6 :       (long)( fd_uint128_sat_mul( bank->f.slot, bank->f.slot_params.ns_per_slot ) / NS_IN_S ) );
      54           6 : }
      55             : 
      56             : static void
      57             : fd_sysvar_clock_write( fd_bank_t *                   bank,
      58             :                        fd_accdb_t *                  accdb,
      59             :                        fd_capture_ctx_t *            capture_ctx,
      60        4524 :                        fd_sol_sysvar_clock_t const * clock ) {
      61        4524 :   fd_sysvar_account_update( bank, accdb, capture_ctx, &fd_sysvar_clock_id, clock, sizeof(fd_sol_sysvar_clock_t) );
      62        4524 : }
      63             : 
      64             : fd_sol_sysvar_clock_t *
      65             : fd_sysvar_clock_read( fd_accdb_t *            accdb,
      66             :                       fd_accdb_fork_id_t      fork_id,
      67        4533 :                       fd_sol_sysvar_clock_t * clock ) {
      68        4533 :   fd_acc_t acc = fd_accdb_read_one( accdb, fork_id, fd_sysvar_clock_id.uc );
      69        4533 :   if( FD_UNLIKELY( !acc.lamports || acc.data_len<sizeof(fd_sol_sysvar_clock_t) ) ) {
      70             :     /* This check is needed as a quirk of the fuzzer. If a sysvar
      71             :        account exists in the accounts database, but doesn't have any
      72             :        lamports, this means that the account does not exist.  This
      73             :        wouldn't happen in a real execution environment. */
      74           0 :     fd_accdb_unread_one( accdb, &acc );
      75           0 :     return NULL;
      76           0 :   }
      77             : 
      78        4533 :   fd_memcpy( clock, acc.data, sizeof(fd_sol_sysvar_clock_t) );
      79        4533 :   fd_accdb_unread_one( accdb, &acc );
      80        4533 :   return clock;
      81        4533 : }
      82             : 
      83             : void
      84             : fd_sysvar_clock_init( fd_bank_t *        bank,
      85             :                       fd_accdb_t *       accdb,
      86           6 :                       fd_capture_ctx_t * capture_ctx ) {
      87           6 :   long timestamp = unix_timestamp_from_genesis( bank );
      88             : 
      89           6 :   fd_sol_sysvar_clock_t clock = {
      90           6 :     .slot                  = bank->f.slot,
      91           6 :     .epoch                 = 0,
      92           6 :     .epoch_start_timestamp = timestamp,
      93           6 :     .leader_schedule_epoch = 1,
      94           6 :     .unix_timestamp        = timestamp,
      95           6 :   };
      96           6 :   fd_sysvar_clock_write( bank, accdb, capture_ctx, &clock );
      97           6 : }
      98             : 
      99             : #define SORT_NAME  sort_stake_ts
     100         552 : #define SORT_KEY_T ts_est_ele_t
     101         276 : #define SORT_BEFORE(a,b) ( (a).timestamp < (b).timestamp )
     102             : #include "../../../util/tmpl/fd_sort.c"
     103             : 
     104             : static void
     105             : accum_vote_stakes( fd_bank_t *          bank,
     106             :                    fd_runtime_stack_t * runtime_stack,
     107             :                    uint128 *            total_stake_out,
     108        4512 :                    ulong *              ts_ele_cnt_out ) {
     109             : 
     110        4512 :   ts_est_ele_t * ts_eles = runtime_stack->clock_ts.staked_ts;
     111        4512 :   ulong ts_ele_cnt = 0UL;
     112             : 
     113        4512 :   uint128 total_stake = 0UL;
     114             : 
     115        4512 :   fd_epoch_schedule_t const * epoch_schedule = &bank->f.epoch_schedule;
     116        4512 :   ulong                       current_slot   = bank->f.slot;
     117             : 
     118        4512 :   fd_vote_stakes_t * vote_stakes = fd_bank_vote_stakes( bank );
     119        4512 :   ulong              fork_id     = bank->vote_stakes_fork_id;
     120             : 
     121        4512 :   uchar __attribute__((aligned(FD_VOTE_STAKES_ITER_ALIGN))) iter_mem[ FD_VOTE_STAKES_ITER_FOOTPRINT ];
     122        4512 :   for( fd_vote_stakes_iter_t * iter = fd_vote_stakes_iter_init( vote_stakes, fork_id, FD_VOTE_STAKES_ITER_T_2, iter_mem );
     123        9669 :        !fd_vote_stakes_iter_done( vote_stakes, fork_id, FD_VOTE_STAKES_ITER_T_2, iter );
     124        5157 :        fd_vote_stakes_iter_next( vote_stakes, fork_id, FD_VOTE_STAKES_ITER_T_2, iter ) ) {
     125        5157 :     fd_pubkey_t pubkey;
     126        5157 :     ulong       stake_t_2;
     127        5157 :     ulong       last_vote_slot;
     128        5157 :     long        last_vote_timestamp;
     129        5157 :     uchar       is_valid;
     130        5157 :     fd_vote_stakes_iter_ele( vote_stakes, fork_id, FD_VOTE_STAKES_ITER_T_2, iter, &pubkey, NULL, &stake_t_2,
     131        5157 :                              &last_vote_slot, &last_vote_timestamp, NULL, &is_valid, NULL, NULL, NULL );
     132        5157 :     if( FD_UNLIKELY( !is_valid ) ) continue;
     133             : 
     134             :     /* https://github.com/anza-xyz/agave/blob/v3.0.0/runtime/src/bank.rs#L2445 */
     135        4971 :     if( FD_UNLIKELY( current_slot<last_vote_slot ) ) {
     136             :       /* Don't count vote accounts with a last vote slot that is greater
     137             :          than the current slot. */
     138           0 :       continue;
     139           0 :     }
     140        4971 :     ulong slot_delta = current_slot-last_vote_slot;
     141             : 
     142             :     /* Don't count vote accounts that haven't voted in the past 432k
     143             :         slots (length of an epoch).
     144             :         https://github.com/anza-xyz/agave/blob/v3.0.0/runtime/src/bank.rs#L2446-L2447 */
     145        4971 :     if( FD_UNLIKELY( slot_delta>epoch_schedule->slots_per_epoch ) ) {
     146         618 :       continue;
     147         618 :     }
     148             : 
     149             :     /* Calculate the timestamp estimate by taking the last vote
     150             :         timestamp and adding the estimated time since the last vote
     151             :         (delta from last vote slot to current slot * slot duration).
     152             :         https://github.com/anza-xyz/agave/blob/v2.3.7/runtime/src/stake_weighted_timestamp.rs#L44-L45 */
     153        4353 :     ulong offset   = fd_slot_params_slot_range_duration_ns( bank, last_vote_slot+1UL, current_slot+1UL );
     154        4353 :     long  estimate = fd_long_sat_add( last_vote_timestamp, (long)(offset / NS_IN_S) );
     155             : 
     156             :     /* For each timestamp, accumulate the stake from E-2.  If the entry
     157             :         for the timestamp doesn't exist yet, insert it.  Otherwise,
     158             :         update the existing entry.
     159             :         https://github.com/anza-xyz/agave/blob/v2.3.7/runtime/src/stake_weighted_timestamp.rs#L46-L53 */
     160        4353 :     ts_eles[ ts_ele_cnt ] = (ts_est_ele_t){
     161        4353 :       .timestamp = estimate,
     162        4353 :       .stake     = { .ud=stake_t_2 },
     163        4353 :     };
     164        4353 :     ts_ele_cnt++;
     165             : 
     166             :     /* https://github.com/anza-xyz/agave/blob/v2.3.7/runtime/src/stake_weighted_timestamp.rs#L54 */
     167        4353 :     total_stake += stake_t_2;
     168        4353 :   }
     169             : 
     170        4512 :   *total_stake_out = total_stake;
     171        4512 :   *ts_ele_cnt_out  = ts_ele_cnt;
     172        4512 : }
     173             : 
     174             : /* get_timestamp_estimate calculates a timestamp estimate.  Does not
     175             :    modify the slot context.  Walks all cached vote accounts (from the
     176             :    "bank") and calculates a unix timestamp estimate. Returns the
     177             :    timestamp estimate.  Crashes the process with FD_LOG_ERR on failure
     178             :    (e.g. too many vote accounts).
     179             : 
     180             :   https://github.com/anza-xyz/agave/blob/v2.3.7/runtime/src/bank.rs#L2563-L2601 */
     181             : static long
     182             : get_timestamp_estimate( fd_bank_t *             bank,
     183             :                         fd_sol_sysvar_clock_t * clock,
     184             :                         fd_runtime_stack_t *    runtime_stack,
     185             :                         ulong const *           parent_epoch,
     186        4512 :                         int *                   out_estimate_present ) {
     187        4512 :   fd_epoch_schedule_t const * epoch_schedule = &bank->f.epoch_schedule;
     188        4512 :   ulong                       current_slot   = bank->f.slot;
     189             : 
     190        4512 :   ts_est_ele_t * ts_eles = runtime_stack->clock_ts.staked_ts;
     191             : 
     192             :   /* https://github.com/anza-xyz/agave/blob/v2.3.7/runtime/src/stake_weighted_timestamp.rs#L41 */
     193        4512 :   ulong  ts_ele_cnt   = 0UL;
     194        4512 :   uint128 total_stake = 0UL;
     195             : 
     196             :   /* A timestamp estimate is calculated at every slot using the most
     197             :      recent vote states of voting validators. This estimated is based on
     198             :      a stake weighted median using the stake as of the end of epoch E-2
     199             :      if we are currently in epoch E. We do not count vote accounts that
     200             :      have not voted in an epoch's worth of slots (432k). */
     201             : 
     202        4512 :   ulong curr_epoch = fd_slot_to_epoch( epoch_schedule, bank->f.slot, NULL );
     203             : 
     204        4512 :   accum_vote_stakes( bank, runtime_stack, &total_stake, &ts_ele_cnt );
     205             : 
     206             :   /* https://github.com/anza-xyz/agave/blob/v2.3.7/runtime/src/stake_weighted_timestamp.rs#L56-L58 */
     207        4512 :   if( FD_UNLIKELY( total_stake==0UL ) ) {
     208         435 :     *out_estimate_present = 0;
     209         435 :     return 0L;
     210         435 :   }
     211             : 
     212        4077 :   sort_stake_ts_inplace( ts_eles, ts_ele_cnt );
     213             : 
     214             :   /* Populate estimate with the stake-weighted median timestamp.
     215             :      https://github.com/anza-xyz/agave/blob/v2.3.7/runtime/src/stake_weighted_timestamp.rs#L59-L68 */
     216        4077 :   uint128 stake_accumulator = 0;
     217        4077 :   long    estimate          = 0L;
     218        4353 :   for( ulong i=0UL; i<ts_ele_cnt; i++ ) {
     219        4353 :     stake_accumulator = fd_uint128_sat_add( stake_accumulator, ts_eles[i].stake.ud );
     220        4353 :     if( stake_accumulator>(total_stake/2UL) ) {
     221        4077 :       estimate = ts_eles[ i ].timestamp;
     222        4077 :       break;
     223        4077 :     }
     224        4353 :   }
     225             : 
     226             :   /* Bound estimate by `max_allowable_drift` since the start of the
     227             :      epoch.
     228             :      https://github.com/anza-xyz/agave/blob/v2.3.7/runtime/src/stake_weighted_timestamp.rs#L69-L99 */
     229        4077 :   ulong epoch_for_start_slot  = parent_epoch ? *parent_epoch : curr_epoch;
     230        4077 :   ulong epoch_start_slot      = fd_epoch_slot0( epoch_schedule, epoch_for_start_slot );
     231        4077 :   long  epoch_start_timestamp = clock->epoch_start_timestamp;
     232             : 
     233             :   /* https://github.com/anza-xyz/agave/blob/v2.3.7/runtime/src/stake_weighted_timestamp.rs#L71-L72 */
     234        4077 :   ulong poh_estimate_offset = fd_slot_params_slot_range_duration_ns( bank, epoch_start_slot+1UL, current_slot+1UL );
     235             : 
     236             :   /* https://github.com/anza-xyz/agave/blob/v2.3.7/runtime/src/stake_weighted_timestamp.rs#L73-L77 */
     237        4077 :   ulong estimate_offset = fd_ulong_sat_mul( NS_IN_S, fd_ulong_sat_sub( (ulong)estimate, (ulong)epoch_start_timestamp ) );
     238             : 
     239             :   /* https://github.com/anza-xyz/agave/blob/v2.3.7/runtime/src/stake_weighted_timestamp.rs#L78-L81 */
     240        4077 :   ulong max_allowable_drift_fast = fd_ulong_sat_mul( poh_estimate_offset, MAX_ALLOWABLE_DRIFT_FAST_PERCENT ) / 100UL;
     241        4077 :   ulong max_allowable_drift_slow = fd_ulong_sat_mul( poh_estimate_offset, MAX_ALLOWABLE_DRIFT_SLOW_PERCENT ) / 100UL;
     242             : 
     243             :   /* https://github.com/anza-xyz/agave/blob/v2.3.7/runtime/src/stake_weighted_timestamp.rs#L82-L98 */
     244        4077 :   if( estimate_offset>poh_estimate_offset && fd_ulong_sat_sub( estimate_offset, poh_estimate_offset )>max_allowable_drift_slow ) {
     245          72 :     estimate = fd_long_sat_add(
     246          72 :         epoch_start_timestamp,
     247          72 :         fd_long_sat_add( (long)poh_estimate_offset / NS_IN_S, (long)max_allowable_drift_slow / NS_IN_S ) );
     248        4005 :   } else if( estimate_offset<poh_estimate_offset && fd_ulong_sat_sub( poh_estimate_offset, estimate_offset )>max_allowable_drift_fast ) {
     249        3561 :     estimate = fd_long_sat_sub(
     250        3561 :         fd_long_sat_add( epoch_start_timestamp, (long)poh_estimate_offset / NS_IN_S ),
     251        3561 :         (long)max_allowable_drift_fast / NS_IN_S );
     252        3561 :   }
     253             : 
     254        4077 :   *out_estimate_present = 1;
     255        4077 :   return estimate;
     256        4512 : }
     257             : 
     258             : /* TODO: This function should be called from genesis bootup as well with
     259             :    parent_epoch = NULL
     260             :    https://github.com/anza-xyz/agave/blob/v2.3.7/runtime/src/bank.rs#L2158-L2215 */
     261             : void
     262             : fd_sysvar_clock_update( fd_bank_t *          bank,
     263             :                         fd_accdb_t *         accdb,
     264             :                         fd_capture_ctx_t *   capture_ctx,
     265             :                         fd_runtime_stack_t * runtime_stack,
     266        4512 :                         ulong const *        parent_epoch ) {
     267        4512 :   fd_sol_sysvar_clock_t clock_[1];
     268        4512 :   fd_sol_sysvar_clock_t * clock = fd_sysvar_clock_read( accdb, bank->accdb_fork_id, clock_ );
     269        4512 :   if( FD_UNLIKELY( !clock ) ) FD_LOG_ERR(( "fd_sysvar_clock_read failed" ));
     270             : 
     271        4512 :   fd_epoch_schedule_t const * epoch_schedule = &bank->f.epoch_schedule;
     272        4512 :   ulong                       current_slot   = bank->f.slot;
     273        4512 :   ulong                       current_epoch  = fd_slot_to_epoch( epoch_schedule, current_slot, NULL );
     274             : 
     275             :   /* https://github.com/anza-xyz/agave/blob/v2.3.7/runtime/src/bank.rs#L2159 */
     276        4512 :   long unix_timestamp = clock->unix_timestamp;
     277             : 
     278             :   /* https://github.com/anza-xyz/agave/blob/v2.3.7/runtime/src/bank.rs#L2175 */
     279        4512 :   long ancestor_timestamp = clock->unix_timestamp;
     280             : 
     281             :   /* TODO: Are we handling slot 0 correctly?
     282             :      https://github.com/anza-xyz/agave/blob/v2.3.7/runtime/src/bank.rs#L2176-L2183 */
     283        4512 :   int  estimate_present   = 0;
     284        4512 :   long timestamp_estimate = get_timestamp_estimate( bank, clock, runtime_stack, parent_epoch, &estimate_present );
     285             : 
     286             :   /* If the timestamp was successfully calculated, use it. Otherwise,
     287             :      keep the old one. */
     288        4512 :   if( FD_LIKELY( estimate_present ) ) {
     289        4077 :     unix_timestamp = timestamp_estimate;
     290             : 
     291             :     /* https://github.com/anza-xyz/agave/blob/v2.3.7/runtime/src/bank.rs#L2180-L2182 */
     292        4077 :     if( timestamp_estimate<ancestor_timestamp ) {
     293           3 :       unix_timestamp = ancestor_timestamp;
     294           3 :     }
     295        4077 :   }
     296             : 
     297             :   /* https://github.com/anza-xyz/agave/blob/v2.3.7/runtime/src/bank.rs#L2191-L2197 */
     298        4512 :   long epoch_start_timestamp = (parent_epoch!=NULL && *parent_epoch!=current_epoch) ?
     299         282 :       unix_timestamp :
     300        4512 :       clock->epoch_start_timestamp;
     301             : 
     302             :   /* https://github.com/anza-xyz/agave/blob/v2.3.7/runtime/src/bank.rs#L2198-L2201 */
     303        4512 :   if( FD_UNLIKELY( current_slot==0UL ) ) {
     304           0 :     long timestamp_from_genesis = unix_timestamp_from_genesis( bank );
     305           0 :     unix_timestamp              = timestamp_from_genesis;
     306           0 :     epoch_start_timestamp       = timestamp_from_genesis;
     307           0 :   }
     308             : 
     309             :   /* https://github.com/anza-xyz/agave/blob/v2.3.7/runtime/src/bank.rs#L2202-L2208 */
     310        4512 :   *clock = (fd_sol_sysvar_clock_t){
     311        4512 :     .slot                  = current_slot,
     312        4512 :     .epoch_start_timestamp = epoch_start_timestamp,
     313        4512 :     .epoch                 = current_epoch,
     314        4512 :     .leader_schedule_epoch = fd_slot_to_leader_schedule_epoch( epoch_schedule, current_slot ),
     315        4512 :     .unix_timestamp        = unix_timestamp,
     316        4512 :   };
     317             : 
     318             :   /* https://github.com/anza-xyz/agave/blob/v2.3.7/runtime/src/bank.rs#L2209-L2214 */
     319        4512 :   fd_sysvar_clock_write( bank, accdb, capture_ctx, clock );
     320        4512 : }
     321             : 
     322             : void
     323             : fd_sysvar_clock_update_slot_alpenglow( fd_bank_t *        bank,
     324             :                                        fd_accdb_t *       accdb,
     325           6 :                                        fd_capture_ctx_t * capture_ctx ) {
     326           6 :   fd_sol_sysvar_clock_t clock_[1];
     327           6 :   fd_sol_sysvar_clock_t * clock = fd_sysvar_clock_read( accdb, bank->accdb_fork_id, clock_ );
     328           6 :   if( FD_UNLIKELY( !clock ) ) FD_LOG_ERR(( "fd_sysvar_clock_read failed" ));
     329             : 
     330           6 :   fd_epoch_schedule_t const * epoch_schedule = &bank->f.epoch_schedule;
     331           6 :   ulong current_epoch = fd_slot_to_epoch( epoch_schedule, bank->f.slot,        NULL );
     332           6 :   ulong parent_epoch  = fd_slot_to_epoch( epoch_schedule, bank->f.parent_slot, NULL );
     333             : 
     334           6 :   long epoch_start_timestamp;
     335           6 :   if(      FD_UNLIKELY( !bank->f.slot               ) ) epoch_start_timestamp = unix_timestamp_from_genesis( bank );
     336           6 :   else if( FD_UNLIKELY( parent_epoch!=current_epoch ) ) epoch_start_timestamp = clock->unix_timestamp;
     337           6 :   else                                                  epoch_start_timestamp = clock->epoch_start_timestamp;
     338             : 
     339           6 :   *clock = (fd_sol_sysvar_clock_t){
     340           6 :     .slot                  = bank->f.slot,
     341           6 :     .epoch_start_timestamp = epoch_start_timestamp,
     342           6 :     .epoch                 = current_epoch,
     343           6 :     .leader_schedule_epoch = fd_slot_to_leader_schedule_epoch( epoch_schedule, bank->f.slot ),
     344           6 :     .unix_timestamp        = clock->unix_timestamp, /* parent's footer timestamp */
     345           6 :   };
     346           6 :   fd_sysvar_clock_write( bank, accdb, capture_ctx, clock );
     347           6 : }

Generated by: LCOV version 1.14