Line data Source code
1 : #include "fd_rewards.h"
2 : #include <math.h>
3 :
4 : #include "../runtime/fd_executor_err.h"
5 : #include "../runtime/fd_system_ids.h"
6 : #include "../runtime/context/fd_exec_epoch_ctx.h"
7 : #include "../runtime/context/fd_exec_slot_ctx.h"
8 : #include "../../ballet/siphash13/fd_siphash13.h"
9 : #include "../runtime/program/fd_program_util.h"
10 :
11 : /* https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/sdk/program/src/native_token.rs#L6 */
12 0 : #define LAMPORTS_PER_SOL ( 1000000000UL )
13 :
14 : /* Number of blocks for reward calculation and storing vote accounts.
15 : Distributing rewards to stake accounts begins AFTER this many blocks.
16 :
17 : https://github.com/anza-xyz/agave/blob/9a7bf72940f4b3cd7fc94f54e005868ce707d53d/runtime/src/bank/partitioned_epoch_rewards/mod.rs#L27 */
18 0 : #define REWARD_CALCULATION_NUM_BLOCKS ( 1UL )
19 :
20 : /* stake accounts to store in one block during partitioned reward interval. Target to store 64 rewards per entry/tick in a block. A block has a minimum of 64 entries/tick. This gives 4096 total rewards to store in one block. */
21 0 : #define STAKE_ACCOUNT_STORES_PER_BLOCK ( 4096UL )
22 :
23 : /* https://github.com/anza-xyz/agave/blob/2316fea4c0852e59c071f72d72db020017ffd7d0/runtime/src/bank/partitioned_epoch_rewards/mod.rs#L219 */
24 0 : #define MAX_FACTOR_OF_REWARD_BLOCKS_IN_EPOCH ( 10UL )
25 :
26 : /* https://github.com/anza-xyz/agave/blob/7117ed9653ce19e8b2dea108eff1f3eb6a3378a7/sdk/src/inflation.rs#L85 */
27 : static double
28 0 : total( fd_inflation_t const * inflation, double year ) {
29 0 : if ( FD_UNLIKELY( year == 0.0 ) ) {
30 0 : FD_LOG_ERR(( "inflation year 0" ));
31 0 : }
32 0 : double tapered = inflation->initial * pow((1.0 - inflation->taper), year);
33 0 : return (tapered > inflation->terminal) ? tapered : inflation->terminal;
34 0 : }
35 :
36 : /* https://github.com/anza-xyz/agave/blob/7117ed9653ce19e8b2dea108eff1f3eb6a3378a7/sdk/src/inflation.rs#L102 */
37 : static double
38 0 : foundation( fd_inflation_t const * inflation, double year ) {
39 0 : return (year < inflation->foundation_term) ? inflation->foundation * total(inflation, year) : 0.0;
40 0 : }
41 :
42 : /* https://github.com/anza-xyz/agave/blob/7117ed9653ce19e8b2dea108eff1f3eb6a3378a7/sdk/src/inflation.rs#L97 */
43 : static double
44 0 : validator( fd_inflation_t const * inflation, double year) {
45 : /* https://github.com/firedancer-io/solana/blob/dab3da8e7b667d7527565bddbdbecf7ec1fb868e/sdk/src/inflation.rs#L96-L99 */
46 0 : FD_LOG_DEBUG(("Validator Rate: %.16f %.16f %.16f %.16f %.16f", year, total( inflation, year ), foundation( inflation, year ), inflation->taper, inflation->initial));
47 0 : return total( inflation, year ) - foundation( inflation, year );
48 0 : }
49 :
50 : /* Calculates the starting slot for inflation from the activation slot. The activation slot is the earliest
51 : activation slot of the following features:
52 : - devnet_and_testnet
53 : - full_inflation_enable, if full_inflation_vote has been activated
54 :
55 : https://github.com/anza-xyz/agave/blob/7117ed9653ce19e8b2dea108eff1f3eb6a3378a7/runtime/src/bank.rs#L2095 */
56 : static FD_FN_CONST ulong
57 0 : get_inflation_start_slot( fd_exec_slot_ctx_t * slot_ctx ) {
58 0 : ulong devnet_and_testnet = FD_FEATURE_ACTIVE(slot_ctx, devnet_and_testnet) ? slot_ctx->epoch_ctx->features.devnet_and_testnet : ULONG_MAX;
59 :
60 0 : ulong enable = ULONG_MAX;
61 0 : if ( FD_FEATURE_ACTIVE( slot_ctx, full_inflation_vote ) && FD_FEATURE_ACTIVE(slot_ctx, full_inflation_enable ) ) {
62 0 : enable = slot_ctx->epoch_ctx->features.full_inflation_enable;
63 0 : }
64 :
65 0 : ulong min_slot = fd_ulong_min( enable, devnet_and_testnet );
66 0 : if ( min_slot == ULONG_MAX ) {
67 0 : if ( FD_FEATURE_ACTIVE( slot_ctx, pico_inflation ) ) {
68 0 : min_slot = slot_ctx->epoch_ctx->features.pico_inflation;
69 0 : } else {
70 0 : min_slot = 0;
71 0 : }
72 0 : }
73 0 : return min_slot;
74 0 : }
75 :
76 : /* https://github.com/anza-xyz/agave/blob/7117ed9653ce19e8b2dea108eff1f3eb6a3378a7/runtime/src/bank.rs#L2110 */
77 : static ulong
78 : get_inflation_num_slots( fd_exec_slot_ctx_t * slot_ctx,
79 : fd_epoch_schedule_t const * epoch_schedule,
80 0 : ulong slot ) {
81 0 : ulong inflation_activation_slot = get_inflation_start_slot( slot_ctx );
82 0 : ulong inflation_start_slot = fd_epoch_slot0(
83 0 : epoch_schedule,
84 0 : fd_ulong_sat_sub(
85 0 : fd_slot_to_epoch( epoch_schedule, inflation_activation_slot, NULL ),
86 0 : 1 )
87 0 : );
88 :
89 0 : ulong epoch = fd_slot_to_epoch(epoch_schedule, slot, NULL);
90 :
91 0 : return fd_epoch_slot0(epoch_schedule, epoch) - inflation_start_slot;
92 0 : }
93 :
94 : /* https://github.com/anza-xyz/agave/blob/7117ed9653ce19e8b2dea108eff1f3eb6a3378a7/runtime/src/bank.rs#L2121 */
95 : static double
96 0 : slot_in_year_for_inflation( fd_exec_slot_ctx_t * slot_ctx ) {
97 0 : fd_epoch_bank_t const * epoch_bank = fd_exec_epoch_ctx_epoch_bank( slot_ctx->epoch_ctx );
98 0 : ulong num_slots = get_inflation_num_slots( slot_ctx, &epoch_bank->epoch_schedule, slot_ctx->slot_bank.slot );
99 0 : return (double)num_slots / (double)epoch_bank->slots_per_year;
100 0 : }
101 :
102 : /* For a given stake and vote_state, calculate how many points were earned (credits * stake) and new value
103 : for credits_observed were the points paid
104 :
105 : https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/programs/stake/src/points.rs#L109 */
106 : static void
107 : calculate_stake_points_and_credits (
108 : fd_stake_history_t const * stake_history,
109 : fd_stake_t * stake,
110 : fd_vote_state_versioned_t * vote_state_versioned,
111 : fd_calculated_stake_points_t * result
112 0 : ) {
113 :
114 0 : ulong credits_in_stake = stake->credits_observed;
115 :
116 0 : fd_vote_epoch_credits_t * epoch_credits;
117 0 : switch (vote_state_versioned->discriminant) {
118 0 : case fd_vote_state_versioned_enum_current:
119 0 : epoch_credits = vote_state_versioned->inner.current.epoch_credits;
120 0 : break;
121 0 : case fd_vote_state_versioned_enum_v0_23_5:
122 0 : epoch_credits = vote_state_versioned->inner.v0_23_5.epoch_credits;
123 0 : break;
124 0 : case fd_vote_state_versioned_enum_v1_14_11:
125 0 : epoch_credits = vote_state_versioned->inner.v1_14_11.epoch_credits;
126 0 : break;
127 0 : default:
128 0 : FD_LOG_ERR(( "invalid vote account, should never happen" ));
129 0 : }
130 0 : ulong credits_in_vote = 0UL;
131 0 : if ( FD_LIKELY( !deq_fd_vote_epoch_credits_t_empty( epoch_credits ) ) ) {
132 0 : credits_in_vote = deq_fd_vote_epoch_credits_t_peek_tail_const( epoch_credits )->credits;
133 0 : }
134 :
135 : /* If the Vote account has less credits observed than the Stake account,
136 : something is wrong and we need to force an update.
137 :
138 : https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/programs/stake/src/points.rs#L142 */
139 0 : if ( FD_UNLIKELY( credits_in_vote < credits_in_stake ) ) {
140 0 : result->points = 0;
141 0 : result->new_credits_observed = credits_in_vote;
142 0 : result->force_credits_update_with_skipped_reward = 1;
143 0 : return;
144 0 : }
145 :
146 : /* If the Vote account has the same amount of credits observed as the Stake account,
147 : then the Vote account hasn't earnt any credits and so there is nothing to update.
148 :
149 : https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/programs/stake/src/points.rs#L148 */
150 0 : if ( FD_UNLIKELY( credits_in_vote == credits_in_stake ) ) {
151 0 : result->points = 0;
152 0 : result->new_credits_observed = credits_in_vote;
153 0 : result->force_credits_update_with_skipped_reward = 0;
154 0 : return;
155 0 : }
156 :
157 : /* Calculate the points for each epoch credit */
158 0 : uint128 points = 0;
159 0 : ulong new_credits_observed = credits_in_stake;
160 0 : for ( deq_fd_vote_epoch_credits_t_iter_t iter = deq_fd_vote_epoch_credits_t_iter_init( epoch_credits );
161 0 : !deq_fd_vote_epoch_credits_t_iter_done( epoch_credits, iter );
162 0 : iter = deq_fd_vote_epoch_credits_t_iter_next( epoch_credits, iter ) ) {
163 :
164 0 : fd_vote_epoch_credits_t * ele = deq_fd_vote_epoch_credits_t_iter_ele( epoch_credits, iter );
165 0 : ulong final_epoch_credits = ele->credits;
166 0 : ulong initial_epoch_credits = ele->prev_credits;
167 0 : uint128 earned_credits = 0;
168 0 : if ( FD_LIKELY( credits_in_stake < initial_epoch_credits ) ) {
169 0 : earned_credits = (uint128)(final_epoch_credits - initial_epoch_credits);
170 0 : } else if ( FD_UNLIKELY( credits_in_stake < final_epoch_credits ) ) {
171 0 : earned_credits = (uint128)(final_epoch_credits - new_credits_observed);
172 0 : }
173 :
174 0 : new_credits_observed = fd_ulong_max( new_credits_observed, final_epoch_credits );
175 :
176 0 : ulong stake_amount = fd_stake_activating_and_deactivating( &stake->delegation, ele->epoch, stake_history, NULL ).effective;
177 :
178 0 : points += (uint128)stake_amount * earned_credits;
179 0 : }
180 :
181 0 : result->points = points;
182 0 : result->new_credits_observed = new_credits_observed;
183 0 : result->force_credits_update_with_skipped_reward = 0;
184 0 : }
185 :
186 : /* https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/programs/stake/src/rewards.rs#L127 */
187 : static int
188 : calculate_stake_rewards(
189 : fd_stake_history_t const * stake_history,
190 : fd_stake_state_v2_t * stake_state,
191 : fd_vote_state_versioned_t * vote_state_versioned,
192 : ulong rewarded_epoch,
193 : fd_point_value_t * point_value,
194 : fd_calculated_stake_rewards_t * result
195 0 : ) {
196 0 : fd_calculated_stake_points_t stake_points_result = {0};
197 0 : calculate_stake_points_and_credits( stake_history, &stake_state->inner.stake.stake, vote_state_versioned, &stake_points_result);
198 :
199 : // Drive credits_observed forward unconditionally when rewards are disabled
200 : // or when this is the stake's activation epoch
201 0 : if ( ( point_value->rewards == 0 ) ||
202 0 : ( stake_state->inner.stake.stake.delegation.activation_epoch == rewarded_epoch ) ) {
203 0 : stake_points_result.force_credits_update_with_skipped_reward |= 1;
204 0 : }
205 :
206 0 : if (stake_points_result.force_credits_update_with_skipped_reward) {
207 0 : result->staker_rewards = 0;
208 0 : result->voter_rewards = 0;
209 0 : result->new_credits_observed = stake_points_result.new_credits_observed;
210 0 : return 0;
211 0 : }
212 0 : if ( stake_points_result.points == 0 || point_value->points == 0 ) {
213 0 : return 1;
214 0 : }
215 :
216 : /* FIXME: need to error out if the conversion from uint128 to u64 fails, also use 128 checked mul and div */
217 0 : ulong rewards = (ulong)(stake_points_result.points * (uint128)(point_value->rewards) / (uint128) point_value->points);
218 0 : if (rewards == 0) {
219 0 : return 1;
220 0 : }
221 :
222 0 : fd_commission_split_t split_result;
223 0 : fd_vote_commission_split( vote_state_versioned, rewards, &split_result );
224 0 : if (split_result.is_split && (split_result.voter_portion == 0 || split_result.staker_portion == 0)) {
225 0 : return 1;
226 0 : }
227 :
228 0 : result->staker_rewards = split_result.staker_portion;
229 0 : result->voter_rewards = split_result.voter_portion;
230 0 : result->new_credits_observed = stake_points_result.new_credits_observed;
231 0 : return 0;
232 0 : }
233 :
234 : /* https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/programs/stake/src/rewards.rs#L33 */
235 : static int
236 : redeem_rewards( fd_stake_history_t const * stake_history,
237 : fd_stake_state_v2_t * stake_state,
238 : fd_vote_state_versioned_t * vote_state_versioned,
239 : ulong rewarded_epoch,
240 : fd_point_value_t * point_value,
241 0 : fd_calculated_stake_rewards_t * calculated_stake_rewards) {
242 :
243 0 : int rc = calculate_stake_rewards( stake_history, stake_state, vote_state_versioned, rewarded_epoch, point_value, calculated_stake_rewards );
244 0 : if ( FD_UNLIKELY( rc != 0 ) ) {
245 0 : return rc;
246 0 : }
247 :
248 0 : return FD_EXECUTOR_INSTR_SUCCESS;
249 0 : }
250 :
251 : /* https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/programs/stake/src/points.rs#L70 */
252 : int
253 : calculate_points(
254 : fd_stake_state_v2_t * stake_state,
255 : fd_vote_state_versioned_t * vote_state_versioned,
256 : fd_stake_history_t const * stake_history,
257 : uint128 * result
258 0 : ) {
259 0 : if ( FD_UNLIKELY( !fd_stake_state_v2_is_stake( stake_state ) ) ) {
260 0 : return FD_EXECUTOR_INSTR_ERR_INVALID_ACC_DATA;
261 0 : }
262 :
263 0 : fd_calculated_stake_points_t stake_point_result;
264 0 : calculate_stake_points_and_credits( stake_history, &stake_state->inner.stake.stake, vote_state_versioned, &stake_point_result );
265 0 : *result = stake_point_result.points;
266 :
267 0 : return FD_EXECUTOR_INSTR_SUCCESS;
268 0 : }
269 :
270 : /* Returns the length of the given epoch in slots
271 :
272 : https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/sdk/program/src/epoch_schedule.rs#L103 */
273 : static ulong
274 : get_slots_in_epoch(
275 : ulong epoch,
276 : fd_epoch_bank_t const * epoch_bank
277 0 : ) {
278 0 : return (epoch < epoch_bank->epoch_schedule.first_normal_epoch) ?
279 0 : 1UL << fd_ulong_sat_add(epoch, FD_EPOCH_LEN_MIN_TRAILING_ZERO) :
280 0 : epoch_bank->epoch_schedule.slots_per_epoch;
281 0 : }
282 :
283 : /* https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/runtime/src/bank.rs#L2082 */
284 : static double
285 : epoch_duration_in_years(
286 : fd_epoch_bank_t const * epoch_bank,
287 : ulong prev_epoch
288 0 : ) {
289 0 : ulong slots_in_epoch = get_slots_in_epoch( prev_epoch, epoch_bank );
290 0 : return (double)slots_in_epoch / (double) epoch_bank->slots_per_year;
291 0 : }
292 :
293 : /* https://github.com/anza-xyz/agave/blob/7117ed9653ce19e8b2dea108eff1f3eb6a3378a7/runtime/src/bank.rs#L2128 */
294 : static void
295 : calculate_previous_epoch_inflation_rewards(
296 : fd_exec_slot_ctx_t * slot_ctx,
297 : ulong prev_epoch_capitalization,
298 : ulong prev_epoch,
299 : fd_prev_epoch_inflation_rewards_t * rewards
300 0 : ) {
301 0 : double slot_in_year = slot_in_year_for_inflation( slot_ctx );
302 :
303 0 : fd_epoch_bank_t const * epoch_bank = fd_exec_epoch_ctx_epoch_bank( slot_ctx->epoch_ctx );
304 0 : rewards->validator_rate = validator( &epoch_bank->inflation, slot_in_year );
305 0 : rewards->foundation_rate = foundation( &epoch_bank->inflation, slot_in_year );
306 0 : rewards->prev_epoch_duration_in_years = epoch_duration_in_years(epoch_bank, prev_epoch);
307 0 : rewards->validator_rewards = (ulong)(rewards->validator_rate * (double)prev_epoch_capitalization * rewards->prev_epoch_duration_in_years);
308 0 : 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));
309 0 : }
310 :
311 : /* https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/programs/stake/src/lib.rs#L29 */
312 : static ulong
313 0 : get_minimum_stake_delegation( fd_exec_slot_ctx_t * slot_ctx ) {
314 0 : if ( !FD_FEATURE_ACTIVE( slot_ctx, stake_minimum_delegation_for_rewards ) ) {
315 0 : return 0UL;
316 0 : }
317 :
318 0 : if ( !FD_FEATURE_ACTIVE( slot_ctx, stake_raise_minimum_delegation_to_1_sol ) ) {
319 0 : return LAMPORTS_PER_SOL;
320 0 : }
321 :
322 0 : return 1;
323 0 : }
324 :
325 : /* Calculates epoch reward points from stake/vote accounts.
326 :
327 : https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L472 */
328 : static void
329 : calculate_reward_points_partitioned(
330 : fd_exec_slot_ctx_t * slot_ctx,
331 : fd_stake_history_t const * stake_history,
332 : ulong rewards,
333 : fd_point_value_t * result
334 0 : ) {
335 : /* There is a cache of vote account keys stored in the slot context */
336 : /* TODO: check this cache is correct */
337 :
338 0 : uint128 points = 0;
339 0 : fd_epoch_bank_t const * epoch_bank = fd_exec_epoch_ctx_epoch_bank( slot_ctx->epoch_ctx );
340 :
341 0 : ulong minimum_stake_delegation = get_minimum_stake_delegation( slot_ctx );
342 :
343 : /* Calculate the points for each stake delegation */
344 0 : for( fd_delegation_pair_t_mapnode_t const * n = fd_delegation_pair_t_map_minimum_const( epoch_bank->stakes.stake_delegations_pool, epoch_bank->stakes.stake_delegations_root );
345 0 : n;
346 0 : n = fd_delegation_pair_t_map_successor_const( epoch_bank->stakes.stake_delegations_pool, n )
347 0 : ) {
348 0 : FD_SCRATCH_SCOPE_BEGIN {
349 0 : fd_valloc_t valloc = fd_scratch_virtual();
350 :
351 : /* Fetch the stake account */
352 0 : FD_BORROWED_ACCOUNT_DECL(stake_acc_rec);
353 0 : fd_pubkey_t const * stake_acc = &n->elem.account;
354 0 : int err = fd_acc_mgr_view( slot_ctx->acc_mgr, slot_ctx->funk_txn, stake_acc, stake_acc_rec);
355 0 : if ( err != FD_ACC_MGR_SUCCESS && err != FD_ACC_MGR_ERR_UNKNOWN_ACCOUNT ) {
356 0 : FD_LOG_ERR(( "failed to read stake account from funk" ));
357 0 : continue;
358 0 : }
359 0 : if ( err == FD_ACC_MGR_ERR_UNKNOWN_ACCOUNT ) {
360 0 : FD_LOG_DEBUG(( "stake account not found %s", FD_BASE58_ENC_32_ALLOCA( stake_acc->uc ) ));
361 0 : continue;
362 0 : }
363 0 : if ( stake_acc_rec->const_meta->info.lamports == 0 ) {
364 0 : FD_LOG_DEBUG(( "stake acc with zero lamports %s", FD_BASE58_ENC_32_ALLOCA( stake_acc->uc ) ));
365 0 : continue;
366 0 : }
367 :
368 : /* Check the minimum stake delegation */
369 0 : fd_stake_state_v2_t stake_state[1] = {0};
370 0 : err = fd_stake_get_state( stake_acc_rec, &valloc, stake_state );
371 0 : if ( err != 0 ) {
372 0 : FD_LOG_DEBUG(( "get stake state failed" ));
373 0 : continue;
374 0 : }
375 0 : if ( FD_UNLIKELY( stake_state->inner.stake.stake.delegation.stake < minimum_stake_delegation ) ) {
376 0 : continue;
377 0 : }
378 :
379 : /* Check that the vote account is present in our cache */
380 0 : fd_vote_accounts_pair_t_mapnode_t key;
381 0 : fd_pubkey_t const * voter_acc = &n->elem.delegation.voter_pubkey;
382 0 : fd_memcpy( &key.elem.key, voter_acc, sizeof(fd_pubkey_t) );
383 0 : fd_epoch_bank_t const * epoch_bank = fd_exec_epoch_ctx_epoch_bank(
384 0 : slot_ctx->epoch_ctx );
385 0 : if ( FD_UNLIKELY( fd_vote_accounts_pair_t_map_find(
386 0 : epoch_bank->stakes.vote_accounts.vote_accounts_pool,
387 0 : epoch_bank->stakes.vote_accounts.vote_accounts_root,
388 0 : &key ) == NULL ) ) {
389 0 : FD_LOG_DEBUG(( "vote account missing from cache" ));
390 0 : continue;
391 0 : }
392 :
393 : /* Check that the vote account is valid and has the correct owner */
394 0 : FD_BORROWED_ACCOUNT_DECL(voter_acc_rec);
395 0 : err = fd_acc_mgr_view( slot_ctx->acc_mgr, slot_ctx->funk_txn, voter_acc, voter_acc_rec );
396 0 : if ( FD_UNLIKELY( err ) ) {
397 0 : FD_LOG_DEBUG(( "failed to read vote account from funk" ));
398 0 : continue;
399 0 : }
400 0 : if( FD_UNLIKELY( memcmp( &voter_acc_rec->const_meta->info.owner, fd_solana_vote_program_id.key, sizeof(fd_pubkey_t) ) != 0 ) ) {
401 0 : FD_LOG_DEBUG(( "vote account has wrong owner" ));
402 0 : continue;
403 0 : }
404 0 : fd_bincode_decode_ctx_t decode = {
405 0 : .data = voter_acc_rec->const_data,
406 0 : .dataend = voter_acc_rec->const_data + voter_acc_rec->const_meta->dlen,
407 0 : .valloc = valloc,
408 0 : };
409 0 : fd_vote_state_versioned_t vote_state[1] = {0};
410 0 : if( FD_UNLIKELY( 0!=fd_vote_state_versioned_decode( vote_state, &decode ) ) ) {
411 0 : FD_LOG_DEBUG(( "vote_state_versioned_decode failed" ));
412 0 : continue;
413 0 : }
414 :
415 0 : uint128 account_points;
416 0 : err = calculate_points( stake_state, vote_state, stake_history, &account_points );
417 0 : if ( FD_UNLIKELY( err ) ) {
418 0 : FD_LOG_DEBUG(( "failed to calculate points" ));
419 0 : continue;
420 0 : }
421 :
422 0 : points += account_points;
423 0 : } FD_SCRATCH_SCOPE_END;
424 0 : }
425 :
426 : /* TODO: factor this out */
427 : /* Calculate points for each stake account in slot_bank.stake_account_keys.stake_accounts_pool */
428 0 : for ( fd_stake_accounts_pair_t_mapnode_t const * n = fd_stake_accounts_pair_t_map_minimum_const( slot_ctx->slot_bank.stake_account_keys.stake_accounts_pool, slot_ctx->slot_bank.stake_account_keys.stake_accounts_root );
429 0 : n;
430 0 : n = fd_stake_accounts_pair_t_map_successor_const( slot_ctx->slot_bank.stake_account_keys.stake_accounts_pool, n ) ) {
431 :
432 0 : FD_SCRATCH_SCOPE_BEGIN {
433 0 : fd_valloc_t valloc = fd_scratch_virtual();
434 :
435 : /* Fetch the stake account */
436 0 : FD_BORROWED_ACCOUNT_DECL(stake_acc_rec);
437 0 : fd_pubkey_t const * stake_acc = &n->elem.key;
438 0 : int err = fd_acc_mgr_view( slot_ctx->acc_mgr, slot_ctx->funk_txn, stake_acc, stake_acc_rec);
439 0 : if ( err != FD_ACC_MGR_SUCCESS && err != FD_ACC_MGR_ERR_UNKNOWN_ACCOUNT ) {
440 0 : FD_LOG_ERR(( "failed to read stake account from funk" ));
441 0 : continue;
442 0 : }
443 0 : if ( err == FD_ACC_MGR_ERR_UNKNOWN_ACCOUNT ) {
444 0 : FD_LOG_DEBUG(( "stake account not found %s", FD_BASE58_ENC_32_ALLOCA( stake_acc->uc ) ));
445 0 : continue;
446 0 : }
447 0 : if ( stake_acc_rec->const_meta->info.lamports == 0 ) {
448 0 : FD_LOG_DEBUG(( "stake acc with zero lamports %s", FD_BASE58_ENC_32_ALLOCA( stake_acc->uc ) ));
449 0 : continue;
450 0 : }
451 :
452 : /* Check the minimum stake delegation */
453 0 : fd_stake_state_v2_t stake_state[1] = {0};
454 0 : err = fd_stake_get_state( stake_acc_rec, &valloc, stake_state );
455 0 : if ( err != 0 ) {
456 0 : FD_LOG_DEBUG(( "get stake state failed" ));
457 0 : continue;
458 0 : }
459 0 : if ( FD_UNLIKELY( stake_state->inner.stake.stake.delegation.stake < minimum_stake_delegation ) ) {
460 0 : continue;
461 0 : }
462 :
463 : /* Check that the vote account is present in our cache */
464 0 : fd_vote_accounts_pair_t_mapnode_t key;
465 0 : fd_pubkey_t const * voter_acc = &stake_state->inner.stake.stake.delegation.voter_pubkey;
466 0 : fd_memcpy( &key.elem.key, voter_acc, sizeof(fd_pubkey_t) );
467 0 : fd_epoch_bank_t const * epoch_bank = fd_exec_epoch_ctx_epoch_bank(
468 0 : slot_ctx->epoch_ctx );
469 0 : if ( FD_UNLIKELY( fd_vote_accounts_pair_t_map_find(
470 0 : epoch_bank->stakes.vote_accounts.vote_accounts_pool,
471 0 : epoch_bank->stakes.vote_accounts.vote_accounts_root,
472 0 : &key ) == NULL ) ) {
473 0 : FD_LOG_DEBUG(( "vote account missing from cache" ));
474 0 : continue;
475 0 : }
476 :
477 : /* Check that the vote account is valid and has the correct owner */
478 0 : FD_BORROWED_ACCOUNT_DECL(voter_acc_rec);
479 0 : err = fd_acc_mgr_view( slot_ctx->acc_mgr, slot_ctx->funk_txn, voter_acc, voter_acc_rec );
480 0 : if ( FD_UNLIKELY( err ) ) {
481 0 : FD_LOG_DEBUG(( "failed to read vote account from funk" ));
482 0 : continue;
483 0 : }
484 0 : if( FD_UNLIKELY( memcmp( &voter_acc_rec->const_meta->info.owner, fd_solana_vote_program_id.key, sizeof(fd_pubkey_t) ) != 0 ) ) {
485 0 : FD_LOG_DEBUG(( "vote account has wrong owner" ));
486 0 : continue;
487 0 : }
488 0 : fd_bincode_decode_ctx_t decode = {
489 0 : .data = voter_acc_rec->const_data,
490 0 : .dataend = voter_acc_rec->const_data + voter_acc_rec->const_meta->dlen,
491 0 : .valloc = valloc,
492 0 : };
493 0 : fd_vote_state_versioned_t vote_state[1] = {0};
494 0 : if( FD_UNLIKELY( 0!=fd_vote_state_versioned_decode( vote_state, &decode ) ) ) {
495 0 : FD_LOG_DEBUG(( "vote_state_versioned_decode failed" ));
496 0 : continue;
497 0 : }
498 :
499 0 : uint128 account_points;
500 0 : err = calculate_points( stake_state, vote_state, stake_history, &account_points );
501 0 : if ( FD_UNLIKELY( err ) ) {
502 0 : FD_LOG_DEBUG(( "failed to calculate points" ));
503 0 : continue;
504 0 : }
505 :
506 0 : points += account_points;
507 0 : } FD_SCRATCH_SCOPE_END;
508 0 : }
509 :
510 0 : if (points > 0) {
511 0 : result->points = points;
512 0 : result->rewards = rewards;
513 0 : }
514 0 : }
515 :
516 : /* Calculate the partitioned stake rewards for a single stake/vote account pair, updates result with these. */
517 : static void
518 : calculate_stake_vote_rewards_account(
519 : fd_exec_slot_ctx_t * slot_ctx,
520 : fd_stake_history_t const * stake_history,
521 : ulong rewarded_epoch,
522 : fd_point_value_t * point_value,
523 : fd_pubkey_t const * stake_acc,
524 : fd_calculate_stake_vote_rewards_result_t * result
525 0 : ) {
526 0 : FD_SCRATCH_SCOPE_BEGIN {
527 :
528 0 : fd_epoch_bank_t const * epoch_bank = fd_exec_epoch_ctx_epoch_bank( slot_ctx->epoch_ctx );
529 0 : ulong minimum_stake_delegation = get_minimum_stake_delegation( slot_ctx );
530 :
531 0 : FD_BORROWED_ACCOUNT_DECL( stake_acc_rec );
532 0 : if( fd_acc_mgr_view( slot_ctx->acc_mgr, slot_ctx->funk_txn, stake_acc, stake_acc_rec) != 0 ) {
533 0 : FD_LOG_DEBUG(( "Stake acc not found %s", FD_BASE58_ENC_32_ALLOCA( stake_acc->uc ) ));
534 0 : return;
535 0 : }
536 :
537 0 : fd_stake_state_v2_t stake_state[1] = {0};
538 0 : if ( fd_stake_get_state( stake_acc_rec, &slot_ctx->valloc, stake_state ) != 0 ) {
539 0 : FD_LOG_DEBUG(( "Failed to read stake state from stake account %s", FD_BASE58_ENC_32_ALLOCA( stake_acc ) ));
540 0 : return;
541 0 : }
542 0 : if ( !fd_stake_state_v2_is_stake( stake_state ) ) {
543 0 : FD_LOG_DEBUG(( "stake account does not have active delegation" ));
544 0 : return;
545 0 : }
546 0 : fd_pubkey_t const * voter_acc = &stake_state->inner.stake.stake.delegation.voter_pubkey;
547 :
548 0 : if ( FD_FEATURE_ACTIVE(slot_ctx, stake_minimum_delegation_for_rewards )) {
549 0 : if ( stake_state->inner.stake.stake.delegation.stake < minimum_stake_delegation ) {
550 0 : return;
551 0 : }
552 0 : }
553 :
554 0 : fd_vote_accounts_pair_t_mapnode_t key;
555 0 : fd_memcpy( &key.elem.key, voter_acc, sizeof(fd_pubkey_t) );
556 0 : if ( fd_vote_accounts_pair_t_map_find( epoch_bank->stakes.vote_accounts.vote_accounts_pool, epoch_bank->stakes.vote_accounts.vote_accounts_root, &key ) == NULL
557 0 : && fd_vote_accounts_pair_t_map_find( slot_ctx->slot_bank.vote_account_keys.vote_accounts_pool, slot_ctx->slot_bank.vote_account_keys.vote_accounts_root, &key ) == NULL) {
558 0 : return;
559 0 : }
560 :
561 0 : FD_BORROWED_ACCOUNT_DECL( voter_acc_rec );
562 0 : int read_err = fd_acc_mgr_view( slot_ctx->acc_mgr, slot_ctx->funk_txn, voter_acc, voter_acc_rec );
563 0 : if( read_err!=0 || memcmp( &voter_acc_rec->const_meta->info.owner, fd_solana_vote_program_id.key, sizeof(fd_pubkey_t) ) != 0 ) {
564 0 : return;
565 0 : }
566 :
567 0 : fd_valloc_t valloc = fd_scratch_virtual();
568 0 : fd_bincode_decode_ctx_t decode = {
569 0 : .data = voter_acc_rec->const_data,
570 0 : .dataend = voter_acc_rec->const_data + voter_acc_rec->const_meta->dlen,
571 0 : .valloc = valloc,
572 0 : };
573 0 : fd_vote_state_versioned_t vote_state_versioned[1] = {0};
574 0 : if( fd_vote_state_versioned_decode( vote_state_versioned, &decode ) != 0 ) {
575 0 : FD_LOG_ERR(( "failed to decode vote state" ));
576 0 : return;
577 0 : }
578 :
579 : /* Note, this doesn't actually redeem any rewards.. this is a misnomer. */
580 0 : fd_calculated_stake_rewards_t calculated_stake_rewards[1] = {0};
581 0 : int err = redeem_rewards( stake_history, stake_state, vote_state_versioned, rewarded_epoch, point_value, calculated_stake_rewards );
582 0 : if ( err != 0) {
583 0 : FD_LOG_DEBUG(( "redeem_rewards failed for %s with error %d", FD_BASE58_ENC_32_ALLOCA( stake_acc->key ), err ));
584 0 : return;
585 0 : }
586 :
587 : /* Fetch the comission for the vote account */
588 0 : uchar commission = 0;
589 0 : switch (vote_state_versioned->discriminant) {
590 0 : case fd_vote_state_versioned_enum_current:
591 0 : commission = vote_state_versioned->inner.current.commission;
592 0 : break;
593 0 : case fd_vote_state_versioned_enum_v0_23_5:
594 0 : commission = vote_state_versioned->inner.v0_23_5.commission;
595 0 : break;
596 0 : case fd_vote_state_versioned_enum_v1_14_11:
597 0 : commission = vote_state_versioned->inner.v1_14_11.commission;
598 0 : break;
599 0 : default:
600 0 : FD_LOG_DEBUG(( "unsupported vote account" ));
601 0 : return;
602 0 : }
603 :
604 : /* Update the vote reward in the map */
605 0 : fd_vote_reward_t_mapnode_t vote_map_key[1];
606 0 : fd_memcpy( &vote_map_key->elem.pubkey, voter_acc, sizeof(fd_pubkey_t) );
607 0 : fd_vote_reward_t_mapnode_t * vote_reward_node = fd_vote_reward_t_map_find( result->vote_reward_map_pool, result->vote_reward_map_root, vote_map_key );
608 0 : if ( vote_reward_node == NULL ) {
609 0 : vote_reward_node = fd_vote_reward_t_map_acquire( result->vote_reward_map_pool );
610 0 : fd_memcpy( &vote_reward_node->elem.pubkey, voter_acc, sizeof(fd_pubkey_t) );
611 0 : vote_reward_node->elem.commission = commission;
612 0 : vote_reward_node->elem.vote_rewards = calculated_stake_rewards->voter_rewards;
613 0 : vote_reward_node->elem.needs_store = 1;
614 0 : fd_vote_reward_t_map_insert( result->vote_reward_map_pool, &result->vote_reward_map_root, vote_reward_node );
615 0 : } else {
616 0 : vote_reward_node->elem.needs_store = 1;
617 0 : vote_reward_node->elem.vote_rewards = fd_ulong_sat_add(
618 0 : vote_reward_node->elem.vote_rewards, calculated_stake_rewards->voter_rewards
619 0 : );
620 0 : }
621 :
622 : /* Add the stake reward to list of all stake rewards */
623 0 : fd_stake_reward_t * stake_reward = fd_stake_reward_pool_ele_acquire( result->stake_reward_calculation.pool );
624 0 : fd_memcpy( &stake_reward->stake_pubkey, stake_acc, FD_PUBKEY_FOOTPRINT );
625 0 : stake_reward->lamports = calculated_stake_rewards->staker_rewards;
626 0 : stake_reward->credits_observed = calculated_stake_rewards->new_credits_observed;
627 :
628 0 : fd_stake_reward_dlist_ele_push_tail(
629 0 : &result->stake_reward_calculation.stake_rewards,
630 0 : stake_reward,
631 0 : result->stake_reward_calculation.pool );
632 0 : result->stake_reward_calculation.stake_rewards_len += 1;
633 :
634 : /* Update the total stake rewards */
635 0 : result->stake_reward_calculation.total_stake_rewards_lamports += calculated_stake_rewards->staker_rewards;
636 0 : } FD_SCRATCH_SCOPE_END;
637 0 : }
638 :
639 : /* Calculates epoch rewards for stake/vote accounts.
640 : Returns vote rewards, stake rewards, and the sum of all stake rewards in lamports.
641 :
642 : This uses a pool to allocate the stake rewards, which means that we can use dlists to
643 : distribute these into partitions of variable size without copying them or over-allocating
644 : the partitions.
645 : - We use a single dlist to put all the stake rewards during the calculation phase.
646 : - We then distribute these into partitions (whose size cannot be known in advance), where each
647 : partition is a seperate dlist.
648 : - The dlist elements are all backed by the same pool, and allocated once.
649 : This approach optimizes memory usage and reduces copying.
650 :
651 : https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L334 */
652 : static void
653 : calculate_stake_vote_rewards(
654 : fd_exec_slot_ctx_t * slot_ctx,
655 : fd_stake_history_t const * stake_history,
656 : ulong rewarded_epoch,
657 : fd_point_value_t * point_value,
658 : fd_calculate_stake_vote_rewards_result_t * result
659 0 : ) {
660 0 : fd_epoch_bank_t const * epoch_bank = fd_exec_epoch_ctx_epoch_bank( slot_ctx->epoch_ctx );
661 0 : ulong rewards_max_count = fd_ulong_sat_add(
662 0 : fd_delegation_pair_t_map_size( epoch_bank->stakes.stake_delegations_pool, epoch_bank->stakes.stake_delegations_root ),
663 0 : fd_stake_accounts_pair_t_map_size( slot_ctx->slot_bank.stake_account_keys.stake_accounts_pool, slot_ctx->slot_bank.stake_account_keys.stake_accounts_root ) );
664 :
665 : /* Create the stake rewards pool and dlist. The pool will be destoyed after the stake rewards have been distributed. */
666 0 : result->stake_reward_calculation.pool = fd_stake_reward_pool_join(
667 0 : fd_stake_reward_pool_new(
668 0 : fd_valloc_malloc(
669 0 : slot_ctx->valloc,
670 0 : fd_stake_reward_pool_align(),
671 0 : fd_stake_reward_pool_footprint( rewards_max_count ) ), rewards_max_count ) );
672 0 : fd_stake_reward_dlist_new( &result->stake_reward_calculation.stake_rewards );
673 0 : result->stake_reward_calculation.stake_rewards_len = 0UL;
674 :
675 : /* Create the vote rewards map. This will be destroyed after the vote rewards have been distributed. */
676 0 : result->vote_reward_map_pool = fd_vote_reward_t_map_join( fd_vote_reward_t_map_new( fd_valloc_malloc(
677 0 : slot_ctx->valloc,
678 0 : fd_vote_reward_t_map_align(),
679 0 : fd_vote_reward_t_map_footprint( rewards_max_count )), rewards_max_count ) );
680 0 : result->vote_reward_map_root = NULL;
681 :
682 : /* Loop over all the delegations
683 :
684 : https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L367 */
685 0 : for( fd_delegation_pair_t_mapnode_t const * n = fd_delegation_pair_t_map_minimum_const(
686 0 : epoch_bank->stakes.stake_delegations_pool, epoch_bank->stakes.stake_delegations_root );
687 0 : n;
688 0 : n = fd_delegation_pair_t_map_successor_const( epoch_bank->stakes.stake_delegations_pool, n )
689 0 : ) {
690 0 : fd_pubkey_t const * stake_acc = &n->elem.account;
691 :
692 0 : calculate_stake_vote_rewards_account(
693 0 : slot_ctx,
694 0 : stake_history,
695 0 : rewarded_epoch,
696 0 : point_value,
697 0 : stake_acc,
698 0 : result );
699 0 : }
700 :
701 : /* Loop over all the stake accounts in the slot bank pool */
702 0 : for ( fd_stake_accounts_pair_t_mapnode_t const * n =
703 0 : fd_stake_accounts_pair_t_map_minimum_const(
704 0 : slot_ctx->slot_bank.stake_account_keys.stake_accounts_pool, slot_ctx->slot_bank.stake_account_keys.stake_accounts_root );
705 0 : n;
706 0 : n = fd_stake_accounts_pair_t_map_successor_const( slot_ctx->slot_bank.stake_account_keys.stake_accounts_pool, n) ) {
707 :
708 0 : fd_pubkey_t const * stake_acc = &n->elem.key;
709 0 : calculate_stake_vote_rewards_account(
710 0 : slot_ctx,
711 0 : stake_history,
712 0 : rewarded_epoch,
713 0 : point_value,
714 0 : stake_acc,
715 0 : result );
716 0 : }
717 0 : }
718 :
719 : /* Calculate epoch reward and return vote and stake rewards.
720 :
721 : https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L273 */
722 : static void
723 : calculate_validator_rewards(
724 : fd_exec_slot_ctx_t * slot_ctx,
725 : ulong rewarded_epoch,
726 : ulong rewards,
727 : fd_calculate_validator_rewards_result_t * result
728 0 : ) {
729 : /* https://github.com/firedancer-io/solana/blob/dab3da8e7b667d7527565bddbdbecf7ec1fb868e/runtime/src/bank.rs#L2759-L2786 */
730 0 : fd_stake_history_t const * stake_history = fd_sysvar_cache_stake_history( slot_ctx->sysvar_cache );
731 0 : if( FD_UNLIKELY( !stake_history ) ) {
732 0 : FD_LOG_ERR(( "StakeHistory sysvar is missing from sysvar cache" ));
733 0 : }
734 :
735 : /* Calculate the epoch reward points from stake/vote accounts */
736 0 : calculate_reward_points_partitioned( slot_ctx, stake_history, rewards, &result->point_value );
737 :
738 : /* Calculate the stake and vote rewards for each account */
739 0 : calculate_stake_vote_rewards(
740 0 : slot_ctx,
741 0 : stake_history,
742 0 : rewarded_epoch,
743 0 : &result->point_value,
744 0 : &result->calculate_stake_vote_rewards_result );
745 0 : }
746 :
747 : /* Calculate the number of blocks required to distribute rewards to all stake accounts.
748 :
749 : https://github.com/anza-xyz/agave/blob/9a7bf72940f4b3cd7fc94f54e005868ce707d53d/runtime/src/bank/partitioned_epoch_rewards/mod.rs#L214
750 : */
751 : static ulong
752 : get_reward_distribution_num_blocks(
753 : fd_epoch_schedule_t const * epoch_schedule,
754 : ulong slot,
755 : ulong total_stake_accounts
756 0 : ) {
757 : /* https://github.com/firedancer-io/solana/blob/dab3da8e7b667d7527565bddbdbecf7ec1fb868e/runtime/src/bank.rs#L1250-L1267 */
758 0 : if ( epoch_schedule->warmup &&
759 0 : fd_slot_to_epoch( epoch_schedule, slot, NULL ) < epoch_schedule->first_normal_epoch ) {
760 0 : return 1UL;
761 0 : }
762 :
763 0 : ulong num_chunks = total_stake_accounts / (ulong)STAKE_ACCOUNT_STORES_PER_BLOCK + (total_stake_accounts % STAKE_ACCOUNT_STORES_PER_BLOCK != 0);
764 0 : num_chunks = fd_ulong_max( num_chunks, 1 );
765 0 : num_chunks = fd_ulong_min(
766 0 : num_chunks,
767 0 : fd_ulong_max(
768 0 : epoch_schedule->slots_per_epoch / (ulong)MAX_FACTOR_OF_REWARD_BLOCKS_IN_EPOCH,
769 0 : 1) );
770 0 : return num_chunks;
771 0 : }
772 :
773 : static void
774 : hash_rewards_into_partitions(
775 : fd_exec_slot_ctx_t * slot_ctx,
776 : fd_stake_reward_calculation_t * stake_reward_calculation,
777 : const fd_hash_t * parent_blockhash,
778 : fd_stake_reward_calculation_partitioned_t * result
779 0 : ) {
780 : /* Initialize a dlist for every partition.
781 : These will all use the same pool - we do not re-allocate the stake rewards, only move them into partitions. */
782 0 : result->partitioned_stake_rewards.pool = stake_reward_calculation->pool;
783 0 : ulong num_partitions = get_reward_distribution_num_blocks(
784 0 : &fd_exec_epoch_ctx_epoch_bank( slot_ctx->epoch_ctx )->epoch_schedule,
785 0 : slot_ctx->slot_bank.slot,
786 0 : stake_reward_calculation->stake_rewards_len);
787 0 : result->partitioned_stake_rewards.partitions_len = num_partitions;
788 0 : result->partitioned_stake_rewards.partitions = fd_valloc_malloc(
789 0 : slot_ctx->valloc,
790 0 : fd_stake_reward_dlist_align(),
791 0 : fd_stake_reward_dlist_footprint() * num_partitions
792 0 : );
793 :
794 : /* Ownership of these dlist's and the pool gets transferred to stake_rewards_by_partition, which then gets transferred to epoch_reward_status.
795 : These are eventually cleaned up when epoch_reward_status_inactive is called. */
796 0 : for ( ulong i = 0; i < num_partitions; ++i ) {
797 0 : fd_stake_reward_dlist_new( &result->partitioned_stake_rewards.partitions[ i ] );
798 0 : }
799 :
800 : /* Iterate over all the stake rewards, moving references to them into the appropiate partitions.
801 : IMPORTANT: after this, we cannot use the original stake rewards dlist anymore. */
802 0 : fd_stake_reward_dlist_iter_t next_iter;
803 0 : for ( fd_stake_reward_dlist_iter_t iter = fd_stake_reward_dlist_iter_fwd_init(
804 0 : &stake_reward_calculation->stake_rewards, stake_reward_calculation->pool );
805 0 : !fd_stake_reward_dlist_iter_done( iter, &stake_reward_calculation->stake_rewards, stake_reward_calculation->pool );
806 0 : iter = next_iter
807 0 : ) {
808 0 : fd_stake_reward_t * stake_reward = fd_stake_reward_dlist_iter_ele( iter, &stake_reward_calculation->stake_rewards, stake_reward_calculation->pool );
809 : /* Cache the next iter here, as we will overwrite the DLIST_NEXT value further down in the loop iteration. */
810 0 : next_iter = fd_stake_reward_dlist_iter_fwd_next( iter, &stake_reward_calculation->stake_rewards, stake_reward_calculation->pool );
811 :
812 : /* https://github.com/firedancer-io/solana/blob/dab3da8e7b667d7527565bddbdbecf7ec1fb868e/runtime/src/epoch_rewards_hasher.rs#L43C31-L61 */
813 0 : fd_siphash13_t _sip[1] = {0};
814 0 : fd_siphash13_t * hasher = fd_siphash13_init( _sip, 0UL, 0UL );
815 :
816 0 : hasher = fd_siphash13_append( hasher, parent_blockhash->hash, sizeof(fd_hash_t) );
817 0 : fd_siphash13_append( hasher, (const uchar *) stake_reward->stake_pubkey.key, sizeof(fd_pubkey_t) );
818 :
819 0 : ulong hash64 = fd_siphash13_fini( hasher );
820 : /* hash_to_partition */
821 : /* FIXME: should be saturating add */
822 0 : ulong partition_index = (ulong)(
823 0 : (uint128) num_partitions *
824 0 : (uint128) hash64 /
825 0 : ((uint128)ULONG_MAX + 1)
826 0 : );
827 :
828 : /* Move the stake reward to the partition's dlist */
829 0 : fd_stake_reward_dlist_t * partition = &result->partitioned_stake_rewards.partitions[ partition_index ];
830 0 : fd_stake_reward_dlist_ele_push_tail( partition, stake_reward, stake_reward_calculation->pool );
831 0 : }
832 0 : }
833 :
834 : /* Calculate rewards from previous epoch to prepare for partitioned distribution.
835 :
836 : https://github.com/anza-xyz/agave/blob/7117ed9653ce19e8b2dea108eff1f3eb6a3378a7/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L214 */
837 : static void
838 : calculate_rewards_for_partitioning(
839 : fd_exec_slot_ctx_t * slot_ctx,
840 : ulong prev_epoch,
841 : const fd_hash_t * parent_blockhash,
842 : fd_partitioned_rewards_calculation_t * result
843 0 : ) {
844 : /* https://github.com/anza-xyz/agave/blob/7117ed9653ce19e8b2dea108eff1f3eb6a3378a7/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L227 */
845 0 : fd_prev_epoch_inflation_rewards_t rewards;
846 0 : calculate_previous_epoch_inflation_rewards( slot_ctx, slot_ctx->slot_bank.capitalization, prev_epoch, &rewards );
847 :
848 0 : fd_slot_bank_t const * slot_bank = &slot_ctx->slot_bank;
849 :
850 0 : fd_calculate_validator_rewards_result_t validator_result[1] = {0};
851 0 : calculate_validator_rewards( slot_ctx, prev_epoch, rewards.validator_rewards, validator_result );
852 :
853 0 : hash_rewards_into_partitions(
854 0 : slot_ctx,
855 0 : &validator_result->calculate_stake_vote_rewards_result.stake_reward_calculation,
856 0 : parent_blockhash,
857 0 : &result->stake_rewards_by_partition );
858 0 : result->stake_rewards_by_partition.total_stake_rewards_lamports =
859 0 : validator_result->calculate_stake_vote_rewards_result.stake_reward_calculation.total_stake_rewards_lamports;
860 :
861 0 : result->vote_reward_map_pool = validator_result->calculate_stake_vote_rewards_result.vote_reward_map_pool;
862 0 : result->vote_reward_map_root = validator_result->calculate_stake_vote_rewards_result.vote_reward_map_root;
863 0 : result->validator_rewards = rewards.validator_rewards;
864 0 : result->validator_rate = rewards.validator_rate;
865 0 : result->foundation_rate = rewards.foundation_rate;
866 0 : result->prev_epoch_duration_in_years = rewards.prev_epoch_duration_in_years;
867 0 : result->capitalization = slot_bank->capitalization;
868 0 : fd_memcpy( &result->point_value, &validator_result->point_value, FD_POINT_VALUE_FOOTPRINT );
869 0 : }
870 :
871 : /* Calculate rewards from previous epoch and distribute vote rewards
872 :
873 : https://github.com/anza-xyz/agave/blob/7117ed9653ce19e8b2dea108eff1f3eb6a3378a7/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L97 */
874 : static void
875 : calculate_rewards_and_distribute_vote_rewards(
876 : fd_exec_slot_ctx_t * slot_ctx,
877 : ulong prev_epoch,
878 : const fd_hash_t * parent_blockhash,
879 : fd_calculate_rewards_and_distribute_vote_rewards_result_t * result
880 0 : ) {
881 : /* https://github.com/firedancer-io/solana/blob/dab3da8e7b667d7527565bddbdbecf7ec1fb868e/runtime/src/bank.rs#L2406-L2492 */
882 0 : fd_partitioned_rewards_calculation_t rewards_calc_result[1] = {0};
883 0 : calculate_rewards_for_partitioning( slot_ctx, prev_epoch, parent_blockhash, rewards_calc_result );
884 :
885 : /* Iterate over all the vote reward nodes */
886 0 : for ( fd_vote_reward_t_mapnode_t* vote_reward_node = fd_vote_reward_t_map_minimum(
887 0 : rewards_calc_result->vote_reward_map_pool,
888 0 : rewards_calc_result->vote_reward_map_root);
889 0 : vote_reward_node;
890 0 : vote_reward_node = fd_vote_reward_t_map_successor( rewards_calc_result->vote_reward_map_pool, vote_reward_node ) ) {
891 :
892 0 : fd_pubkey_t const * vote_pubkey = &vote_reward_node->elem.pubkey;
893 0 : FD_BORROWED_ACCOUNT_DECL( vote_rec );
894 0 : FD_TEST( fd_acc_mgr_modify( slot_ctx->acc_mgr, slot_ctx->funk_txn, vote_pubkey, 1, 0UL, vote_rec ) == FD_ACC_MGR_SUCCESS );
895 0 : vote_rec->meta->slot = slot_ctx->slot_bank.slot;
896 :
897 0 : FD_TEST( fd_borrowed_account_checked_add_lamports( vote_rec, vote_reward_node->elem.vote_rewards ) == 0 );
898 0 : result->distributed_rewards = fd_ulong_sat_add( result->distributed_rewards, vote_reward_node->elem.vote_rewards );
899 0 : }
900 :
901 : /* Free the vote reward map */
902 0 : fd_valloc_free( slot_ctx->valloc,
903 0 : fd_vote_reward_t_map_delete(
904 0 : fd_vote_reward_t_map_leave( rewards_calc_result->vote_reward_map_pool ) ) );
905 :
906 : /* Verify that we didn't pay any more than we expected to */
907 0 : result->total_rewards = fd_ulong_sat_add( result->distributed_rewards, rewards_calc_result->stake_rewards_by_partition.total_stake_rewards_lamports );
908 0 : FD_TEST( rewards_calc_result->validator_rewards >= result->total_rewards );
909 :
910 0 : slot_ctx->slot_bank.capitalization += result->distributed_rewards;
911 :
912 : /* Cheap because this doesn't copy all the rewards, just pointers to the dlist */
913 0 : fd_memcpy( &result->stake_rewards_by_partition, &rewards_calc_result->stake_rewards_by_partition, FD_STAKE_REWARD_CALCULATION_PARTITIONED_FOOTPRINT );
914 0 : fd_memcpy( &result->point_value, &rewards_calc_result->point_value, FD_POINT_VALUE_FOOTPRINT );
915 0 : }
916 :
917 : /* Distributes a single partitioned reward to a single stake account */
918 : static int
919 : distribute_epoch_reward_to_stake_acc(
920 : fd_exec_slot_ctx_t * slot_ctx,
921 : fd_pubkey_t * stake_pubkey,
922 : ulong reward_lamports,
923 : ulong new_credits_observed
924 0 : ) {
925 :
926 0 : FD_BORROWED_ACCOUNT_DECL( stake_acc_rec );
927 0 : FD_TEST( fd_acc_mgr_modify( slot_ctx->acc_mgr, slot_ctx->funk_txn, stake_pubkey, 0, 0UL, stake_acc_rec ) == FD_ACC_MGR_SUCCESS );
928 0 : stake_acc_rec->meta->slot = slot_ctx->slot_bank.slot;
929 :
930 0 : fd_stake_state_v2_t stake_state[1] = {0};
931 0 : if ( fd_stake_get_state(stake_acc_rec, &slot_ctx->valloc, stake_state) != 0 ) {
932 0 : FD_LOG_DEBUG(( "failed to read stake state for %s", FD_BASE58_ENC_32_ALLOCA( stake_pubkey ) ));
933 0 : return 1;
934 0 : }
935 :
936 0 : if ( !fd_stake_state_v2_is_stake( stake_state ) ) {
937 0 : FD_LOG_DEBUG(( "non-stake stake account, this should never happen" ));
938 0 : return 1;
939 0 : }
940 :
941 0 : if( fd_borrowed_account_checked_add_lamports( stake_acc_rec, reward_lamports ) ) {
942 0 : FD_LOG_DEBUG(( "failed to add lamports to stake account" ));
943 0 : return 1;
944 0 : }
945 :
946 0 : stake_state->inner.stake.stake.credits_observed = new_credits_observed;
947 0 : stake_state->inner.stake.stake.delegation.stake = fd_ulong_sat_add(
948 0 : stake_state->inner.stake.stake.delegation.stake,
949 0 : reward_lamports
950 0 : );
951 :
952 0 : if ( FD_UNLIKELY( write_stake_state( stake_acc_rec, stake_state ) != 0 ) ) {
953 0 : FD_LOG_ERR(( "write_stake_state failed" ));
954 0 : }
955 :
956 0 : return 0;
957 0 : }
958 :
959 : /* Sets the epoch reward status to inactive, and destroys any allocated state associated with the active state. */
960 : void
961 : set_epoch_reward_status_inactive(
962 : fd_exec_slot_ctx_t * slot_ctx
963 0 : ) {
964 0 : if ( slot_ctx->epoch_reward_status.discriminant == fd_epoch_reward_status_enum_Active ) {
965 0 : fd_partitioned_stake_rewards_t * partitioned_rewards = &slot_ctx->epoch_reward_status.inner.Active.partitioned_stake_rewards;
966 : /* Destroy the partitions */
967 0 : fd_valloc_free( slot_ctx->valloc,
968 0 : fd_stake_reward_dlist_delete(
969 0 : fd_stake_reward_dlist_leave( partitioned_rewards->partitions ) ) );
970 :
971 : /* Destroy the underlying pool */
972 0 : fd_valloc_free(
973 0 : slot_ctx->valloc,
974 0 : fd_stake_reward_pool_delete(
975 0 : fd_stake_reward_pool_leave( partitioned_rewards->pool ) ) );
976 0 : }
977 0 : slot_ctx->epoch_reward_status.discriminant = fd_epoch_reward_status_enum_Inactive;
978 0 : }
979 :
980 : /* Sets the epoch reward status to active.
981 :
982 : Takes ownership of the given stake_rewards_by_partition data structure,
983 : which will be destroyed when set_epoch_reward_status_inactive is called. */
984 : void
985 : set_epoch_reward_status_active(
986 : fd_exec_slot_ctx_t * slot_ctx,
987 : ulong distribution_starting_block_height,
988 0 : fd_partitioned_stake_rewards_t * partitioned_rewards ) {
989 :
990 0 : slot_ctx->epoch_reward_status.discriminant = fd_epoch_reward_status_enum_Active;
991 0 : slot_ctx->epoch_reward_status.inner.Active.distribution_starting_block_height = distribution_starting_block_height;
992 :
993 0 : fd_memcpy( &slot_ctx->epoch_reward_status.inner.Active.partitioned_stake_rewards, partitioned_rewards, FD_PARTITIONED_STAKE_REWARDS_FOOTPRINT );
994 0 : }
995 :
996 : /* Process reward credits for a partition of rewards.
997 : Store the rewards to AccountsDB, update reward history record and total capitalization
998 :
999 : https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/runtime/src/bank/partitioned_epoch_rewards/distribution.rs#L88 */
1000 : static void
1001 : distribute_epoch_rewards_in_partition(
1002 : fd_stake_reward_dlist_t * partition,
1003 : fd_stake_reward_t *pool,
1004 : fd_exec_slot_ctx_t * slot_ctx
1005 0 : ) {
1006 :
1007 0 : ulong lamports_distributed = 0UL;
1008 0 : ulong lamports_burned = 0UL;
1009 :
1010 0 : for ( fd_stake_reward_dlist_iter_t iter = fd_stake_reward_dlist_iter_fwd_init( partition, pool );
1011 0 : !fd_stake_reward_dlist_iter_done( iter, partition, pool );
1012 0 : iter = fd_stake_reward_dlist_iter_fwd_next( iter, partition, pool )
1013 0 : ) {
1014 0 : fd_stake_reward_t * stake_reward = fd_stake_reward_dlist_iter_ele( iter, partition, pool );
1015 :
1016 0 : if ( distribute_epoch_reward_to_stake_acc(
1017 0 : slot_ctx,
1018 0 : &stake_reward->stake_pubkey,
1019 0 : stake_reward->lamports,
1020 0 : stake_reward->credits_observed ) == 0 ) {
1021 0 : lamports_distributed += stake_reward->lamports;
1022 0 : } else {
1023 0 : lamports_burned += stake_reward->lamports;
1024 0 : }
1025 :
1026 0 : }
1027 :
1028 : /* Update the epoch rewards sysvar with the amount distributed and burnt */
1029 0 : if ( FD_LIKELY( (
1030 0 : FD_FEATURE_ACTIVE( slot_ctx, enable_partitioned_epoch_reward ) ||
1031 0 : FD_FEATURE_ACTIVE( slot_ctx, partitioned_epoch_rewards_superfeature ) ) ) ) {
1032 0 : fd_sysvar_epoch_rewards_distribute( slot_ctx, lamports_distributed + lamports_burned );
1033 0 : }
1034 :
1035 0 : FD_LOG_DEBUG(( "lamports burned: %lu, lamports distributed: %lu", lamports_burned, lamports_distributed ));
1036 :
1037 0 : slot_ctx->slot_bank.capitalization += lamports_distributed;
1038 0 : }
1039 :
1040 : /* Process reward distribution for the block if it is inside reward interval.
1041 :
1042 : https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/runtime/src/bank/partitioned_epoch_rewards/distribution.rs#L42 */
1043 : void
1044 : fd_distribute_partitioned_epoch_rewards(
1045 : fd_exec_slot_ctx_t * slot_ctx
1046 0 : ) {
1047 0 : if ( slot_ctx->epoch_reward_status.discriminant == fd_epoch_reward_status_enum_Inactive ) {
1048 0 : return;
1049 0 : }
1050 0 : fd_start_block_height_and_rewards_t * status = &slot_ctx->epoch_reward_status.inner.Active;
1051 :
1052 0 : fd_slot_bank_t * slot_bank = &slot_ctx->slot_bank;
1053 0 : ulong height = slot_bank->block_height;
1054 0 : fd_epoch_bank_t const * epoch_bank = fd_exec_epoch_ctx_epoch_bank_const( slot_ctx->epoch_ctx );
1055 :
1056 0 : ulong distribution_starting_block_height = status->distribution_starting_block_height;
1057 0 : ulong distribution_end_exclusive = distribution_starting_block_height + status->partitioned_stake_rewards.partitions_len;
1058 :
1059 : /* TODO: track current epoch in epoch ctx? */
1060 0 : ulong epoch = fd_slot_to_epoch( &epoch_bank->epoch_schedule, slot_bank->slot, NULL );
1061 0 : FD_TEST( get_slots_in_epoch( epoch, epoch_bank ) > status->partitioned_stake_rewards.partitions_len );
1062 :
1063 0 : if ( ( height >= distribution_starting_block_height ) && ( height < distribution_end_exclusive ) ) {
1064 0 : ulong partition_index = height - distribution_starting_block_height;
1065 0 : distribute_epoch_rewards_in_partition(
1066 0 : &status->partitioned_stake_rewards.partitions[ partition_index ],
1067 0 : status->partitioned_stake_rewards.pool,
1068 0 : slot_ctx
1069 0 : );
1070 0 : }
1071 :
1072 : /* If we have finished distributing rewards, set the status to inactive */
1073 0 : if ( fd_ulong_sat_add( height, 1UL ) >= distribution_end_exclusive ) {
1074 0 : set_epoch_reward_status_inactive( slot_ctx );
1075 0 : fd_sysvar_epoch_rewards_set_inactive( slot_ctx );
1076 0 : }
1077 0 : }
1078 :
1079 : /* Non-partitioned epoch rewards entry-point. This uses the same logic as the partitioned epoch rewards code,
1080 : but distributes the rewards in one go. */
1081 : void
1082 : fd_update_rewards(
1083 : fd_exec_slot_ctx_t * slot_ctx,
1084 : const fd_hash_t * parent_blockhash,
1085 : ulong parent_epoch
1086 0 : ) {
1087 :
1088 : /* https://github.com/anza-xyz/agave/blob/7117ed9653ce19e8b2dea108eff1f3eb6a3378a7/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L55 */
1089 0 : fd_calculate_rewards_and_distribute_vote_rewards_result_t rewards_result[1] = {0};
1090 0 : calculate_rewards_and_distribute_vote_rewards(
1091 0 : slot_ctx,
1092 0 : parent_epoch,
1093 0 : parent_blockhash,
1094 0 : rewards_result
1095 0 : );
1096 :
1097 : /* Distribute all of the partitioned epoch rewards in one go */
1098 0 : for ( ulong i = 0UL; i < rewards_result->stake_rewards_by_partition.partitioned_stake_rewards.partitions_len; i++ ) {
1099 0 : distribute_epoch_rewards_in_partition(
1100 0 : &rewards_result->stake_rewards_by_partition.partitioned_stake_rewards.partitions[ i ],
1101 0 : rewards_result->stake_rewards_by_partition.partitioned_stake_rewards.pool,
1102 0 : slot_ctx
1103 0 : );
1104 0 : }
1105 0 : }
1106 :
1107 : /* Partitioned epoch rewards entry-point.
1108 :
1109 : https://github.com/anza-xyz/agave/blob/7117ed9653ce19e8b2dea108eff1f3eb6a3378a7/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L41
1110 : */
1111 : void
1112 : fd_begin_partitioned_rewards(
1113 : fd_exec_slot_ctx_t * slot_ctx,
1114 : const fd_hash_t * parent_blockhash,
1115 : ulong parent_epoch
1116 0 : ) {
1117 : /* https://github.com/anza-xyz/agave/blob/7117ed9653ce19e8b2dea108eff1f3eb6a3378a7/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L55 */
1118 0 : fd_calculate_rewards_and_distribute_vote_rewards_result_t rewards_result[1] = {0};
1119 0 : calculate_rewards_and_distribute_vote_rewards(
1120 0 : slot_ctx,
1121 0 : parent_epoch,
1122 0 : parent_blockhash,
1123 0 : rewards_result
1124 0 : );
1125 :
1126 : /* https://github.com/anza-xyz/agave/blob/9a7bf72940f4b3cd7fc94f54e005868ce707d53d/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L62 */
1127 0 : ulong distribution_starting_block_height = slot_ctx->slot_bank.block_height + REWARD_CALCULATION_NUM_BLOCKS;
1128 :
1129 : /* Set the epoch reward status to be active */
1130 0 : set_epoch_reward_status_active( slot_ctx, distribution_starting_block_height, &rewards_result->stake_rewards_by_partition.partitioned_stake_rewards );
1131 :
1132 : /* Initialise the epoch rewards sysvar
1133 :
1134 : https://github.com/anza-xyz/agave/blob/9a7bf72940f4b3cd7fc94f54e005868ce707d53d/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L78 */
1135 0 : fd_sysvar_epoch_rewards_init(
1136 0 : slot_ctx,
1137 0 : rewards_result->total_rewards,
1138 0 : rewards_result->distributed_rewards,
1139 0 : distribution_starting_block_height,
1140 0 : rewards_result->stake_rewards_by_partition.partitioned_stake_rewards.partitions_len,
1141 0 : rewards_result->point_value,
1142 0 : parent_blockhash
1143 0 : );
1144 0 : }
1145 :
1146 : /*
1147 : Re-calculates partitioned stake rewards.
1148 : This updates the slot context's epoch reward status with the recalculated partitioned rewards.
1149 :
1150 : https://github.com/anza-xyz/agave/blob/2316fea4c0852e59c071f72d72db020017ffd7d0/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L536 */
1151 : void
1152 : fd_rewards_recalculate_partitioned_rewards(
1153 : fd_exec_slot_ctx_t * slot_ctx
1154 0 : ) {
1155 0 : fd_sysvar_epoch_rewards_t epoch_rewards[1];
1156 0 : if ( FD_UNLIKELY( fd_sysvar_epoch_rewards_read( epoch_rewards, slot_ctx ) == NULL ) ) {
1157 0 : FD_LOG_NOTICE(( "failed to read sysvar epoch rewards - the sysvar may not have been created yet" ));
1158 0 : set_epoch_reward_status_inactive( slot_ctx );
1159 0 : return;
1160 0 : }
1161 :
1162 0 : if ( FD_UNLIKELY( epoch_rewards->active ) ) {
1163 : /* If partitioned rewards are active, the rewarded epoch is always the immediately
1164 : preceeding epoch.
1165 :
1166 : https://github.com/anza-xyz/agave/blob/2316fea4c0852e59c071f72d72db020017ffd7d0/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L566 */
1167 0 : fd_epoch_schedule_t * epoch_schedule = &fd_exec_epoch_ctx_epoch_bank( slot_ctx->epoch_ctx )->epoch_schedule;
1168 0 : ulong epoch = fd_slot_to_epoch( epoch_schedule, slot_ctx->slot_bank.slot, NULL );
1169 0 : ulong rewarded_epoch = fd_ulong_sat_sub( epoch, 1UL );
1170 :
1171 0 : fd_stake_history_t const * stake_history = fd_sysvar_cache_stake_history( slot_ctx->sysvar_cache );
1172 0 : if( FD_UNLIKELY( !stake_history ) ) {
1173 0 : FD_LOG_ERR(( "StakeHistory sysvar is missing from sysvar cache" ));
1174 0 : }
1175 :
1176 0 : fd_point_value_t point_value = {
1177 0 : .points = epoch_rewards->total_points,
1178 0 : .rewards = epoch_rewards->total_rewards
1179 0 : };
1180 :
1181 : /* In future, the calculation will be cached in the snapshot, but for now we just re-calculate it
1182 : (as Agave does). */
1183 0 : fd_calculate_stake_vote_rewards_result_t calculate_stake_vote_rewards_result[1];
1184 0 : calculate_stake_vote_rewards(
1185 0 : slot_ctx,
1186 0 : stake_history,
1187 0 : rewarded_epoch,
1188 0 : &point_value,
1189 0 : calculate_stake_vote_rewards_result
1190 0 : );
1191 :
1192 : /* Free the vote reward map, as this isn't actually used in this code path. */
1193 0 : fd_valloc_free( slot_ctx->valloc,
1194 0 : fd_vote_reward_t_map_delete(
1195 0 : fd_vote_reward_t_map_leave( calculate_stake_vote_rewards_result->vote_reward_map_pool ) ) );
1196 :
1197 0 : fd_stake_reward_calculation_partitioned_t stake_rewards_by_partition[1];
1198 0 : hash_rewards_into_partitions(
1199 0 : slot_ctx,
1200 0 : &calculate_stake_vote_rewards_result->stake_reward_calculation,
1201 0 : &epoch_rewards->parent_blockhash,
1202 0 : stake_rewards_by_partition );
1203 :
1204 : /* Update the epoch reward status with the newly re-calculated partitions. */
1205 0 : set_epoch_reward_status_active(
1206 0 : slot_ctx,
1207 0 : epoch_rewards->distribution_starting_block_height,
1208 0 : &stake_rewards_by_partition->partitioned_stake_rewards );
1209 0 : } else {
1210 0 : set_epoch_reward_status_inactive( slot_ctx );
1211 0 : }
1212 :
1213 0 : }
|