LCOV - code coverage report
Current view: top level - flamenco/rewards - fd_rewards.c (source / functions) Hit Total Coverage
Test: cov.lcov Lines: 233 696 33.5 %
Date: 2026-01-08 05:16:19 Functions: 18 26 69.2 %

          Line data    Source code
       1             : #include "fd_rewards.h"
       2             : #include <math.h>
       3             : 
       4             : #include "../runtime/fd_acc_mgr.h"
       5             : #include "../runtime/sysvar/fd_sysvar_epoch_rewards.h"
       6             : #include "../runtime/sysvar/fd_sysvar_epoch_schedule.h"
       7             : #include "../stakes/fd_stakes.h"
       8             : #include "../runtime/program/fd_stake_program.h"
       9             : #include "../runtime/sysvar/fd_sysvar_stake_history.h"
      10             : #include "../capture/fd_capture_ctx.h"
      11             : #include "../runtime/fd_runtime_stack.h"
      12             : #include "../runtime/fd_runtime.h"
      13             : #include "fd_epoch_rewards.h"
      14             : #include "../accdb/fd_accdb_impl_v1.h"
      15             : 
      16             : /* https://github.com/anza-xyz/agave/blob/7117ed9653ce19e8b2dea108eff1f3eb6a3378a7/sdk/src/inflation.rs#L85 */
      17             : static double
      18          18 : total( fd_inflation_t const * inflation, double year ) {
      19          18 :   if ( FD_UNLIKELY( year == 0.0 ) ) {
      20           0 :     FD_LOG_ERR(( "inflation year 0" ));
      21           0 :   }
      22          18 :   double tapered = inflation->initial * pow( (1.0 - inflation->taper), year );
      23          18 :   return (tapered > inflation->terminal) ? tapered : inflation->terminal;
      24          18 : }
      25             : 
      26             : /* https://github.com/anza-xyz/agave/blob/7117ed9653ce19e8b2dea108eff1f3eb6a3378a7/sdk/src/inflation.rs#L102 */
      27             : static double
      28          27 : foundation( fd_inflation_t const * inflation, double year ) {
      29          27 :   return (year < inflation->foundation_term) ? inflation->foundation * total(inflation, year) : 0.0;
      30          27 : }
      31             : 
      32             : /* https://github.com/anza-xyz/agave/blob/7117ed9653ce19e8b2dea108eff1f3eb6a3378a7/sdk/src/inflation.rs#L97 */
      33             : static double
      34           9 : validator( fd_inflation_t const * inflation, double year) {
      35             :   /* https://github.com/firedancer-io/solana/blob/dab3da8e7b667d7527565bddbdbecf7ec1fb868e/sdk/src/inflation.rs#L96-L99 */
      36           9 :   FD_LOG_DEBUG(("Validator Rate: %.16f %.16f %.16f %.16f %.16f", year, total( inflation, year ), foundation( inflation, year ), inflation->taper, inflation->initial));
      37           9 :   return total( inflation, year ) - foundation( inflation, year );
      38           9 : }
      39             : 
      40             : /* Calculates the starting slot for inflation from the activation slot. The activation slot is the earliest
      41             :     activation slot of the following features:
      42             :     - devnet_and_testnet
      43             :     - full_inflation_enable, if full_inflation_vote has been activated
      44             : 
      45             :     https://github.com/anza-xyz/agave/blob/7117ed9653ce19e8b2dea108eff1f3eb6a3378a7/runtime/src/bank.rs#L2095 */
      46             : static FD_FN_CONST ulong
      47           9 : get_inflation_start_slot( fd_bank_t const * bank ) {
      48           9 :   ulong devnet_and_testnet = FD_FEATURE_ACTIVE_BANK( bank, devnet_and_testnet )
      49           9 :       ? fd_bank_features_query( bank )->devnet_and_testnet
      50           9 :       : ULONG_MAX;
      51             : 
      52           9 :   ulong enable = ULONG_MAX;
      53           9 :   if( FD_FEATURE_ACTIVE_BANK( bank, full_inflation_vote ) &&
      54           9 :       FD_FEATURE_ACTIVE_BANK( bank, full_inflation_enable ) ) {
      55           0 :     enable = fd_bank_features_query( bank )->full_inflation_enable;
      56           0 :   }
      57             : 
      58           9 :   ulong min_slot = fd_ulong_min( enable, devnet_and_testnet );
      59           9 :   if( min_slot == ULONG_MAX ) {
      60           9 :     if( FD_FEATURE_ACTIVE_BANK( bank, pico_inflation ) ) {
      61           0 :       min_slot = fd_bank_features_query( bank )->pico_inflation;
      62           9 :     } else {
      63           9 :       min_slot = 0;
      64           9 :     }
      65           9 :   }
      66           9 :   return min_slot;
      67           9 : }
      68             : 
      69             : /* https://github.com/anza-xyz/agave/blob/7117ed9653ce19e8b2dea108eff1f3eb6a3378a7/runtime/src/bank.rs#L2110 */
      70             : static ulong
      71             : get_inflation_num_slots( fd_bank_t const *           bank,
      72             :                          fd_epoch_schedule_t const * epoch_schedule,
      73           9 :                          ulong                       slot ) {
      74           9 :   ulong inflation_activation_slot = get_inflation_start_slot( bank );
      75           9 :   ulong inflation_start_slot      = fd_epoch_slot0( epoch_schedule,
      76           9 :                                                     fd_ulong_sat_sub( fd_slot_to_epoch( epoch_schedule,
      77           9 :                                                                                         inflation_activation_slot, NULL ),
      78           9 :                                                                       1UL ) );
      79             : 
      80           9 :   ulong epoch = fd_slot_to_epoch( epoch_schedule, slot, NULL );
      81             : 
      82           9 :   return fd_epoch_slot0( epoch_schedule, epoch ) - inflation_start_slot;
      83           9 : }
      84             : 
      85             : /* https://github.com/anza-xyz/agave/blob/7117ed9653ce19e8b2dea108eff1f3eb6a3378a7/runtime/src/bank.rs#L2121 */
      86             : static double
      87           9 : slot_in_year_for_inflation( fd_bank_t const * bank ) {
      88           9 :   fd_epoch_schedule_t const * epoch_schedule = fd_bank_epoch_schedule_query( bank );
      89           9 :   ulong num_slots = get_inflation_num_slots( bank, epoch_schedule, fd_bank_slot_get( bank ) );
      90           9 :   return (double)num_slots / (double)fd_bank_slots_per_year_get( bank );
      91           9 : }
      92             : 
      93             : static void
      94             : get_vote_credits( uchar const *               account_data,
      95             :                   ulong                       account_data_len,
      96             :                   uchar *                     buf,
      97           0 :                   fd_vote_epoch_credits_t * * credits ) {
      98             : 
      99           0 :   fd_bincode_decode_ctx_t ctx = {
     100           0 :     .data    = account_data,
     101           0 :     .dataend = account_data + account_data_len,
     102           0 :   };
     103             : 
     104           0 :   fd_vote_state_versioned_t * vsv = fd_vote_state_versioned_decode( buf, &ctx );
     105           0 :   if( FD_UNLIKELY( vsv==NULL ) ) {
     106           0 :     FD_LOG_CRIT(( "unable to decode vote state versioned" ));
     107           0 :   }
     108             : 
     109           0 :   switch( vsv->discriminant ) {
     110           0 :   case fd_vote_state_versioned_enum_v0_23_5:
     111           0 :     *credits = vsv->inner.v0_23_5.epoch_credits;
     112           0 :     break;
     113           0 :   case fd_vote_state_versioned_enum_v1_14_11:
     114           0 :     *credits = vsv->inner.v1_14_11.epoch_credits;
     115           0 :     break;
     116           0 :   case fd_vote_state_versioned_enum_v3:
     117           0 :     *credits = vsv->inner.v3.epoch_credits;
     118           0 :     break;
     119           0 :   case fd_vote_state_versioned_enum_v4:
     120           0 :     *credits = vsv->inner.v4.epoch_credits;
     121           0 :     break;
     122           0 :   default:
     123           0 :     __builtin_unreachable();
     124           0 :   }
     125           0 : }
     126             : 
     127             : static void
     128             : calculate_stake_points_and_credits_recalculation( fd_stake_history_t const *     stake_history,
     129             :                                                   fd_stake_delegation_t const *  stake,
     130             :                                                   ulong *                        new_rate_activation_epoch,
     131             :                                                   fd_vote_state_credits_t *      recalc_vote_state_credits,
     132           0 :                                                   fd_calculated_stake_points_t * result ) {
     133           0 :   ulong credits_in_stake = stake->credits_observed;
     134           0 :   ulong credits_in_vote  = recalc_vote_state_credits->credits[ recalc_vote_state_credits->credits_cnt-1UL ];
     135             : 
     136             :   /* If the Vote account has less credits observed than the Stake account,
     137             :       something is wrong and we need to force an update.
     138             : 
     139             :       https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/programs/stake/src/points.rs#L142 */
     140           0 :   if( FD_UNLIKELY( credits_in_vote < credits_in_stake ) ) {
     141           0 :     result->points.ud = 0;
     142           0 :     result->new_credits_observed = credits_in_vote;
     143           0 :     result->force_credits_update_with_skipped_reward = 1;
     144           0 :     return;
     145           0 :   }
     146             : 
     147             :   /* If the Vote account has the same amount of credits observed as the Stake account,
     148             :       then the Vote account hasn't earnt any credits and so there is nothing to update.
     149             : 
     150             :       https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/programs/stake/src/points.rs#L148 */
     151           0 :   if( FD_UNLIKELY( credits_in_vote == credits_in_stake ) ) {
     152           0 :     result->points.ud = 0;
     153           0 :     result->new_credits_observed = credits_in_vote;
     154           0 :     result->force_credits_update_with_skipped_reward = 0;
     155           0 :     return;
     156           0 :   }
     157             : 
     158             :   /* Calculate the points for each epoch credit */
     159           0 :   uint128 points               = 0;
     160           0 :   ulong   new_credits_observed = credits_in_stake;
     161           0 :   for( ulong i=0UL; i<recalc_vote_state_credits->credits_cnt; i++ ) {
     162             : 
     163           0 :     ulong final_epoch_credits   = recalc_vote_state_credits->credits[ i ];
     164           0 :     ulong initial_epoch_credits = recalc_vote_state_credits->prev_credits[ i ];
     165           0 :     uint128 earned_credits = 0;
     166           0 :     if( FD_LIKELY( credits_in_stake < initial_epoch_credits ) ) {
     167           0 :       earned_credits = (uint128)(final_epoch_credits - initial_epoch_credits);
     168           0 :     } else if( FD_UNLIKELY( credits_in_stake < final_epoch_credits ) ) {
     169           0 :       earned_credits = (uint128)(final_epoch_credits - new_credits_observed);
     170           0 :     }
     171             : 
     172           0 :     new_credits_observed = fd_ulong_max( new_credits_observed, final_epoch_credits );
     173             : 
     174           0 :     fd_delegation_t delegation = {
     175           0 :       .voter_pubkey         = stake->vote_account,
     176           0 :       .stake                = stake->stake,
     177           0 :       .activation_epoch     = stake->activation_epoch,
     178           0 :       .deactivation_epoch   = stake->deactivation_epoch,
     179           0 :       .warmup_cooldown_rate = stake->warmup_cooldown_rate,
     180           0 :     };
     181             : 
     182           0 :     ulong stake_amount = fd_stake_activating_and_deactivating(
     183           0 :         &delegation,
     184           0 :         recalc_vote_state_credits->epoch[ i ],
     185           0 :         stake_history,
     186           0 :         new_rate_activation_epoch ).effective;
     187             : 
     188           0 :     points += (uint128)stake_amount * earned_credits;
     189             : 
     190           0 :   }
     191             : 
     192           0 :   result->points.ud = points;
     193           0 :   result->new_credits_observed = new_credits_observed;
     194           0 :   result->force_credits_update_with_skipped_reward = 0;
     195           0 : }
     196             : 
     197             : /* For a given stake and vote_state, calculate how many points were earned (credits * stake) and new value
     198             :    for credits_observed were the points paid
     199             : 
     200             :     https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/programs/stake/src/points.rs#L109 */
     201             : static void
     202             : calculate_stake_points_and_credits( fd_funk_t *                    funk,
     203             :                                     fd_funk_txn_xid_t const *      xid,
     204             :                                     fd_stake_history_t const *     stake_history,
     205             :                                     fd_stake_delegation_t const *  stake,
     206             :                                     fd_vote_state_ele_t const *    vote_state,
     207             :                                     ulong *                        new_rate_activation_epoch,
     208           0 :                                     fd_calculated_stake_points_t * result ) {
     209             : 
     210             : 
     211           0 :   fd_txn_account_t vote_rec[1];
     212           0 :   if( FD_UNLIKELY( fd_txn_account_init_from_funk_readonly( vote_rec,
     213           0 :                                                            &vote_state->vote_account,
     214           0 :                                                            funk,
     215           0 :                                                            xid )!=FD_ACC_MGR_SUCCESS ) ) {
     216           0 :     FD_LOG_ERR(( "Unable to read vote account" ));
     217           0 :   }
     218             : 
     219           0 :   fd_vote_epoch_credits_t * epoch_credits = NULL;
     220             : 
     221           0 :   uchar __attribute__((aligned(FD_VOTE_STATE_VERSIONED_ALIGN))) buf[ FD_VOTE_STATE_VERSIONED_FOOTPRINT ];
     222             : 
     223           0 :   get_vote_credits( fd_txn_account_get_data( vote_rec ), fd_txn_account_get_data_len( vote_rec ), buf, &epoch_credits );
     224             : 
     225           0 :   ulong credits_in_stake = stake->credits_observed;
     226           0 :   ulong credits_cnt      = deq_fd_vote_epoch_credits_t_cnt( epoch_credits );
     227           0 :   ulong credits_in_vote  = credits_cnt > 0UL ? deq_fd_vote_epoch_credits_t_peek_tail( epoch_credits )->credits : 0UL;
     228             : 
     229             :   /* If the Vote account has less credits observed than the Stake account,
     230             :       something is wrong and we need to force an update.
     231             : 
     232             :       https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/programs/stake/src/points.rs#L142 */
     233           0 :   if( FD_UNLIKELY( credits_in_vote < credits_in_stake ) ) {
     234           0 :     result->points.ud = 0;
     235           0 :     result->new_credits_observed = credits_in_vote;
     236           0 :     result->force_credits_update_with_skipped_reward = 1;
     237           0 :     return;
     238           0 :   }
     239             : 
     240             :   /* If the Vote account has the same amount of credits observed as the Stake account,
     241             :       then the Vote account hasn't earnt any credits and so there is nothing to update.
     242             : 
     243             :       https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/programs/stake/src/points.rs#L148 */
     244           0 :   if( FD_UNLIKELY( credits_in_vote == credits_in_stake ) ) {
     245           0 :     result->points.ud = 0;
     246           0 :     result->new_credits_observed = credits_in_vote;
     247           0 :     result->force_credits_update_with_skipped_reward = 0;
     248           0 :     return;
     249           0 :   }
     250             : 
     251             :   /* Calculate the points for each epoch credit */
     252           0 :   uint128 points               = 0;
     253           0 :   ulong   new_credits_observed = credits_in_stake;
     254           0 :   for( deq_fd_vote_epoch_credits_t_iter_t iter = deq_fd_vote_epoch_credits_t_iter_init( epoch_credits );
     255           0 :        !deq_fd_vote_epoch_credits_t_iter_done( epoch_credits, iter );
     256           0 :        iter = deq_fd_vote_epoch_credits_t_iter_next( epoch_credits, iter ) ) {
     257             : 
     258           0 :     fd_vote_epoch_credits_t * ele = deq_fd_vote_epoch_credits_t_iter_ele( epoch_credits, iter );
     259             : 
     260           0 :     ulong final_epoch_credits   = ele->credits;
     261           0 :     ulong initial_epoch_credits = ele->prev_credits;
     262           0 :     uint128 earned_credits = 0;
     263           0 :     if( FD_LIKELY( credits_in_stake < initial_epoch_credits ) ) {
     264           0 :       earned_credits = (uint128)(final_epoch_credits - initial_epoch_credits);
     265           0 :     } else if( FD_UNLIKELY( credits_in_stake < final_epoch_credits ) ) {
     266           0 :       earned_credits = (uint128)(final_epoch_credits - new_credits_observed);
     267           0 :     }
     268             : 
     269           0 :     new_credits_observed = fd_ulong_max( new_credits_observed, final_epoch_credits );
     270             : 
     271           0 :     fd_delegation_t delegation = {
     272           0 :       .voter_pubkey         = stake->vote_account,
     273           0 :       .stake                = stake->stake,
     274           0 :       .activation_epoch     = stake->activation_epoch,
     275           0 :       .deactivation_epoch   = stake->deactivation_epoch,
     276           0 :       .warmup_cooldown_rate = stake->warmup_cooldown_rate,
     277           0 :     };
     278             : 
     279           0 :     ulong stake_amount = fd_stake_activating_and_deactivating(
     280           0 :         &delegation,
     281           0 :         ele->epoch,
     282           0 :         stake_history,
     283           0 :         new_rate_activation_epoch ).effective;
     284             : 
     285           0 :     points += (uint128)stake_amount * earned_credits;
     286             : 
     287           0 :   }
     288             : 
     289           0 :   result->points.ud = points;
     290           0 :   result->new_credits_observed = new_credits_observed;
     291           0 :   result->force_credits_update_with_skipped_reward = 0;
     292           0 : }
     293             : 
     294             : struct fd_commission_split {
     295             :   ulong voter_portion;
     296             :   ulong staker_portion;
     297             :   uint  is_split;
     298             : };
     299             : typedef struct fd_commission_split fd_commission_split_t;
     300             : 
     301             : /// returns commission split as (voter_portion, staker_portion, was_split) tuple
     302             : ///
     303             : /// if commission calculation is 100% one way or other, indicate with false for was_split
     304             : 
     305             : // https://github.com/anza-xyz/agave/blob/v2.0.1/sdk/program/src/vote/state/mod.rs#L543
     306             : void
     307             : fd_vote_commission_split( uchar                   commission,
     308             :                           ulong                   on,
     309           0 :                           fd_commission_split_t * result ) {
     310           0 :   uint commission_split = fd_uint_min( (uint)commission, 100 );
     311           0 :   result->is_split      = (commission_split != 0 && commission_split != 100);
     312             :   // https://github.com/anza-xyz/agave/blob/v2.0.1/sdk/program/src/vote/state/mod.rs#L545
     313           0 :   if( commission_split==0U ) {
     314           0 :     result->voter_portion  = 0;
     315           0 :     result->staker_portion = on;
     316           0 :     return;
     317           0 :   }
     318             :   // https://github.com/anza-xyz/agave/blob/v2.0.1/sdk/program/src/vote/state/mod.rs#L546
     319           0 :   if( commission_split==100U ) {
     320           0 :     result->voter_portion  = on;
     321           0 :     result->staker_portion = 0;
     322           0 :     return;
     323           0 :   }
     324             :   /* Note: order of operations may matter for int division. That's why I didn't make the
     325             :    * optimization of getting out the common calculations */
     326             : 
     327             :   // ... This is copied from the solana comments...
     328             :   //
     329             :   // Calculate mine and theirs independently and symmetrically instead
     330             :   // of using the remainder of the other to treat them strictly
     331             :   // equally. This is also to cancel the rewarding if either of the
     332             :   // parties should receive only fractional lamports, resulting in not
     333             :   // being rewarded at all. Thus, note that we intentionally discard
     334             :   // any residual fractional lamports.
     335             : 
     336             :   // https://github.com/anza-xyz/agave/blob/v2.0.1/sdk/program/src/vote/state/mod.rs#L548
     337           0 :   result->voter_portion =
     338           0 :       (ulong)((uint128)on * (uint128)commission_split / (uint128)100);
     339           0 :   result->staker_portion =
     340           0 :       (ulong)((uint128)on * (uint128)( 100 - commission_split ) / (uint128)100);
     341           0 : }
     342             : 
     343             : /* https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/programs/stake/src/rewards.rs#L33 */
     344             : static int
     345             : redeem_rewards( fd_funk_t *                     funk,
     346             :                 fd_funk_txn_xid_t const *       xid,
     347             :                 fd_stake_history_t const *      stake_history,
     348             :                 fd_stake_delegation_t const *   stake,
     349             :                 fd_vote_state_ele_t const *     vote_state,
     350             :                 ulong                           rewarded_epoch,
     351             :                 ulong                           total_rewards,
     352             :                 uint128                         total_points,
     353             :                 ulong *                         new_rate_activation_epoch,
     354             :                 fd_vote_state_credits_t *       recalc_vote_state_credits,
     355           0 :                 fd_calculated_stake_rewards_t * result ) {
     356             : 
     357             :   /* The firedancer implementation of redeem_rewards inlines a lot of
     358             :      the helper functions that the Agave implementation uses.
     359             :      In Agave: redeem_rewards calls redeem_stake_rewards which calls
     360             :      calculate_stake_rewards. */
     361             : 
     362           0 :   fd_calculated_stake_points_t stake_points_result = {0};
     363           0 :   if( FD_LIKELY( !recalc_vote_state_credits ) ) {
     364           0 :     calculate_stake_points_and_credits(
     365           0 :       funk,
     366           0 :       xid,
     367           0 :       stake_history,
     368           0 :       stake,
     369           0 :       vote_state,
     370           0 :       new_rate_activation_epoch,
     371           0 :       &stake_points_result );
     372           0 :   } else {
     373           0 :     calculate_stake_points_and_credits_recalculation(
     374           0 :       stake_history,
     375           0 :       stake,
     376           0 :       new_rate_activation_epoch,
     377           0 :       recalc_vote_state_credits,
     378           0 :       &stake_points_result );
     379           0 :   }
     380             : 
     381             :   // Drive credits_observed forward unconditionally when rewards are disabled
     382             :   // or when this is the stake's activation epoch
     383           0 :   if( total_rewards==0UL || stake->activation_epoch==rewarded_epoch ) {
     384           0 :       stake_points_result.force_credits_update_with_skipped_reward = 1;
     385           0 :   }
     386             : 
     387           0 :   if( stake_points_result.force_credits_update_with_skipped_reward ) {
     388           0 :     result->staker_rewards       = 0;
     389           0 :     result->voter_rewards        = 0;
     390           0 :     result->new_credits_observed = stake_points_result.new_credits_observed;
     391           0 :     return 0;
     392           0 :   }
     393           0 :   if( stake_points_result.points.ud==0 || total_points==0 ) {
     394           0 :     return 1;
     395           0 :   }
     396             : 
     397             :   /* FIXME: need to error out if the conversion from uint128 to u64 fails, also use 128 checked mul and div */
     398           0 :   ulong rewards = (ulong)(stake_points_result.points.ud * (uint128)(total_rewards) / (uint128) total_points);
     399           0 :   if( rewards == 0 ) {
     400           0 :     return 1;
     401           0 :   }
     402             : 
     403           0 :   fd_commission_split_t split_result;
     404           0 :   fd_vote_commission_split( vote_state->commission, rewards, &split_result );
     405           0 :   if( split_result.is_split && (split_result.voter_portion == 0 || split_result.staker_portion == 0) ) {
     406           0 :     return 1;
     407           0 :   }
     408             : 
     409           0 :   result->staker_rewards       = split_result.staker_portion;
     410           0 :   result->voter_rewards        = split_result.voter_portion;
     411           0 :   result->new_credits_observed = stake_points_result.new_credits_observed;
     412           0 :   return 0;
     413           0 : }
     414             : 
     415             : /* Returns the length of the given epoch in slots
     416             : 
     417             :    https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/sdk/program/src/epoch_schedule.rs#L103 */
     418             : static ulong
     419             : get_slots_in_epoch( ulong                       epoch,
     420          30 :                     fd_epoch_schedule_t const * epoch_schedule ) {
     421          30 :   return epoch < epoch_schedule->first_normal_epoch ?
     422           0 :          1UL << fd_ulong_sat_add( epoch, FD_EPOCH_LEN_MIN_TRAILING_ZERO ) :
     423          30 :          epoch_schedule->slots_per_epoch;
     424          30 : }
     425             : 
     426             : /* https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/runtime/src/bank.rs#L2082 */
     427             : static double
     428             : epoch_duration_in_years( fd_bank_t const * bank,
     429           9 :                          ulong             prev_epoch ) {
     430           9 :   ulong slots_in_epoch = get_slots_in_epoch( prev_epoch, fd_bank_epoch_schedule_query( bank ) );
     431           9 :   return (double)slots_in_epoch / (double)fd_bank_slots_per_year_get( bank );
     432           9 : }
     433             : 
     434             : /* https://github.com/anza-xyz/agave/blob/7117ed9653ce19e8b2dea108eff1f3eb6a3378a7/runtime/src/bank.rs#L2128 */
     435             : static void
     436             : calculate_previous_epoch_inflation_rewards( fd_bank_t const *                   bank,
     437             :                                             ulong                               prev_epoch_capitalization,
     438             :                                             ulong                               prev_epoch,
     439           9 :                                             fd_prev_epoch_inflation_rewards_t * rewards ) {
     440           9 :   double slot_in_year = slot_in_year_for_inflation( bank );
     441             : 
     442           9 :   rewards->validator_rate               = validator( fd_bank_inflation_query( bank ), slot_in_year );
     443           9 :   rewards->foundation_rate              = foundation( fd_bank_inflation_query( bank ), slot_in_year );
     444           9 :   rewards->prev_epoch_duration_in_years = epoch_duration_in_years( bank, prev_epoch );
     445           9 :   rewards->validator_rewards            = (ulong)(rewards->validator_rate * (double)prev_epoch_capitalization * rewards->prev_epoch_duration_in_years);
     446           9 :   FD_LOG_DEBUG(( "Rewards %lu, Rate %.16f, Duration %.18f Capitalization %lu Slot in year %.16f", rewards->validator_rewards, rewards->validator_rate, rewards->prev_epoch_duration_in_years, prev_epoch_capitalization, slot_in_year ));
     447           9 : }
     448             : 
     449             : /* https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/programs/stake/src/lib.rs#L29 */
     450             : static ulong
     451          18 : get_minimum_stake_delegation( fd_bank_t * bank ) {
     452          18 :   if( !FD_FEATURE_ACTIVE_BANK( bank, stake_minimum_delegation_for_rewards ) ) {
     453          18 :     return 0UL;
     454          18 :   }
     455             : 
     456           0 :   if( FD_FEATURE_ACTIVE_BANK( bank, stake_raise_minimum_delegation_to_1_sol ) ) {
     457           0 :     return LAMPORTS_PER_SOL;
     458           0 :   }
     459             : 
     460           0 :   return 1;
     461           0 : }
     462             : 
     463             : /* Calculates epoch reward points from stake/vote accounts.
     464             :    https://github.com/anza-xyz/agave/blob/v2.3.1/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L445 */
     465             : static uint128
     466             : calculate_reward_points_partitioned( fd_bank_t *                    bank,
     467             :                                      fd_funk_t *                    funk,
     468             :                                      fd_funk_txn_xid_t const *      xid,
     469             :                                      fd_stake_delegations_t const * stake_delegations,
     470           9 :                                      fd_stake_history_t const *     stake_history ) {
     471           9 :   ulong minimum_stake_delegation = get_minimum_stake_delegation( bank );
     472             : 
     473             :   /* Calculate the points for each stake delegation */
     474           9 :   int _err[1];
     475           9 :   ulong   new_warmup_cooldown_rate_epoch_val = 0UL;
     476           9 :   ulong * new_warmup_cooldown_rate_epoch     = &new_warmup_cooldown_rate_epoch_val;
     477           9 :   int is_some = fd_new_warmup_cooldown_rate_epoch(
     478           9 :       fd_bank_epoch_schedule_query( bank ),
     479           9 :       fd_bank_features_query( bank ),
     480           9 :       fd_bank_slot_get( bank ),
     481           9 :       new_warmup_cooldown_rate_epoch,
     482           9 :       _err );
     483           9 :   if( FD_UNLIKELY( !is_some ) ) {
     484           9 :     new_warmup_cooldown_rate_epoch = NULL;
     485           9 :   }
     486             : 
     487           9 :   uint128 total_points = 0;
     488             : 
     489           9 :   fd_vote_states_t const * vote_states = fd_bank_vote_states_locking_query( bank );
     490             : 
     491           9 :   fd_stake_delegations_iter_t iter_[1];
     492           9 :   for( fd_stake_delegations_iter_t * iter = fd_stake_delegations_iter_init( iter_, stake_delegations );
     493           9 :        !fd_stake_delegations_iter_done( iter );
     494           9 :        fd_stake_delegations_iter_next( iter ) ) {
     495           0 :     fd_stake_delegation_t const * stake_delegation = fd_stake_delegations_iter_ele( iter );
     496             : 
     497           0 :     if( FD_UNLIKELY( stake_delegation->stake<minimum_stake_delegation ) ) {
     498           0 :       continue;
     499           0 :     }
     500             : 
     501           0 :     fd_vote_state_ele_t * vote_state_ele = fd_vote_states_query( vote_states, &stake_delegation->vote_account );
     502           0 :     if( FD_UNLIKELY( !vote_state_ele ) ) {
     503           0 :       continue;
     504           0 :     }
     505             : 
     506           0 :     fd_calculated_stake_points_t stake_point_result;
     507           0 :     calculate_stake_points_and_credits( funk,
     508           0 :                                         xid,
     509           0 :                                         stake_history,
     510           0 :                                         stake_delegation,
     511           0 :                                         vote_state_ele,
     512           0 :                                         new_warmup_cooldown_rate_epoch,
     513           0 :                                         &stake_point_result );
     514           0 :     total_points += stake_point_result.points.ud;
     515           0 :   }
     516             : 
     517           9 :   fd_bank_vote_states_end_locking_query( bank );
     518             : 
     519           9 :   return total_points;
     520           9 : }
     521             : 
     522             : /* Calculates epoch rewards for stake/vote accounts.
     523             :    Returns vote rewards, stake rewards, and the sum of all stake rewards
     524             :    in lamports.
     525             : 
     526             :    In the future, the calculation will be cached in the snapshot, but
     527             :    for now we just re-calculate it (as Agave does).
     528             :    calculate_stake_vote_rewards is responsible for calculating
     529             :    stake account rewards based off of a combination of the
     530             :    stake delegation state as well as the vote account. If this
     531             :    calculation is done at the end of an epoch, we can just use the
     532             :    vote states at the end of the current epoch. However, because we
     533             :    are presumably booting up a node in the middle of rewards
     534             :    distribution, we need to make sure that we are using the vote
     535             :    states from the end of the previous epoch.
     536             : 
     537             :    https://github.com/anza-xyz/agave/blob/v2.3.1/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L323 */
     538             : static void
     539             : calculate_stake_vote_rewards( fd_bank_t *                    bank,
     540             :                               fd_funk_t *                    funk,
     541             :                               fd_funk_txn_xid_t const *      xid,
     542             :                               fd_stake_delegations_t const * stake_delegations,
     543             :                               fd_capture_ctx_t *             capture_ctx FD_PARAM_UNUSED,
     544             :                               fd_stake_history_t const *     stake_history,
     545             :                               ulong                          rewarded_epoch,
     546             :                               ulong                          total_rewards,
     547             :                               uint128                        total_points,
     548           9 :                               fd_runtime_stack_t *           runtime_stack ) {
     549             : 
     550           9 :   int _err[1];
     551           9 :   ulong   new_warmup_cooldown_rate_epoch_val = 0UL;
     552           9 :   ulong * new_warmup_cooldown_rate_epoch     = &new_warmup_cooldown_rate_epoch_val;
     553           9 :   int is_some = fd_new_warmup_cooldown_rate_epoch(
     554           9 :       fd_bank_epoch_schedule_query( bank ),
     555           9 :       fd_bank_features_query( bank ),
     556           9 :       fd_bank_slot_get( bank ),
     557           9 :       new_warmup_cooldown_rate_epoch,
     558           9 :       _err );
     559           9 :   if( FD_UNLIKELY( !is_some ) ) {
     560           9 :     new_warmup_cooldown_rate_epoch = NULL;
     561           9 :   }
     562             : 
     563           9 :   ulong minimum_stake_delegation = get_minimum_stake_delegation( bank );
     564             : 
     565           9 :   fd_vote_states_t * vote_states = !!runtime_stack->stakes.prev_vote_credits_used ? fd_bank_vote_states_prev_modify( bank ) : fd_bank_vote_states_locking_modify( bank );
     566             : 
     567           9 :   fd_epoch_rewards_t * epoch_rewards = fd_bank_epoch_rewards_modify( bank );
     568           9 :   fd_epoch_rewards_init( epoch_rewards );
     569             : 
     570             :   /* Reset the vote rewards for each vote account. */
     571           9 :   fd_memset( runtime_stack->stakes.vote_rewards, 0UL, sizeof(runtime_stack->stakes.vote_rewards) );
     572             : 
     573           9 :   fd_stake_delegations_iter_t iter_[1];
     574           9 :   for( fd_stake_delegations_iter_t * iter = fd_stake_delegations_iter_init( iter_, stake_delegations );
     575           9 :        !fd_stake_delegations_iter_done( iter );
     576           9 :        fd_stake_delegations_iter_next( iter ) ) {
     577           0 :     fd_stake_delegation_t const * stake_delegation = fd_stake_delegations_iter_ele( iter );
     578             : 
     579           0 :     if( FD_FEATURE_ACTIVE_BANK( bank, stake_minimum_delegation_for_rewards ) ) {
     580           0 :       if( stake_delegation->stake<minimum_stake_delegation ) {
     581           0 :         continue;
     582           0 :       }
     583           0 :     }
     584             : 
     585           0 :     fd_pubkey_t const *   voter_acc      = &stake_delegation->vote_account;
     586           0 :     fd_vote_state_ele_t * vote_state_ele = fd_vote_states_query( vote_states, voter_acc );
     587           0 :     if( FD_UNLIKELY( !vote_state_ele ) ) {
     588           0 :       continue;
     589           0 :     }
     590             : 
     591           0 :     fd_vote_state_credits_t * realc_credit = !!runtime_stack->stakes.prev_vote_credits_used ?
     592           0 :                                              &runtime_stack->stakes.vote_credits[ vote_state_ele->idx ] : NULL;
     593             : 
     594             :     /* redeem_rewards is actually just responsible for calculating the
     595             :        vote and stake rewards for each stake account.  It does not do
     596             :        rewards redemption: it is a misnomer. */
     597           0 :     fd_calculated_stake_rewards_t calculated_stake_rewards[1] = {0};
     598           0 :     int err = redeem_rewards(
     599           0 :         funk,
     600           0 :         xid,
     601           0 :         stake_history,
     602           0 :         stake_delegation,
     603           0 :         vote_state_ele,
     604           0 :         rewarded_epoch,
     605           0 :         total_rewards,
     606           0 :         total_points,
     607           0 :         new_warmup_cooldown_rate_epoch,
     608           0 :         realc_credit,
     609           0 :         calculated_stake_rewards );
     610             : 
     611           0 :     if( FD_UNLIKELY( err!=0 ) ) {
     612           0 :       continue;
     613           0 :     }
     614             : 
     615           0 :     if ( capture_ctx && capture_ctx->capture ) {
     616           0 :       fd_capture_link_write_stake_reward_event( capture_ctx,
     617           0 :                                                 fd_bank_slot_get( bank ),
     618           0 :                                                 stake_delegation->stake_account,
     619           0 :                                                 *voter_acc,
     620           0 :                                                 vote_state_ele->commission,
     621           0 :                                                 (long)calculated_stake_rewards->voter_rewards,
     622           0 :                                                 (long)calculated_stake_rewards->staker_rewards,
     623           0 :                                                 (long)calculated_stake_rewards->new_credits_observed );
     624           0 :     }
     625             : 
     626           0 :     runtime_stack->stakes.vote_rewards[ vote_state_ele->idx ] += calculated_stake_rewards->voter_rewards;
     627             : 
     628           0 :     fd_epoch_rewards_insert( epoch_rewards, &stake_delegation->stake_account, calculated_stake_rewards->new_credits_observed, calculated_stake_rewards->staker_rewards );
     629           0 :   }
     630             : 
     631           9 :   if( !runtime_stack->stakes.prev_vote_credits_used ) fd_bank_vote_states_end_locking_modify( bank );
     632           9 : }
     633             : 
     634             : /* Calculate epoch reward and return vote and stake rewards.
     635             : 
     636             :    https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L273 */
     637             : static uint128
     638             : calculate_validator_rewards( fd_bank_t *                    bank,
     639             :                              fd_funk_t *                    funk,
     640             :                              fd_funk_txn_xid_t const *      xid,
     641             :                              fd_runtime_stack_t *           runtime_stack,
     642             :                              fd_stake_delegations_t const * stake_delegations,
     643             :                              fd_capture_ctx_t *             capture_ctx,
     644             :                              ulong                          rewarded_epoch,
     645           9 :                              ulong *                        rewards_out ) {
     646             : 
     647           9 :   fd_stake_history_t stake_history[1];
     648           9 :   if( FD_UNLIKELY( !fd_sysvar_stake_history_read( funk, xid, stake_history ) ) ) {
     649           0 :     FD_LOG_ERR(( "Unable to read and decode stake history sysvar" ));
     650           0 :   }
     651             : 
     652             :   /* Calculate the epoch reward points from stake/vote accounts */
     653           9 :   uint128 points = calculate_reward_points_partitioned(
     654           9 :       bank,
     655           9 :       funk,
     656           9 :       xid,
     657           9 :       stake_delegations,
     658           9 :       stake_history );
     659             : 
     660             :   /* If there are no points, then we set the rewards to 0. */
     661           9 :   *rewards_out = points>0UL ? *rewards_out: 0UL;
     662             : 
     663           9 :   if( capture_ctx  && capture_ctx->capture ) {
     664           0 :     ulong epoch = fd_bank_epoch_get( bank );
     665           0 :     ulong slot  = fd_bank_slot_get( bank );
     666           0 :     fd_capture_link_write_stake_rewards_begin( capture_ctx,
     667           0 :                                                slot,
     668           0 :                                                epoch,
     669           0 :                                                epoch-1UL, /* FIXME: this is not strictly correct */
     670           0 :                                                *rewards_out,
     671           0 :                                                (ulong)points );
     672           0 :   }
     673             : 
     674             :   /* Calculate the stake and vote rewards for each account. We want to
     675             :      use the vote states from the end of the current_epoch. */
     676           9 :   calculate_stake_vote_rewards(
     677           9 :       bank,
     678           9 :       funk,
     679           9 :       xid,
     680           9 :       stake_delegations,
     681           9 :       capture_ctx,
     682           9 :       stake_history,
     683           9 :       rewarded_epoch,
     684           9 :       *rewards_out,
     685           9 :       points,
     686           9 :       runtime_stack );
     687             : 
     688           9 :   return points;
     689           9 : }
     690             : 
     691             : /* Calculate the number of blocks required to distribute rewards to all stake accounts.
     692             : 
     693             :     https://github.com/anza-xyz/agave/blob/9a7bf72940f4b3cd7fc94f54e005868ce707d53d/runtime/src/bank/partitioned_epoch_rewards/mod.rs#L214
     694             :  */
     695             : static ulong
     696             : get_reward_distribution_num_blocks( fd_epoch_schedule_t const * epoch_schedule,
     697             :                                     ulong                       slot,
     698           9 :                                     ulong                       total_stake_accounts ) {
     699             :   /* https://github.com/firedancer-io/solana/blob/dab3da8e7b667d7527565bddbdbecf7ec1fb868e/runtime/src/bank.rs#L1250-L1267 */
     700           9 :   if( epoch_schedule->warmup &&
     701           9 :       fd_slot_to_epoch( epoch_schedule, slot, NULL ) < epoch_schedule->first_normal_epoch ) {
     702           0 :     return 1UL;
     703           0 :   }
     704             : 
     705           9 :   ulong num_chunks = total_stake_accounts / (ulong)STAKE_ACCOUNT_STORES_PER_BLOCK + (total_stake_accounts % STAKE_ACCOUNT_STORES_PER_BLOCK != 0);
     706           9 :   num_chunks       = fd_ulong_max( num_chunks, 1UL );
     707           9 :   num_chunks       = fd_ulong_min( num_chunks,
     708           9 :                                    fd_ulong_max( epoch_schedule->slots_per_epoch / (ulong)MAX_FACTOR_OF_REWARD_BLOCKS_IN_EPOCH, 1UL ) );
     709           9 :   return num_chunks;
     710           9 : }
     711             : 
     712             : /* Calculate rewards from previous epoch to prepare for partitioned distribution.
     713             : 
     714             :    https://github.com/anza-xyz/agave/blob/v3.0.4/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L277 */
     715             : static void
     716             : calculate_rewards_for_partitioning( fd_bank_t *                            bank,
     717             :                                     fd_funk_t *                            funk,
     718             :                                     fd_funk_txn_xid_t const *              xid,
     719             :                                     fd_runtime_stack_t *                   runtime_stack,
     720             :                                     fd_stake_delegations_t const *         stake_delegations,
     721             :                                     fd_capture_ctx_t *                     capture_ctx,
     722             :                                     ulong                                  prev_epoch,
     723           9 :                                     fd_partitioned_rewards_calculation_t * result ) {
     724           9 :   fd_prev_epoch_inflation_rewards_t rewards;
     725             : 
     726           9 :   calculate_previous_epoch_inflation_rewards( bank,
     727           9 :                                               fd_bank_capitalization_get( bank ),
     728           9 :                                               prev_epoch,
     729           9 :                                               &rewards );
     730             : 
     731           9 :   ulong total_rewards = rewards.validator_rewards;
     732             : 
     733           9 :   uint128 points = calculate_validator_rewards( bank,
     734           9 :                                                 funk,
     735           9 :                                                 xid,
     736           9 :                                                 runtime_stack,
     737           9 :                                                 stake_delegations,
     738           9 :                                                 capture_ctx,
     739           9 :                                                 prev_epoch,
     740           9 :                                                 &total_rewards );
     741             : 
     742             :   /* The agave client does not partition the stake rewards until the
     743             :      first distribution block.  We calculate the partitions during the
     744             :      boundary. */
     745           9 :   result->validator_points             = points;
     746           9 :   result->validator_rewards            = total_rewards;
     747           9 :   result->validator_rate               = rewards.validator_rate;
     748           9 :   result->foundation_rate              = rewards.foundation_rate;
     749           9 :   result->prev_epoch_duration_in_years = rewards.prev_epoch_duration_in_years;
     750           9 :   result->capitalization               = fd_bank_capitalization_get( bank );
     751           9 : }
     752             : 
     753             : /* Calculate rewards from previous epoch and distribute vote rewards
     754             :    https://github.com/anza-xyz/agave/blob/v3.0.4/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L148 */
     755             : static void
     756             : calculate_rewards_and_distribute_vote_rewards( fd_bank_t *                    bank,
     757             :                                                fd_accdb_user_t *              accdb,
     758             :                                                fd_funk_txn_xid_t const *      xid,
     759             :                                                fd_runtime_stack_t *           runtime_stack,
     760             :                                                fd_stake_delegations_t const * stake_delegations,
     761             :                                                fd_capture_ctx_t *             capture_ctx,
     762           9 :                                                ulong                          prev_epoch ) {
     763             : 
     764             :   /* First we must compute the stake and vote rewards for the just
     765             :      completed epoch.  We store the stake account rewards and vote
     766             :      states rewards in the bank */
     767             : 
     768           9 :   fd_funk_t * funk = fd_accdb_user_v1_funk( accdb );
     769           9 :   fd_partitioned_rewards_calculation_t rewards_calc_result[1] = {0};
     770           9 :   calculate_rewards_for_partitioning( bank,
     771           9 :                                       funk,
     772           9 :                                       xid,
     773           9 :                                       runtime_stack,
     774           9 :                                       stake_delegations,
     775           9 :                                       capture_ctx,
     776           9 :                                       prev_epoch,
     777           9 :                                       rewards_calc_result );
     778             : 
     779           9 :   fd_vote_states_t const * vote_states = fd_bank_vote_states_locking_query( bank );
     780             : 
     781             :   /* Iterate over all the vote reward nodes and distribute the rewards
     782             :      to the vote accounts.  After each reward has been paid out,
     783             :      calcualte the lthash for each vote account. */
     784           9 :   ulong distributed_rewards = 0UL;
     785           9 :   fd_vote_states_iter_t iter_[1];
     786           9 :   for( fd_vote_states_iter_t * iter = fd_vote_states_iter_init( iter_, vote_states );
     787           9 :        !fd_vote_states_iter_done( iter );
     788           9 :        fd_vote_states_iter_next( iter ) ) {
     789           0 :     fd_vote_state_ele_t * vote_state = fd_vote_states_iter_ele( iter );
     790             : 
     791           0 :     ulong rewards = runtime_stack->stakes.vote_rewards[ vote_state->idx ];
     792           0 :     if( rewards==0UL ) {
     793           0 :       continue;
     794           0 :     }
     795             : 
     796           0 :     fd_pubkey_t const * vote_pubkey = &vote_state->vote_account;
     797           0 :     fd_txn_account_t vote_rec[1];
     798           0 :     fd_funk_rec_prepare_t prepare = {0};
     799             : 
     800           0 :     if( FD_UNLIKELY( !fd_txn_account_init_from_funk_mutable(
     801           0 :         vote_rec,
     802           0 :         vote_pubkey,
     803           0 :         accdb,
     804           0 :         xid,
     805           0 :         1,
     806           0 :         0UL,
     807           0 :         &prepare
     808           0 :     ) ) ) {
     809           0 :       FD_LOG_ERR(( "Unable to modify vote account" ));
     810           0 :     }
     811             : 
     812           0 :     fd_lthash_value_t prev_hash[1];
     813           0 :     fd_hashes_account_lthash(
     814           0 :       vote_pubkey,
     815           0 :       fd_txn_account_get_meta( vote_rec ),
     816           0 :       fd_txn_account_get_data( vote_rec ),
     817           0 :       prev_hash );
     818             : 
     819           0 :     fd_txn_account_set_slot( vote_rec, fd_bank_slot_get( bank ) );
     820             : 
     821           0 :     if( FD_UNLIKELY( fd_txn_account_checked_add_lamports( vote_rec, rewards ) ) ) {
     822           0 :       FD_LOG_ERR(( "Adding lamports to vote account would cause overflow" ));
     823           0 :     }
     824             : 
     825           0 :     fd_hashes_update_lthash( vote_rec->pubkey, vote_rec->meta, prev_hash,bank, capture_ctx );
     826           0 :     fd_txn_account_mutable_fini( vote_rec, accdb, &prepare );
     827             : 
     828           0 :     distributed_rewards = fd_ulong_sat_add( distributed_rewards, rewards );
     829           0 :   }
     830             : 
     831           9 :   fd_bank_vote_states_end_locking_query( bank );
     832             : 
     833             :   /* Verify that we didn't pay any more than we expected to */
     834           9 :   fd_epoch_rewards_t * epoch_rewards = fd_bank_epoch_rewards_modify( bank );
     835             : 
     836           9 :   ulong total_rewards = fd_ulong_sat_add( distributed_rewards, epoch_rewards->total_stake_rewards );
     837           9 :   if( FD_UNLIKELY( rewards_calc_result->validator_rewards<total_rewards ) ) {
     838           0 :     FD_LOG_CRIT(( "Unexpected rewards calculation result" ));
     839           0 :   }
     840             : 
     841           9 :   fd_bank_capitalization_set( bank, fd_bank_capitalization_get( bank ) + distributed_rewards );
     842             : 
     843           9 :   epoch_rewards->distributed_rewards = distributed_rewards;
     844           9 :   epoch_rewards->total_rewards       = rewards_calc_result->validator_rewards;
     845           9 :   epoch_rewards->total_points.ud     = rewards_calc_result->validator_points;
     846           9 : }
     847             : 
     848             : /* Distributes a single partitioned reward to a single stake account */
     849             : static int
     850             : distribute_epoch_reward_to_stake_acc( fd_bank_t *               bank,
     851             :                                       fd_accdb_user_t *         accdb,
     852             :                                       fd_funk_txn_xid_t const * xid,
     853             :                                       fd_capture_ctx_t *        capture_ctx,
     854             :                                       fd_pubkey_t *             stake_pubkey,
     855             :                                       ulong                     reward_lamports,
     856           0 :                                       ulong                     new_credits_observed ) {
     857           0 :   fd_txn_account_t stake_acc_rec[1];
     858           0 :   fd_funk_rec_prepare_t prepare = {0};
     859           0 :   if( FD_UNLIKELY( !fd_txn_account_init_from_funk_mutable( stake_acc_rec,
     860           0 :                                                           stake_pubkey,
     861           0 :                                                           accdb,
     862           0 :                                                           xid,
     863           0 :                                                           0,
     864           0 :                                                           0UL,
     865           0 :                                                           &prepare ) ) ) {
     866           0 :     FD_LOG_ERR(( "Unable to modify stake account" ));
     867           0 :   }
     868             : 
     869           0 :   fd_lthash_value_t prev_hash[1];
     870           0 :   fd_hashes_account_lthash(
     871           0 :     stake_pubkey,
     872           0 :     fd_txn_account_get_meta( stake_acc_rec ),
     873           0 :     fd_txn_account_get_data( stake_acc_rec ),
     874           0 :     prev_hash );
     875             : 
     876           0 :   fd_txn_account_set_slot( stake_acc_rec, fd_bank_slot_get( bank ) );
     877             : 
     878           0 :   fd_stake_state_v2_t stake_state[1] = {0};
     879           0 :   if( fd_stake_get_state( stake_acc_rec->meta, stake_state ) != 0 ) {
     880           0 :     FD_BASE58_ENCODE_32_BYTES( stake_pubkey->key, stake_pubkey_b58 );
     881           0 :     FD_LOG_DEBUG(( "failed to read stake state for %s", stake_pubkey_b58 ));
     882           0 :     return 1;
     883           0 :   }
     884             : 
     885           0 :   if ( !fd_stake_state_v2_is_stake( stake_state ) ) {
     886           0 :     FD_LOG_DEBUG(( "non-stake stake account, this should never happen" ));
     887           0 :     return 1;
     888           0 :   }
     889             : 
     890           0 :   if( fd_txn_account_checked_add_lamports( stake_acc_rec, reward_lamports ) ) {
     891           0 :     FD_LOG_DEBUG(( "failed to add lamports to stake account" ));
     892           0 :     return 1;
     893           0 :   }
     894             : 
     895           0 :   ulong old_credits_observed                      = stake_state->inner.stake.stake.credits_observed;
     896           0 :   stake_state->inner.stake.stake.credits_observed = new_credits_observed;
     897           0 :   stake_state->inner.stake.stake.delegation.stake = fd_ulong_sat_add( stake_state->inner.stake.stake.delegation.stake,
     898           0 :                                                                       reward_lamports );
     899             : 
     900             :   /* The stake account has just been updated, so we need to update the
     901             :      stake delegations stored in the bank. */
     902           0 :   fd_stake_delegations_t * stake_delegations = fd_bank_stake_delegations_delta_locking_modify( bank );
     903           0 :   fd_stake_delegations_update(
     904           0 :       stake_delegations,
     905           0 :       stake_pubkey,
     906           0 :       &stake_state->inner.stake.stake.delegation.voter_pubkey,
     907           0 :       stake_state->inner.stake.stake.delegation.stake,
     908           0 :       stake_state->inner.stake.stake.delegation.activation_epoch,
     909           0 :       stake_state->inner.stake.stake.delegation.deactivation_epoch,
     910           0 :       stake_state->inner.stake.stake.credits_observed,
     911           0 :       stake_state->inner.stake.stake.delegation.warmup_cooldown_rate );
     912           0 :   fd_bank_stake_delegations_delta_end_locking_modify( bank );
     913             : 
     914           0 :   if ( capture_ctx && capture_ctx->capture ) {
     915           0 :     fd_capture_link_write_stake_account_payout( capture_ctx,
     916           0 :                                                     fd_bank_slot_get( bank ),
     917           0 :                                                     *stake_pubkey,
     918           0 :                                                     fd_bank_slot_get( bank ),
     919           0 :                                                     fd_txn_account_get_lamports( stake_acc_rec ),
     920           0 :                                                     (long)reward_lamports,
     921           0 :                                                     new_credits_observed,
     922           0 :                                                     (long)( new_credits_observed - old_credits_observed ),
     923           0 :                                                     stake_state->inner.stake.stake.delegation.stake,
     924           0 :                                                     (long)reward_lamports );
     925           0 :   }
     926             : 
     927           0 :   if( FD_UNLIKELY( write_stake_state( stake_acc_rec, stake_state ) != 0 ) ) {
     928           0 :     FD_LOG_ERR(( "write_stake_state failed" ));
     929           0 :   }
     930             : 
     931           0 :   fd_hashes_update_lthash( stake_acc_rec->pubkey, stake_acc_rec->meta, prev_hash, bank, capture_ctx );
     932           0 :   fd_txn_account_mutable_fini( stake_acc_rec, accdb, &prepare );
     933             : 
     934           0 :   return 0;
     935           0 : }
     936             : 
     937             : /* Process reward credits for a partition of rewards.  Store the rewards
     938             :    to AccountsDB, update reward history record and total capitalization
     939             :    https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/runtime/src/bank/partitioned_epoch_rewards/distribution.rs#L88 */
     940             : static void
     941             : distribute_epoch_rewards_in_partition( fd_epoch_rewards_t const * epoch_rewards,
     942             :                                        ulong                      partition_idx,
     943             :                                        fd_bank_t *                bank,
     944             :                                        fd_accdb_user_t *          accdb,
     945             :                                        fd_funk_txn_xid_t const *  xid,
     946           0 :                                        fd_capture_ctx_t *         capture_ctx ) {
     947             : 
     948           0 :   ulong lamports_distributed = 0UL;
     949           0 :   ulong lamports_burned      = 0UL;
     950             : 
     951           0 :   fd_epoch_rewards_iter_t iter_[1];
     952           0 :   for( fd_epoch_rewards_iter_t * iter = fd_epoch_rewards_iter_init( iter_, epoch_rewards, partition_idx );
     953           0 :        !fd_epoch_rewards_iter_done( iter );
     954           0 :        fd_epoch_rewards_iter_next( iter ) ) {
     955           0 :     fd_epoch_stake_reward_t * stake_reward = fd_epoch_rewards_iter_ele( iter );
     956             : 
     957           0 :     if( FD_LIKELY( !distribute_epoch_reward_to_stake_acc( bank,
     958           0 :                                                           accdb,
     959           0 :                                                           xid,
     960           0 :                                                           capture_ctx,
     961           0 :                                                           &stake_reward->stake_pubkey,
     962           0 :                                                           stake_reward->lamports,
     963           0 :                                                           stake_reward->credits_observed ) )  ) {
     964           0 :       lamports_distributed += stake_reward->lamports;
     965           0 :     } else {
     966           0 :       lamports_burned += stake_reward->lamports;
     967           0 :     }
     968           0 :   }
     969             : 
     970             :   /* Update the epoch rewards sysvar with the amount distributed and burnt */
     971           0 :   fd_sysvar_epoch_rewards_distribute( bank, accdb, xid, capture_ctx, lamports_distributed + lamports_burned );
     972             : 
     973           0 :   FD_LOG_DEBUG(( "lamports burned: %lu, lamports distributed: %lu", lamports_burned, lamports_distributed ));
     974             : 
     975           0 :   fd_bank_capitalization_set( bank, fd_bank_capitalization_get( bank ) + lamports_distributed );
     976           0 : }
     977             : 
     978             : /* Process reward distribution for the block if it is inside reward interval.
     979             : 
     980             :    https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/runtime/src/bank/partitioned_epoch_rewards/distribution.rs#L42 */
     981             : void
     982             : fd_distribute_partitioned_epoch_rewards( fd_bank_t *               bank,
     983             :                                          fd_accdb_user_t *         accdb,
     984             :                                          fd_funk_txn_xid_t const * xid,
     985          27 :                                          fd_capture_ctx_t *        capture_ctx ) {
     986             : 
     987          27 :   fd_epoch_rewards_t const * epoch_rewards = fd_bank_epoch_rewards_query( bank );
     988          27 :   if( FD_LIKELY( !epoch_rewards ) ) {
     989           6 :     return;
     990           6 :   }
     991             : 
     992          21 :   ulong block_height = fd_bank_block_height_get( bank );
     993          21 :   ulong distribution_starting_block_height = epoch_rewards->starting_block_height;
     994          21 :   ulong distribution_end_exclusive         = fd_epoch_rewards_get_exclusive_ending_block_height( epoch_rewards );
     995             : 
     996          21 :   fd_epoch_schedule_t const * epoch_schedule = fd_bank_epoch_schedule_query( bank );
     997          21 :   ulong                       epoch          = fd_bank_epoch_get( bank );
     998             : 
     999          21 :   if( FD_UNLIKELY( get_slots_in_epoch( epoch, epoch_schedule ) <= epoch_rewards->num_partitions ) ) {
    1000           0 :     FD_LOG_CRIT(( "Should not be distributing rewards" ));
    1001           0 :   }
    1002             : 
    1003          21 :   if( FD_UNLIKELY( block_height>=distribution_starting_block_height && block_height<distribution_end_exclusive ) ) {
    1004             : 
    1005           0 :     ulong partition_idx = block_height-distribution_starting_block_height;
    1006           0 :     distribute_epoch_rewards_in_partition( epoch_rewards, partition_idx, bank, accdb, xid, capture_ctx );
    1007             : 
    1008             :     /* If we have finished distributing rewards, set the status to inactive */
    1009           0 :     if( fd_ulong_sat_add( block_height, 1UL )>=distribution_end_exclusive ) {
    1010           0 :       fd_sysvar_epoch_rewards_set_inactive( bank, accdb, xid, capture_ctx );
    1011           0 :     }
    1012           0 :   }
    1013          21 : }
    1014             : 
    1015             : /* Partitioned epoch rewards entry-point.
    1016             : 
    1017             :    https://github.com/anza-xyz/agave/blob/v3.0.4/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L102
    1018             : */
    1019             : void
    1020             : fd_begin_partitioned_rewards( fd_bank_t *                    bank,
    1021             :                               fd_accdb_user_t *              accdb,
    1022             :                               fd_funk_txn_xid_t const *      xid,
    1023             :                               fd_runtime_stack_t *           runtime_stack,
    1024             :                               fd_capture_ctx_t *             capture_ctx,
    1025             :                               fd_stake_delegations_t const * stake_delegations,
    1026             :                               fd_hash_t const *              parent_blockhash,
    1027           9 :                               ulong                          parent_epoch ) {
    1028             : 
    1029           9 :   calculate_rewards_and_distribute_vote_rewards(
    1030           9 :       bank,
    1031           9 :       accdb,
    1032           9 :       xid,
    1033           9 :       runtime_stack,
    1034           9 :       stake_delegations,
    1035           9 :       capture_ctx,
    1036           9 :       parent_epoch );
    1037             : 
    1038             :   /* Once the rewards for vote accounts have been distributed and stake
    1039             :      account rewards have been calculated, we can now set our epoch
    1040             :      reward status to be active and we can initialize the epoch rewards
    1041             :      sysvar.  This sysvar is then deleted once all of the partitioned
    1042             :      stake rewards have been distributed.
    1043             : 
    1044             :      The Agave client calculates the partitions for each stake reward
    1045             :      when the first distribution block is reached.  The Firedancer
    1046             :      client differs here since we hash the partitions during the epoch
    1047             :      boundary. */
    1048             : 
    1049           9 :   fd_epoch_schedule_t const * epoch_schedule = fd_bank_epoch_schedule_query( bank );
    1050           9 :   fd_epoch_rewards_t *        epoch_rewards  = fd_epoch_rewards_join( fd_bank_epoch_rewards_modify( bank ) );
    1051             : 
    1052           9 :   ulong num_partitions = get_reward_distribution_num_blocks(
    1053           9 :       epoch_schedule,
    1054           9 :       fd_bank_slot_get( bank ),
    1055           9 :       epoch_rewards->stake_rewards_cnt );
    1056             : 
    1057           9 :   fd_epoch_rewards_hash_into_partitions( epoch_rewards, parent_blockhash, num_partitions );
    1058             : 
    1059           9 :   ulong distribution_starting_block_height = fd_bank_block_height_get( bank ) + REWARD_CALCULATION_NUM_BLOCKS;
    1060             : 
    1061           9 :   epoch_rewards->starting_block_height = distribution_starting_block_height;
    1062             : 
    1063           9 :   fd_sysvar_epoch_rewards_init(
    1064           9 :       bank,
    1065           9 :       accdb,
    1066           9 :       xid,
    1067           9 :       capture_ctx,
    1068           9 :       epoch_rewards->distributed_rewards,
    1069           9 :       distribution_starting_block_height,
    1070           9 :       epoch_rewards->num_partitions,
    1071           9 :       epoch_rewards->total_rewards,
    1072           9 :       epoch_rewards->total_points.ud,
    1073           9 :       parent_blockhash );
    1074           9 : }
    1075             : 
    1076             : /*
    1077             :     Re-calculates partitioned stake rewards.
    1078             :     This updates the slot context's epoch reward status with the recalculated partitioned rewards.
    1079             : 
    1080             :     https://github.com/anza-xyz/agave/blob/v2.2.14/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L521 */
    1081             : void
    1082             : fd_rewards_recalculate_partitioned_rewards( fd_banks_t *              banks,
    1083             :                                             fd_bank_t *               bank,
    1084             :                                             fd_funk_t *               funk,
    1085             :                                             fd_funk_txn_xid_t const * xid,
    1086             :                                             fd_runtime_stack_t *      runtime_stack,
    1087           0 :                                             fd_capture_ctx_t *        capture_ctx ) {
    1088             : 
    1089           0 :   fd_sysvar_epoch_rewards_t epoch_rewards_sysvar[1];
    1090           0 :   if( FD_UNLIKELY( !fd_sysvar_epoch_rewards_read( funk, xid, epoch_rewards_sysvar ) ) ) {
    1091           0 :     FD_LOG_DEBUG(( "Failed to read or decode epoch rewards sysvar - may not have been created yet" ));
    1092           0 :     return;
    1093           0 :   }
    1094             : 
    1095           0 :   FD_LOG_DEBUG(( "recalculating partitioned rewards" ));
    1096             : 
    1097           0 :   if( FD_UNLIKELY( !epoch_rewards_sysvar->active ) ) {
    1098           0 :     FD_LOG_DEBUG(( "epoch rewards is inactive" ));
    1099           0 :     return;
    1100           0 :   }
    1101             : 
    1102             :   /* If partitioned rewards are active, the rewarded epoch is always the immediately
    1103             :       preceeding epoch.
    1104             : 
    1105             :       https://github.com/anza-xyz/agave/blob/2316fea4c0852e59c071f72d72db020017ffd7d0/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L566 */
    1106           0 :   FD_LOG_DEBUG(( "epoch rewards is active" ));
    1107           0 :   runtime_stack->stakes.prev_vote_credits_used = 1;
    1108             : 
    1109           0 :   ulong const slot           = fd_bank_slot_get( bank );
    1110           0 :   ulong const epoch          = fd_bank_epoch_get( bank );
    1111           0 :   ulong const rewarded_epoch = fd_ulong_sat_sub( epoch, 1UL );
    1112             : 
    1113           0 :   int _err[1] = {0};
    1114           0 :   ulong new_warmup_cooldown_rate_epoch_;
    1115           0 :   ulong * new_warmup_cooldown_rate_epoch = &new_warmup_cooldown_rate_epoch_;
    1116           0 :   int is_some = fd_new_warmup_cooldown_rate_epoch(
    1117           0 :       fd_bank_epoch_schedule_query( bank ),
    1118           0 :       fd_bank_features_query( bank ),
    1119           0 :       slot,
    1120           0 :       new_warmup_cooldown_rate_epoch,
    1121           0 :       _err );
    1122           0 :   if( FD_UNLIKELY( !is_some ) ) {
    1123           0 :     new_warmup_cooldown_rate_epoch = NULL;
    1124           0 :   }
    1125             : 
    1126           0 :   fd_stake_history_t stake_history[1];
    1127           0 :   if( FD_UNLIKELY( !fd_sysvar_stake_history_read( funk, xid, stake_history ) ) ) {
    1128           0 :     FD_LOG_ERR(( "Unable to read and decode stake history sysvar" ));
    1129           0 :   }
    1130             : 
    1131           0 :   fd_stake_delegations_t const * stake_delegations = fd_bank_stake_delegations_frontier_query( banks, bank );
    1132           0 :   if( FD_UNLIKELY( !stake_delegations ) ) {
    1133           0 :     FD_LOG_CRIT(( "stake_delegations is NULL" ));
    1134           0 :   }
    1135             : 
    1136             :   /* Make sure is_recalculation is ==1 since we are booting up in the
    1137             :       middle of rewards distribution (so we should use the epoch
    1138             :       stakes for the end of epoch E-1 since we are still distributing
    1139             :       rewards for the previous epoch). */
    1140           0 :   calculate_stake_vote_rewards(
    1141           0 :       bank,
    1142           0 :       funk,
    1143           0 :       xid,
    1144           0 :       stake_delegations,
    1145           0 :       capture_ctx,
    1146           0 :       stake_history,
    1147           0 :       rewarded_epoch,
    1148           0 :       epoch_rewards_sysvar->total_rewards,
    1149           0 :       epoch_rewards_sysvar->total_points.ud,
    1150           0 :       runtime_stack );
    1151             : 
    1152           0 :   fd_epoch_rewards_t * epoch_rewards = fd_epoch_rewards_join( fd_bank_epoch_rewards_modify( bank ) );
    1153           0 :   fd_epoch_rewards_hash_into_partitions( epoch_rewards, &epoch_rewards_sysvar->parent_blockhash, epoch_rewards_sysvar->num_partitions );
    1154             : 
    1155             :   /* Update the epoch reward status with the newly re-calculated partitions. */
    1156           0 :   epoch_rewards->starting_block_height = epoch_rewards_sysvar->distribution_starting_block_height;
    1157             : 
    1158           0 :   runtime_stack->stakes.prev_vote_credits_used = 0;
    1159           0 : }

Generated by: LCOV version 1.14