Line data Source code
1 : #include "fd_rewards.h"
2 : #include <math.h>
3 :
4 : #include "../runtime/fd_acc_mgr.h"
5 : #include "../runtime/fd_executor_err.h"
6 : #include "../runtime/program/fd_vote_program.h"
7 : #include "../runtime/sysvar/fd_sysvar_epoch_rewards.h"
8 : #include "../runtime/sysvar/fd_sysvar_epoch_schedule.h"
9 : #include "../stakes/fd_stakes.h"
10 : #include "../runtime/program/fd_stake_program.h"
11 : #include "../runtime/sysvar/fd_sysvar_stake_history.h"
12 : #include "../runtime/context/fd_capture_ctx.h"
13 : #include "../runtime/fd_runtime.h"
14 :
15 : /* https://github.com/anza-xyz/agave/blob/7117ed9653ce19e8b2dea108eff1f3eb6a3378a7/sdk/src/inflation.rs#L85 */
16 : static double
17 0 : total( fd_inflation_t const * inflation, double year ) {
18 0 : if ( FD_UNLIKELY( year == 0.0 ) ) {
19 0 : FD_LOG_ERR(( "inflation year 0" ));
20 0 : }
21 0 : double tapered = inflation->initial * pow( (1.0 - inflation->taper), year );
22 0 : return (tapered > inflation->terminal) ? tapered : inflation->terminal;
23 0 : }
24 :
25 : /* https://github.com/anza-xyz/agave/blob/7117ed9653ce19e8b2dea108eff1f3eb6a3378a7/sdk/src/inflation.rs#L102 */
26 : static double
27 0 : foundation( fd_inflation_t const * inflation, double year ) {
28 0 : return (year < inflation->foundation_term) ? inflation->foundation * total(inflation, year) : 0.0;
29 0 : }
30 :
31 : /* https://github.com/anza-xyz/agave/blob/7117ed9653ce19e8b2dea108eff1f3eb6a3378a7/sdk/src/inflation.rs#L97 */
32 : static double
33 0 : validator( fd_inflation_t const * inflation, double year) {
34 : /* https://github.com/firedancer-io/solana/blob/dab3da8e7b667d7527565bddbdbecf7ec1fb868e/sdk/src/inflation.rs#L96-L99 */
35 0 : FD_LOG_DEBUG(("Validator Rate: %.16f %.16f %.16f %.16f %.16f", year, total( inflation, year ), foundation( inflation, year ), inflation->taper, inflation->initial));
36 0 : return total( inflation, year ) - foundation( inflation, year );
37 0 : }
38 :
39 : /* Calculates the starting slot for inflation from the activation slot. The activation slot is the earliest
40 : activation slot of the following features:
41 : - devnet_and_testnet
42 : - full_inflation_enable, if full_inflation_vote has been activated
43 :
44 : https://github.com/anza-xyz/agave/blob/7117ed9653ce19e8b2dea108eff1f3eb6a3378a7/runtime/src/bank.rs#L2095 */
45 : static FD_FN_CONST ulong
46 0 : get_inflation_start_slot( fd_bank_t const * bank ) {
47 0 : ulong devnet_and_testnet = FD_FEATURE_ACTIVE_BANK( bank, devnet_and_testnet )
48 0 : ? fd_bank_features_query( bank )->devnet_and_testnet
49 0 : : ULONG_MAX;
50 :
51 0 : ulong enable = ULONG_MAX;
52 0 : if( FD_FEATURE_ACTIVE_BANK( bank, full_inflation_vote ) &&
53 0 : FD_FEATURE_ACTIVE_BANK( bank, full_inflation_enable ) ) {
54 0 : enable = fd_bank_features_query( bank )->full_inflation_enable;
55 0 : }
56 :
57 0 : ulong min_slot = fd_ulong_min( enable, devnet_and_testnet );
58 0 : if( min_slot == ULONG_MAX ) {
59 0 : if( FD_FEATURE_ACTIVE_BANK( bank, pico_inflation ) ) {
60 0 : min_slot = fd_bank_features_query( bank )->pico_inflation;
61 0 : } else {
62 0 : min_slot = 0;
63 0 : }
64 0 : }
65 0 : return min_slot;
66 0 : }
67 :
68 : /* https://github.com/anza-xyz/agave/blob/7117ed9653ce19e8b2dea108eff1f3eb6a3378a7/runtime/src/bank.rs#L2110 */
69 : static ulong
70 : get_inflation_num_slots( fd_bank_t const * bank,
71 : fd_epoch_schedule_t const * epoch_schedule,
72 0 : ulong slot ) {
73 0 : ulong inflation_activation_slot = get_inflation_start_slot( bank );
74 0 : ulong inflation_start_slot = fd_epoch_slot0( epoch_schedule,
75 0 : fd_ulong_sat_sub( fd_slot_to_epoch( epoch_schedule,
76 0 : inflation_activation_slot, NULL ),
77 0 : 1UL ) );
78 :
79 0 : ulong epoch = fd_slot_to_epoch( epoch_schedule, slot, NULL );
80 :
81 0 : return fd_epoch_slot0( epoch_schedule, epoch ) - inflation_start_slot;
82 0 : }
83 :
84 : /* https://github.com/anza-xyz/agave/blob/7117ed9653ce19e8b2dea108eff1f3eb6a3378a7/runtime/src/bank.rs#L2121 */
85 : static double
86 0 : slot_in_year_for_inflation( fd_bank_t const * bank ) {
87 0 : fd_epoch_schedule_t const * epoch_schedule = fd_bank_epoch_schedule_query( bank );
88 0 : ulong num_slots = get_inflation_num_slots( bank, epoch_schedule, fd_bank_slot_get( bank ) );
89 0 : return (double)num_slots / (double)fd_bank_slots_per_year_get( bank );
90 0 : }
91 :
92 : /* For a given stake and vote_state, calculate how many points were earned (credits * stake) and new value
93 : for credits_observed were the points paid
94 :
95 : https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/programs/stake/src/points.rs#L109 */
96 : static void
97 : calculate_stake_points_and_credits( fd_stake_history_t const * stake_history,
98 : fd_stake_delegation_t const * stake,
99 : fd_vote_state_ele_t const * vote_state,
100 : ulong * new_rate_activation_epoch,
101 0 : fd_calculated_stake_points_t * result ) {
102 :
103 0 : ulong credits_in_stake = stake->credits_observed;
104 0 : ulong credits_in_vote = 0UL;
105 0 : if( FD_LIKELY( vote_state->credits_cnt>0UL ) ) {
106 0 : credits_in_vote = vote_state->credits[vote_state->credits_cnt-1UL];
107 0 : }
108 :
109 : /* If the Vote account has less credits observed than the Stake account,
110 : something is wrong and we need to force an update.
111 :
112 : https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/programs/stake/src/points.rs#L142 */
113 0 : if( FD_UNLIKELY( credits_in_vote < credits_in_stake ) ) {
114 0 : result->points = 0;
115 0 : result->new_credits_observed = credits_in_vote;
116 0 : result->force_credits_update_with_skipped_reward = 1;
117 0 : return;
118 0 : }
119 :
120 : /* If the Vote account has the same amount of credits observed as the Stake account,
121 : then the Vote account hasn't earnt any credits and so there is nothing to update.
122 :
123 : https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/programs/stake/src/points.rs#L148 */
124 0 : if( FD_UNLIKELY( credits_in_vote == credits_in_stake ) ) {
125 0 : result->points = 0;
126 0 : result->new_credits_observed = credits_in_vote;
127 0 : result->force_credits_update_with_skipped_reward = 0;
128 0 : return;
129 0 : }
130 :
131 : /* Calculate the points for each epoch credit */
132 0 : uint128 points = 0;
133 0 : ulong new_credits_observed = credits_in_stake;
134 0 : for( ulong i=0UL; i<vote_state->credits_cnt; i++ ) {
135 :
136 0 : ulong final_epoch_credits = vote_state->credits[i];
137 0 : ulong initial_epoch_credits = vote_state->prev_credits[i];
138 0 : uint128 earned_credits = 0;
139 0 : if( FD_LIKELY( credits_in_stake < initial_epoch_credits ) ) {
140 0 : earned_credits = (uint128)(final_epoch_credits - initial_epoch_credits);
141 0 : } else if( FD_UNLIKELY( credits_in_stake < final_epoch_credits ) ) {
142 0 : earned_credits = (uint128)(final_epoch_credits - new_credits_observed);
143 0 : }
144 :
145 0 : new_credits_observed = fd_ulong_max( new_credits_observed, final_epoch_credits );
146 :
147 0 : fd_delegation_t delegation = {
148 0 : .voter_pubkey = stake->vote_account,
149 0 : .stake = stake->stake,
150 0 : .activation_epoch = stake->activation_epoch,
151 0 : .deactivation_epoch = stake->deactivation_epoch,
152 0 : .warmup_cooldown_rate = stake->warmup_cooldown_rate,
153 0 : };
154 :
155 0 : ulong stake_amount = fd_stake_activating_and_deactivating(
156 0 : &delegation,
157 0 : vote_state->epoch[i],
158 0 : stake_history,
159 0 : new_rate_activation_epoch ).effective;
160 :
161 0 : points += (uint128)stake_amount * earned_credits;
162 :
163 0 : }
164 :
165 :
166 0 : result->points = points;
167 0 : result->new_credits_observed = new_credits_observed;
168 0 : result->force_credits_update_with_skipped_reward = 0;
169 0 : }
170 :
171 : /* https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/programs/stake/src/rewards.rs#L127 */
172 : static int
173 : calculate_stake_rewards( fd_stake_history_t const * stake_history,
174 : fd_stake_delegation_t const * stake,
175 : fd_vote_state_ele_t const * vote_state,
176 : ulong rewarded_epoch,
177 : fd_point_value_t * point_value,
178 : ulong * new_rate_activation_epoch,
179 0 : fd_calculated_stake_rewards_t * result ) {
180 0 : fd_calculated_stake_points_t stake_points_result = {0};
181 :
182 0 : calculate_stake_points_and_credits(
183 0 : stake_history,
184 0 : stake,
185 0 : vote_state,
186 0 : new_rate_activation_epoch,
187 0 : &stake_points_result);
188 :
189 : // Drive credits_observed forward unconditionally when rewards are disabled
190 : // or when this is the stake's activation epoch
191 0 : if( ( point_value->rewards==0UL ) ||
192 0 : ( stake->activation_epoch==rewarded_epoch ) ) {
193 0 : stake_points_result.force_credits_update_with_skipped_reward |= 1;
194 0 : }
195 :
196 0 : if( stake_points_result.force_credits_update_with_skipped_reward ) {
197 0 : result->staker_rewards = 0;
198 0 : result->voter_rewards = 0;
199 0 : result->new_credits_observed = stake_points_result.new_credits_observed;
200 0 : return 0;
201 0 : }
202 0 : if( stake_points_result.points == 0 || point_value->points == 0 ) {
203 0 : return 1;
204 0 : }
205 :
206 : /* FIXME: need to error out if the conversion from uint128 to u64 fails, also use 128 checked mul and div */
207 0 : ulong rewards = (ulong)(stake_points_result.points * (uint128)(point_value->rewards) / (uint128) point_value->points);
208 0 : if( rewards == 0 ) {
209 0 : return 1;
210 0 : }
211 :
212 0 : fd_commission_split_t split_result;
213 0 : fd_vote_commission_split( vote_state->commission, rewards, &split_result );
214 0 : if( split_result.is_split && (split_result.voter_portion == 0 || split_result.staker_portion == 0) ) {
215 0 : return 1;
216 0 : }
217 :
218 0 : result->staker_rewards = split_result.staker_portion;
219 0 : result->voter_rewards = split_result.voter_portion;
220 0 : result->new_credits_observed = stake_points_result.new_credits_observed;
221 0 : return 0;
222 0 : }
223 :
224 : /* https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/programs/stake/src/rewards.rs#L33 */
225 : static int
226 : redeem_rewards( fd_stake_history_t const * stake_history,
227 : fd_stake_delegation_t const * stake,
228 : fd_vote_state_ele_t const * vote_state,
229 : ulong rewarded_epoch,
230 : fd_point_value_t * point_value,
231 : ulong * new_rate_activation_epoch,
232 0 : fd_calculated_stake_rewards_t * calculated_stake_rewards ) {
233 :
234 0 : int rc = calculate_stake_rewards(
235 0 : stake_history,
236 0 : stake,
237 0 : vote_state,
238 0 : rewarded_epoch,
239 0 : point_value,
240 0 : new_rate_activation_epoch,
241 0 : calculated_stake_rewards );
242 0 : if( FD_UNLIKELY( rc!=0 ) ) {
243 0 : return rc;
244 0 : }
245 :
246 0 : return FD_EXECUTOR_INSTR_SUCCESS;
247 0 : }
248 :
249 : /* https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/programs/stake/src/points.rs#L70 */
250 : static int
251 : calculate_points( fd_stake_delegation_t const * stake,
252 : fd_vote_state_ele_t const * vote_state,
253 : fd_stake_history_t const * stake_history,
254 : ulong * new_rate_activation_epoch,
255 0 : uint128 * result ) {
256 0 : fd_calculated_stake_points_t stake_point_result;
257 0 : calculate_stake_points_and_credits( stake_history,
258 0 : stake,
259 0 : vote_state,
260 0 : new_rate_activation_epoch,
261 0 : &stake_point_result );
262 0 : *result = stake_point_result.points;
263 :
264 0 : return FD_EXECUTOR_INSTR_SUCCESS;
265 0 : }
266 :
267 : /* Returns the length of the given epoch in slots
268 :
269 : https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/sdk/program/src/epoch_schedule.rs#L103 */
270 : static ulong
271 : get_slots_in_epoch( ulong epoch,
272 0 : fd_epoch_schedule_t const * epoch_schedule ) {
273 0 : return (epoch < epoch_schedule->first_normal_epoch) ?
274 0 : 1UL << fd_ulong_sat_add(epoch, FD_EPOCH_LEN_MIN_TRAILING_ZERO) :
275 0 : epoch_schedule->slots_per_epoch;
276 0 : }
277 :
278 : /* https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/runtime/src/bank.rs#L2082 */
279 : static double
280 : epoch_duration_in_years( fd_bank_t const * bank,
281 0 : ulong prev_epoch ) {
282 0 : ulong slots_in_epoch = get_slots_in_epoch( prev_epoch, fd_bank_epoch_schedule_query( bank ) );
283 0 : return (double)slots_in_epoch / (double)fd_bank_slots_per_year_get( bank );
284 0 : }
285 :
286 : /* https://github.com/anza-xyz/agave/blob/7117ed9653ce19e8b2dea108eff1f3eb6a3378a7/runtime/src/bank.rs#L2128 */
287 : static void
288 : calculate_previous_epoch_inflation_rewards( fd_bank_t const * bank,
289 : ulong prev_epoch_capitalization,
290 : ulong prev_epoch,
291 0 : fd_prev_epoch_inflation_rewards_t * rewards ) {
292 0 : double slot_in_year = slot_in_year_for_inflation( bank );
293 :
294 0 : rewards->validator_rate = validator( fd_bank_inflation_query( bank ), slot_in_year );
295 0 : rewards->foundation_rate = foundation( fd_bank_inflation_query( bank ), slot_in_year );
296 0 : rewards->prev_epoch_duration_in_years = epoch_duration_in_years( bank, prev_epoch );
297 0 : rewards->validator_rewards = (ulong)(rewards->validator_rate * (double)prev_epoch_capitalization * rewards->prev_epoch_duration_in_years);
298 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 ));
299 0 : }
300 :
301 : /* https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/programs/stake/src/lib.rs#L29 */
302 : static ulong
303 0 : get_minimum_stake_delegation( fd_exec_slot_ctx_t const * slot_ctx ) {
304 0 : if( !FD_FEATURE_ACTIVE_BANK( slot_ctx->bank, stake_minimum_delegation_for_rewards ) ) {
305 0 : return 0UL;
306 0 : }
307 :
308 0 : if( FD_FEATURE_ACTIVE_BANK( slot_ctx->bank, stake_raise_minimum_delegation_to_1_sol ) ) {
309 0 : return LAMPORTS_PER_SOL;
310 0 : }
311 :
312 0 : return 1;
313 0 : }
314 :
315 : static uint128
316 : calculate_points_all( fd_exec_slot_ctx_t const * slot_ctx,
317 : fd_stake_delegations_t const * stake_delegations,
318 : fd_stake_history_t const * stake_history,
319 : ulong * new_warmup_cooldown_rate_epoch,
320 0 : ulong minimum_stake_delegation ) {
321 :
322 0 : uint128 total_points = 0;
323 :
324 0 : fd_vote_states_t const * vote_states = fd_bank_vote_states_locking_query( slot_ctx->bank );
325 :
326 0 : fd_stake_delegations_iter_t iter_[1];
327 0 : for( fd_stake_delegations_iter_t * iter = fd_stake_delegations_iter_init( iter_, stake_delegations );
328 0 : !fd_stake_delegations_iter_done( iter );
329 0 : fd_stake_delegations_iter_next( iter ) ) {
330 0 : fd_stake_delegation_t const * stake_delegation = fd_stake_delegations_iter_ele( iter );
331 :
332 0 : if( FD_UNLIKELY( stake_delegation->stake<minimum_stake_delegation ) ) {
333 0 : continue;
334 0 : }
335 :
336 0 : fd_vote_state_ele_t * vote_state_ele = fd_vote_states_query( vote_states, &stake_delegation->vote_account );
337 0 : if( FD_UNLIKELY( !vote_state_ele ) ) {
338 0 : continue;
339 0 : }
340 :
341 0 : uint128 account_points;
342 0 : int err = calculate_points(
343 0 : stake_delegation,
344 0 : vote_state_ele,
345 0 : stake_history,
346 0 : new_warmup_cooldown_rate_epoch, &account_points );
347 0 : if( FD_UNLIKELY( err ) ) {
348 0 : FD_LOG_DEBUG(( "failed to calculate points" ));
349 0 : continue;
350 0 : }
351 :
352 0 : total_points += account_points;
353 0 : }
354 :
355 0 : fd_bank_vote_states_end_locking_query( slot_ctx->bank );
356 :
357 0 : return total_points;
358 0 : }
359 :
360 : /* Calculates epoch reward points from stake/vote accounts.
361 : https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L472 */
362 : static void
363 : calculate_reward_points_partitioned( fd_exec_slot_ctx_t * slot_ctx,
364 : fd_stake_delegations_t const * stake_delegations,
365 : fd_stake_history_t const * stake_history,
366 : ulong rewards,
367 0 : fd_point_value_t * result ) {
368 0 : ulong minimum_stake_delegation = get_minimum_stake_delegation( slot_ctx );
369 :
370 : /* Calculate the points for each stake delegation */
371 0 : int _err[1];
372 0 : ulong new_warmup_cooldown_rate_epoch_val = 0UL;
373 0 : ulong * new_warmup_cooldown_rate_epoch = &new_warmup_cooldown_rate_epoch_val;
374 0 : int is_some = fd_new_warmup_cooldown_rate_epoch(
375 0 : fd_bank_epoch_schedule_query( slot_ctx->bank ),
376 0 : fd_bank_features_query( slot_ctx->bank ),
377 0 : fd_bank_slot_get( slot_ctx->bank ),
378 0 : new_warmup_cooldown_rate_epoch,
379 0 : _err );
380 0 : if( FD_UNLIKELY( !is_some ) ) {
381 0 : new_warmup_cooldown_rate_epoch = NULL;
382 0 : }
383 :
384 0 : uint128 points = calculate_points_all(
385 0 : slot_ctx,
386 0 : stake_delegations,
387 0 : stake_history,
388 0 : new_warmup_cooldown_rate_epoch,
389 0 : minimum_stake_delegation );
390 :
391 0 : if( points > 0 ) {
392 0 : result->points = points;
393 0 : result->rewards = rewards;
394 0 : }
395 0 : }
396 :
397 : static void
398 : calculate_stake_vote_rewards_account( fd_exec_slot_ctx_t const * slot_ctx,
399 : fd_stake_delegations_t const * stake_delegations,
400 : fd_capture_ctx_t const * capture_ctx,
401 : fd_stake_history_t const * stake_history,
402 : ulong const rewarded_epoch,
403 : ulong * new_warmup_cooldown_rate_epoch,
404 : fd_point_value_t * point_value,
405 : fd_calculate_stake_vote_rewards_result_t * result,
406 : fd_spad_t * spad,
407 0 : int is_recalculation ) {
408 :
409 0 : FD_SPAD_FRAME_BEGIN( spad ) {
410 :
411 0 : ulong minimum_stake_delegation = get_minimum_stake_delegation( slot_ctx );
412 0 : ulong total_stake_rewards = 0UL;
413 0 : ulong dlist_additional_cnt = 0UL;
414 0 : ulong stake_delegation_cnt = fd_stake_delegations_cnt( stake_delegations );
415 :
416 : /* Build a local vote reward map */
417 0 : fd_vote_reward_t_mapnode_t * vote_reward_map_pool = fd_vote_reward_t_map_join( fd_vote_reward_t_map_new( fd_spad_alloc(
418 0 : spad, fd_vote_reward_t_map_align(), fd_vote_reward_t_map_footprint( stake_delegation_cnt ) ), stake_delegation_cnt ) );
419 0 : fd_vote_reward_t_mapnode_t * vote_reward_map_root = NULL;
420 :
421 0 : fd_vote_states_t const * vote_states = NULL;
422 0 : if( !is_recalculation ) {
423 0 : vote_states = fd_bank_vote_states_locking_query( slot_ctx->bank );
424 0 : } else {
425 0 : vote_states = fd_bank_vote_states_prev_locking_query( slot_ctx->bank );
426 0 : }
427 0 : if( FD_UNLIKELY( !vote_states ) ) {
428 0 : FD_LOG_CRIT(( "vote_states is NULL" ));
429 0 : }
430 :
431 0 : fd_stake_delegations_iter_t iter_[1];
432 0 : for( fd_stake_delegations_iter_t * iter = fd_stake_delegations_iter_init( iter_, stake_delegations );
433 0 : !fd_stake_delegations_iter_done( iter );
434 0 : fd_stake_delegations_iter_next( iter ) ) {
435 0 : fd_stake_delegation_t const * stake_delegation = fd_stake_delegations_iter_ele( iter );
436 :
437 0 : if( FD_FEATURE_ACTIVE_BANK( slot_ctx->bank, stake_minimum_delegation_for_rewards ) ) {
438 0 : if( stake_delegation->stake<minimum_stake_delegation ) {
439 0 : continue;
440 0 : }
441 0 : }
442 :
443 0 : fd_pubkey_t const * voter_acc = &stake_delegation->vote_account;
444 0 : fd_vote_state_ele_t * vote_state_ele = fd_vote_states_query( vote_states, voter_acc );
445 0 : if( FD_UNLIKELY( !vote_state_ele ) ) {
446 0 : FD_LOG_DEBUG(( "failed to query vote state" ));
447 0 : continue;
448 0 : }
449 :
450 : /* Note, this doesn't actually redeem any rewards.. this is a misnomer. */
451 0 : fd_calculated_stake_rewards_t calculated_stake_rewards[1] = {0};
452 :
453 0 : int err = redeem_rewards(
454 0 : stake_history,
455 0 : stake_delegation,
456 0 : vote_state_ele,
457 0 : rewarded_epoch,
458 0 : point_value,
459 0 : new_warmup_cooldown_rate_epoch,
460 0 : calculated_stake_rewards );
461 :
462 0 : if( FD_UNLIKELY( err!=0 ) ) {
463 0 : FD_LOG_DEBUG(( "redeem_rewards failed for %s with error %d", FD_BASE58_ENC_32_ALLOCA( &stake_delegation->stake_account ), err ));
464 0 : continue;
465 0 : }
466 :
467 0 : if( capture_ctx ) {
468 0 : fd_solcap_write_stake_reward_event( capture_ctx->capture,
469 0 : &stake_delegation->stake_account,
470 0 : voter_acc,
471 0 : vote_state_ele->commission,
472 0 : (long)calculated_stake_rewards->voter_rewards,
473 0 : (long)calculated_stake_rewards->staker_rewards,
474 0 : (long)calculated_stake_rewards->new_credits_observed );
475 0 : }
476 :
477 : // Find and update the vote reward node in the local map
478 0 : fd_vote_reward_t_mapnode_t vote_map_key[1];
479 0 : vote_map_key->elem.pubkey = *voter_acc;
480 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 );
481 0 : if( FD_UNLIKELY( vote_reward_node==NULL ) ) {
482 0 : FD_LOG_WARNING(( "vote account is missing from the vote rewards pool" ));
483 0 : continue;
484 0 : }
485 :
486 0 : vote_reward_node = fd_vote_reward_t_map_find( vote_reward_map_pool, vote_reward_map_root, vote_map_key );
487 :
488 0 : if( vote_reward_node==NULL ) {
489 0 : vote_reward_node = fd_vote_reward_t_map_acquire( vote_reward_map_pool );
490 0 : vote_reward_node->elem.pubkey = *voter_acc;
491 0 : vote_reward_node->elem.commission = vote_state_ele->commission;
492 0 : vote_reward_node->elem.vote_rewards = calculated_stake_rewards->voter_rewards;
493 0 : vote_reward_node->elem.needs_store = 1;
494 0 : fd_vote_reward_t_map_insert( vote_reward_map_pool, &vote_reward_map_root, vote_reward_node );
495 0 : } else {
496 0 : vote_reward_node->elem.vote_rewards += calculated_stake_rewards->voter_rewards;
497 0 : }
498 :
499 : /* Add the stake reward to list of all stake rewards. The update is
500 : thread-safe because each index in the dlist is only ever accessed
501 : / written to once among all threads. */
502 :
503 :
504 0 : fd_stake_reward_t * stake_reward = fd_stake_reward_calculation_pool_ele_acquire( result->stake_reward_calculation.pool );
505 0 : if( FD_UNLIKELY( !stake_reward ) ) {
506 0 : FD_LOG_CRIT(( "insufficient space allocated for stake reward calculation pool" ));
507 0 : }
508 :
509 0 : fd_memcpy( stake_reward->stake_pubkey.uc, &stake_delegation->stake_account, sizeof(fd_pubkey_t) );
510 0 : stake_reward->lamports = calculated_stake_rewards->staker_rewards;
511 0 : stake_reward->credits_observed = calculated_stake_rewards->new_credits_observed;
512 0 : stake_reward->valid = 1;
513 :
514 : /* Update the total stake rewards */
515 0 : total_stake_rewards += calculated_stake_rewards->staker_rewards;
516 0 : dlist_additional_cnt++;
517 :
518 0 : fd_stake_reward_calculation_dlist_ele_push_tail( result->stake_reward_calculation.stake_rewards, stake_reward, result->stake_reward_calculation.pool );
519 0 : }
520 :
521 0 : if( !is_recalculation ) {
522 0 : fd_bank_vote_states_end_locking_query( slot_ctx->bank );
523 0 : } else {
524 0 : fd_bank_vote_states_prev_end_locking_query( slot_ctx->bank );
525 0 : }
526 :
527 : /* Merge vote rewards with result after */
528 0 : for( fd_vote_reward_t_mapnode_t * vote_reward_node = fd_vote_reward_t_map_minimum( vote_reward_map_pool, vote_reward_map_root );
529 0 : vote_reward_node;
530 0 : vote_reward_node = fd_vote_reward_t_map_successor( vote_reward_map_pool, vote_reward_node ) ) {
531 :
532 0 : fd_vote_reward_t_mapnode_t * result_reward_node = fd_vote_reward_t_map_find( result->vote_reward_map_pool, result->vote_reward_map_root, vote_reward_node );
533 0 : result_reward_node->elem.commission = vote_reward_node->elem.commission;
534 0 : result_reward_node->elem.vote_rewards += vote_reward_node->elem.vote_rewards;
535 0 : result_reward_node->elem.needs_store = 1;
536 0 : }
537 :
538 0 : result->stake_reward_calculation.total_stake_rewards_lamports += total_stake_rewards;
539 0 : result->stake_reward_calculation.stake_rewards_len += dlist_additional_cnt;
540 :
541 0 : } FD_SPAD_FRAME_END;
542 :
543 :
544 0 : }
545 :
546 : /* Calculates epoch rewards for stake/vote accounts.
547 : Returns vote rewards, stake rewards, and the sum of all stake rewards
548 : in lamports.
549 :
550 : In future, the calculation will be cached in the snapshot, but
551 : for now we just re-calculate it (as Agave does).
552 : calculate_stake_vote_rewards is responsible for calculating
553 : stake account rewards based off of a combination of the
554 : stake delegation state as well as the vote account. If this
555 : calculation is done at the end of an epoch, we can just use the
556 : vote states at the end of the current epoch. However, because we
557 : are presumably booting up a node in the middle of rewards
558 : distribution, we need to make sure that we are using the vote
559 : states from the end of the previous epoch.
560 :
561 : https://github.com/anza-xyz/agave/blob/v2.3.1/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L323 */
562 : static void
563 : calculate_stake_vote_rewards( fd_exec_slot_ctx_t * slot_ctx,
564 : fd_stake_delegations_t const * stake_delegations,
565 : fd_capture_ctx_t * capture_ctx,
566 : fd_stake_history_t const * stake_history,
567 : ulong rewarded_epoch,
568 : fd_point_value_t * point_value,
569 : fd_calculate_stake_vote_rewards_result_t * result,
570 : fd_spad_t * runtime_spad,
571 0 : int is_recalculation ) {
572 :
573 0 : int _err[1];
574 0 : ulong new_warmup_cooldown_rate_epoch_val = 0UL;
575 0 : ulong * new_warmup_cooldown_rate_epoch = &new_warmup_cooldown_rate_epoch_val;
576 0 : int is_some = fd_new_warmup_cooldown_rate_epoch(
577 0 : fd_bank_epoch_schedule_query( slot_ctx->bank ),
578 0 : fd_bank_features_query( slot_ctx->bank ),
579 0 : fd_bank_slot_get( slot_ctx->bank ),
580 0 : new_warmup_cooldown_rate_epoch,
581 0 : _err );
582 0 : if( FD_UNLIKELY( !is_some ) ) {
583 0 : new_warmup_cooldown_rate_epoch = NULL;
584 0 : }
585 :
586 0 : ulong rewards_max_count = fd_stake_delegations_cnt( stake_delegations );
587 :
588 : /* Create the stake rewards pool and dlist. The pool will be destoyed after the stake rewards have been distributed. */
589 0 : result->stake_reward_calculation.pool = fd_stake_reward_calculation_pool_join( fd_stake_reward_calculation_pool_new( fd_spad_alloc( runtime_spad,
590 0 : fd_stake_reward_calculation_pool_align(),
591 0 : fd_stake_reward_calculation_pool_footprint( rewards_max_count ) ),
592 0 : rewards_max_count ) );
593 0 : result->stake_reward_calculation.stake_rewards = fd_spad_alloc( runtime_spad,
594 0 : fd_stake_reward_calculation_dlist_align(),
595 0 : fd_stake_reward_calculation_dlist_footprint() );
596 :
597 0 : fd_stake_reward_calculation_dlist_new( result->stake_reward_calculation.stake_rewards );
598 0 : result->stake_reward_calculation.stake_rewards_len = 0UL;
599 :
600 0 : fd_vote_states_t const * vote_states = NULL;
601 0 : if( !is_recalculation ) {
602 0 : vote_states = fd_bank_vote_states_locking_query( slot_ctx->bank );
603 0 : } else {
604 0 : vote_states = fd_bank_vote_states_prev_locking_query( slot_ctx->bank );
605 0 : }
606 0 : if( FD_UNLIKELY( !vote_states ) ) {
607 0 : FD_LOG_CRIT(( "vote_states is NULL" ));
608 0 : }
609 :
610 : /* Create the vote rewards map. This will be destroyed after the vote rewards have been distributed. */
611 0 : ulong vote_account_cnt = fd_vote_states_cnt( vote_states );
612 0 : result->vote_reward_map_pool = fd_vote_reward_t_map_join( fd_vote_reward_t_map_new( fd_spad_alloc( runtime_spad,
613 0 : fd_vote_reward_t_map_align(),
614 0 : fd_vote_reward_t_map_footprint( vote_account_cnt )),
615 0 : vote_account_cnt ) );
616 0 : result->vote_reward_map_root = NULL;
617 :
618 : /* Pre-fill the vote pubkeys in the vote rewards map pool */
619 0 : fd_vote_states_iter_t iter_[1];
620 0 : for( fd_vote_states_iter_t * iter = fd_vote_states_iter_init( iter_, vote_states ); !fd_vote_states_iter_done( iter ); fd_vote_states_iter_next( iter ) ) {
621 0 : fd_vote_state_ele_t const * vote_state = fd_vote_states_iter_ele( iter );
622 :
623 0 : fd_vote_reward_t_mapnode_t * vote_reward_node = fd_vote_reward_t_map_acquire( result->vote_reward_map_pool );
624 :
625 0 : vote_reward_node->elem.pubkey = vote_state->vote_account;
626 0 : vote_reward_node->elem.vote_rewards = 0UL;
627 0 : vote_reward_node->elem.needs_store = 0;
628 :
629 0 : fd_vote_reward_t_map_insert( result->vote_reward_map_pool, &result->vote_reward_map_root, vote_reward_node );
630 0 : }
631 :
632 0 : if( !is_recalculation ) {
633 0 : fd_bank_vote_states_end_locking_query( slot_ctx->bank );
634 0 : } else {
635 0 : fd_bank_vote_states_prev_end_locking_query( slot_ctx->bank );
636 0 : }
637 :
638 : /* Loop over all the delegations
639 : https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L367 */
640 0 : calculate_stake_vote_rewards_account(
641 0 : slot_ctx,
642 0 : stake_delegations,
643 0 : capture_ctx,
644 0 : stake_history,
645 0 : rewarded_epoch,
646 0 : new_warmup_cooldown_rate_epoch,
647 0 : point_value,
648 0 : result,
649 0 : runtime_spad,
650 0 : is_recalculation );
651 0 : }
652 :
653 : /* Calculate epoch reward and return vote and stake rewards.
654 :
655 : https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L273 */
656 : static void
657 : calculate_validator_rewards( fd_exec_slot_ctx_t * slot_ctx,
658 : fd_stake_delegations_t const * stake_delegations,
659 : fd_capture_ctx_t * capture_ctx,
660 : ulong rewarded_epoch,
661 : ulong rewards,
662 : fd_calculate_validator_rewards_result_t * result,
663 0 : fd_spad_t * runtime_spad ) {
664 : /* https://github.com/firedancer-io/solana/blob/dab3da8e7b667d7527565bddbdbecf7ec1fb868e/runtime/src/bank.rs#L2759-L2786 */
665 0 : fd_stake_history_t const * stake_history = fd_sysvar_stake_history_read( slot_ctx->funk, slot_ctx->funk_txn, runtime_spad );
666 0 : if( FD_UNLIKELY( !stake_history ) ) {
667 0 : FD_LOG_ERR(( "Unable to read and decode stake history sysvar" ));
668 0 : }
669 :
670 : /* Calculate the epoch reward points from stake/vote accounts */
671 0 : calculate_reward_points_partitioned(
672 0 : slot_ctx,
673 0 : stake_delegations,
674 0 : stake_history,
675 0 : rewards,
676 0 : &result->point_value );
677 :
678 0 : if( capture_ctx ) {
679 0 : ulong const epoch = fd_bank_epoch_get( slot_ctx->bank );
680 0 : fd_solcap_writer_stake_rewards_begin( capture_ctx->capture,
681 0 : epoch,
682 0 : epoch-1, /* FIXME this is not strictly correct */
683 0 : result->point_value.rewards,
684 0 : result->point_value.points );
685 0 : }
686 :
687 : /* Calculate the stake and vote rewards for each account. We want to
688 : use the vote states from the end of the current_epoch. */
689 0 : calculate_stake_vote_rewards(
690 0 : slot_ctx,
691 0 : stake_delegations,
692 0 : capture_ctx,
693 0 : stake_history,
694 0 : rewarded_epoch,
695 0 : &result->point_value,
696 0 : &result->calculate_stake_vote_rewards_result,
697 0 : runtime_spad,
698 0 : 0 );
699 0 : }
700 :
701 : /* Calculate the number of blocks required to distribute rewards to all stake accounts.
702 :
703 : https://github.com/anza-xyz/agave/blob/9a7bf72940f4b3cd7fc94f54e005868ce707d53d/runtime/src/bank/partitioned_epoch_rewards/mod.rs#L214
704 : */
705 : static ulong
706 : get_reward_distribution_num_blocks( fd_epoch_schedule_t const * epoch_schedule,
707 : ulong slot,
708 0 : ulong total_stake_accounts ) {
709 : /* https://github.com/firedancer-io/solana/blob/dab3da8e7b667d7527565bddbdbecf7ec1fb868e/runtime/src/bank.rs#L1250-L1267 */
710 0 : if( epoch_schedule->warmup &&
711 0 : fd_slot_to_epoch( epoch_schedule, slot, NULL ) < epoch_schedule->first_normal_epoch ) {
712 0 : return 1UL;
713 0 : }
714 :
715 0 : ulong num_chunks = total_stake_accounts / (ulong)STAKE_ACCOUNT_STORES_PER_BLOCK + (total_stake_accounts % STAKE_ACCOUNT_STORES_PER_BLOCK != 0);
716 0 : num_chunks = fd_ulong_max( num_chunks, 1UL );
717 0 : num_chunks = fd_ulong_min( num_chunks,
718 0 : fd_ulong_max( epoch_schedule->slots_per_epoch / (ulong)MAX_FACTOR_OF_REWARD_BLOCKS_IN_EPOCH, 1UL ) );
719 0 : return num_chunks;
720 0 : }
721 :
722 : static void
723 : hash_rewards_into_partitions( fd_bank_t * bank,
724 : fd_stake_reward_calculation_t * stake_reward_calculation,
725 : fd_hash_t const * parent_blockhash,
726 0 : ulong num_partitions ) {
727 :
728 0 : fd_epoch_rewards_t * epoch_rewards = fd_epoch_rewards_join( fd_epoch_rewards_new( fd_bank_epoch_rewards_locking_modify( bank ), FD_RUNTIME_MAX_STAKE_ACCOUNTS ) );
729 0 : if( FD_UNLIKELY( !epoch_rewards ) ) {
730 0 : FD_LOG_CRIT(( "failed to join epoch rewards" ));
731 0 : }
732 0 : fd_epoch_rewards_set_num_partitions( epoch_rewards, num_partitions );
733 :
734 : /* Iterate over all the stake rewards, moving references to them into the appropiate partitions.
735 : IMPORTANT: after this, we cannot use the original stake rewards dlist anymore. */
736 0 : fd_stake_reward_calculation_dlist_iter_t next_iter;
737 0 : for( fd_stake_reward_calculation_dlist_iter_t iter = fd_stake_reward_calculation_dlist_iter_fwd_init( stake_reward_calculation->stake_rewards, stake_reward_calculation->pool );
738 0 : !fd_stake_reward_calculation_dlist_iter_done( iter, stake_reward_calculation->stake_rewards, stake_reward_calculation->pool );
739 0 : iter = next_iter ) {
740 0 : fd_stake_reward_t * stake_reward = fd_stake_reward_calculation_dlist_iter_ele( iter, stake_reward_calculation->stake_rewards, stake_reward_calculation->pool );
741 : /* Cache the next iter here, as we will overwrite the DLIST_NEXT value further down in the loop iteration. */
742 0 : next_iter = fd_stake_reward_calculation_dlist_iter_fwd_next( iter, stake_reward_calculation->stake_rewards, stake_reward_calculation->pool );
743 :
744 0 : if( FD_UNLIKELY( !stake_reward->valid ) ) {
745 0 : continue;
746 0 : }
747 :
748 : /* https://github.com/firedancer-io/solana/blob/dab3da8e7b667d7527565bddbdbecf7ec1fb868e/runtime/src/epoch_rewards_hasher.rs#L43C31-L61 */
749 0 : int err = fd_epoch_rewards_hash_and_insert(
750 0 : epoch_rewards,
751 0 : parent_blockhash,
752 0 : &stake_reward->stake_pubkey,
753 0 : stake_reward->credits_observed,
754 0 : stake_reward->lamports );
755 0 : if( FD_UNLIKELY( err ) ) {
756 0 : FD_LOG_CRIT(( "failed to hash and insert stake reward" ));
757 0 : }
758 0 : }
759 :
760 0 : fd_bank_epoch_rewards_end_locking_modify( bank );
761 0 : }
762 :
763 : /* Calculate rewards from previous epoch to prepare for partitioned distribution.
764 :
765 : https://github.com/anza-xyz/agave/blob/7117ed9653ce19e8b2dea108eff1f3eb6a3378a7/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L214 */
766 : static void
767 : calculate_rewards_for_partitioning( fd_exec_slot_ctx_t * slot_ctx,
768 : fd_stake_delegations_t const * stake_delegations,
769 : fd_capture_ctx_t * capture_ctx,
770 : ulong prev_epoch,
771 : const fd_hash_t * parent_blockhash,
772 : fd_partitioned_rewards_calculation_t * result,
773 0 : fd_spad_t * runtime_spad ) {
774 : /* https://github.com/anza-xyz/agave/blob/7117ed9653ce19e8b2dea108eff1f3eb6a3378a7/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L227 */
775 0 : fd_prev_epoch_inflation_rewards_t rewards;
776 :
777 0 : calculate_previous_epoch_inflation_rewards( slot_ctx->bank,
778 0 : fd_bank_capitalization_get( slot_ctx->bank ),
779 0 : prev_epoch,
780 0 : &rewards );
781 :
782 0 : fd_calculate_validator_rewards_result_t validator_result[1] = {0};
783 0 : calculate_validator_rewards( slot_ctx,
784 0 : stake_delegations,
785 0 : capture_ctx,
786 0 : prev_epoch,
787 0 : rewards.validator_rewards,
788 0 : validator_result,
789 0 : runtime_spad );
790 :
791 0 : fd_stake_reward_calculation_t * stake_reward_calculation = &validator_result->calculate_stake_vote_rewards_result.stake_reward_calculation;
792 0 : fd_epoch_schedule_t const * epoch_schedule = fd_bank_epoch_schedule_query( slot_ctx->bank );
793 :
794 0 : ulong num_partitions = get_reward_distribution_num_blocks(
795 0 : epoch_schedule,
796 0 : fd_bank_slot_get( slot_ctx->bank ),
797 0 : stake_reward_calculation->stake_rewards_len );
798 0 : hash_rewards_into_partitions(
799 0 : slot_ctx->bank,
800 0 : stake_reward_calculation,
801 0 : parent_blockhash,
802 0 : num_partitions );
803 :
804 0 : result->stake_rewards_by_partition.total_stake_rewards_lamports =
805 0 : validator_result->calculate_stake_vote_rewards_result.stake_reward_calculation.total_stake_rewards_lamports;
806 :
807 0 : result->vote_reward_map_pool = validator_result->calculate_stake_vote_rewards_result.vote_reward_map_pool;
808 0 : result->vote_reward_map_root = validator_result->calculate_stake_vote_rewards_result.vote_reward_map_root;
809 0 : result->validator_rewards = rewards.validator_rewards;
810 0 : result->validator_rate = rewards.validator_rate;
811 0 : result->foundation_rate = rewards.foundation_rate;
812 0 : result->prev_epoch_duration_in_years = rewards.prev_epoch_duration_in_years;
813 0 : result->capitalization = fd_bank_capitalization_get( slot_ctx->bank );
814 0 : result->point_value = validator_result->point_value;
815 0 : }
816 :
817 : /* Calculate rewards from previous epoch and distribute vote rewards
818 :
819 : https://github.com/anza-xyz/agave/blob/7117ed9653ce19e8b2dea108eff1f3eb6a3378a7/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L97 */
820 : static void
821 : calculate_rewards_and_distribute_vote_rewards( fd_exec_slot_ctx_t * slot_ctx,
822 : fd_stake_delegations_t const * stake_delegations,
823 : fd_capture_ctx_t * capture_ctx,
824 : ulong prev_epoch,
825 : fd_hash_t const * parent_blockhash,
826 0 : fd_spad_t * runtime_spad ) {
827 :
828 : /* https://github.com/firedancer-io/solana/blob/dab3da8e7b667d7527565bddbdbecf7ec1fb868e/runtime/src/bank.rs#L2406-L2492 */
829 0 : fd_partitioned_rewards_calculation_t rewards_calc_result[1] = {0};
830 0 : calculate_rewards_for_partitioning( slot_ctx,
831 0 : stake_delegations,
832 0 : capture_ctx,
833 0 : prev_epoch,
834 0 : parent_blockhash,
835 0 : rewards_calc_result,
836 0 : runtime_spad );
837 :
838 : /* Iterate over all the vote reward nodes */
839 0 : ulong distributed_rewards = 0UL;
840 0 : for( fd_vote_reward_t_mapnode_t * vote_reward_node = fd_vote_reward_t_map_minimum( rewards_calc_result->vote_reward_map_pool, rewards_calc_result->vote_reward_map_root);
841 0 : vote_reward_node;
842 0 : vote_reward_node = fd_vote_reward_t_map_successor( rewards_calc_result->vote_reward_map_pool, vote_reward_node ) ) {
843 :
844 0 : if( FD_UNLIKELY( !vote_reward_node->elem.needs_store ) ) {
845 0 : continue;
846 0 : }
847 :
848 0 : fd_pubkey_t const * vote_pubkey = &vote_reward_node->elem.pubkey;
849 0 : FD_TXN_ACCOUNT_DECL( vote_rec );
850 0 : fd_funk_rec_prepare_t prepare = {0};
851 :
852 0 : if( FD_UNLIKELY( fd_txn_account_init_from_funk_mutable( vote_rec,
853 0 : vote_pubkey,
854 0 : slot_ctx->funk,
855 0 : slot_ctx->funk_txn,
856 0 : 1,
857 0 : 0UL,
858 0 : &prepare )!=FD_ACC_MGR_SUCCESS ) ) {
859 0 : FD_LOG_ERR(( "Unable to modify vote account" ));
860 0 : }
861 :
862 0 : fd_lthash_value_t prev_hash[1];
863 0 : fd_hashes_account_lthash(
864 0 : vote_pubkey,
865 0 : fd_txn_account_get_meta( vote_rec ),
866 0 : fd_txn_account_get_data( vote_rec ),
867 0 : prev_hash );
868 :
869 0 : fd_txn_account_set_slot( vote_rec, fd_bank_slot_get( slot_ctx->bank ) );
870 :
871 0 : if( FD_UNLIKELY( fd_txn_account_checked_add_lamports( vote_rec, vote_reward_node->elem.vote_rewards ) ) ) {
872 0 : FD_LOG_ERR(( "Adding lamports to vote account would cause overflow" ));
873 0 : }
874 :
875 0 : fd_hashes_update_lthash( vote_rec, prev_hash,slot_ctx->bank, capture_ctx );
876 0 : fd_txn_account_mutable_fini( vote_rec, slot_ctx->funk, slot_ctx->funk_txn, &prepare );
877 :
878 0 : distributed_rewards = fd_ulong_sat_add( distributed_rewards, vote_reward_node->elem.vote_rewards );
879 :
880 0 : if( capture_ctx ) {
881 0 : fd_solcap_write_vote_account_payout( capture_ctx->capture,
882 0 : vote_pubkey,
883 0 : fd_bank_slot_get( slot_ctx->bank ),
884 0 : fd_txn_account_get_lamports( vote_rec ),
885 0 : (long)vote_reward_node->elem.vote_rewards );
886 0 : }
887 0 : }
888 :
889 : /* There is no need to free the vote reward map since it was spad*/
890 :
891 : /* Verify that we didn't pay any more than we expected to */
892 0 : ulong total_rewards = fd_ulong_sat_add( distributed_rewards, rewards_calc_result->stake_rewards_by_partition.total_stake_rewards_lamports );
893 0 : if( FD_UNLIKELY( rewards_calc_result->validator_rewards<total_rewards ) ) {
894 0 : FD_LOG_CRIT(( "Unexpected rewards calculation result" ));
895 0 : }
896 :
897 0 : fd_bank_capitalization_set( slot_ctx->bank, fd_bank_capitalization_get( slot_ctx->bank ) + distributed_rewards );
898 :
899 0 : fd_epoch_rewards_t * epoch_rewards = fd_bank_epoch_rewards_locking_modify( slot_ctx->bank );
900 0 : fd_epoch_rewards_set_distributed_rewards( epoch_rewards, distributed_rewards );
901 0 : fd_epoch_rewards_set_total_rewards( epoch_rewards, rewards_calc_result->point_value.rewards );
902 0 : fd_epoch_rewards_set_total_points( epoch_rewards, rewards_calc_result->point_value.points );
903 0 : fd_bank_epoch_rewards_end_locking_modify( slot_ctx->bank );
904 0 : }
905 :
906 : /* Distributes a single partitioned reward to a single stake account */
907 : static int
908 : distribute_epoch_reward_to_stake_acc( fd_exec_slot_ctx_t * slot_ctx,
909 : fd_capture_ctx_t * capture_ctx,
910 : fd_pubkey_t * stake_pubkey,
911 : ulong reward_lamports,
912 0 : ulong new_credits_observed ) {
913 0 : FD_TXN_ACCOUNT_DECL( stake_acc_rec );
914 0 : fd_funk_rec_prepare_t prepare = {0};
915 0 : if( FD_UNLIKELY( fd_txn_account_init_from_funk_mutable( stake_acc_rec,
916 0 : stake_pubkey,
917 0 : slot_ctx->funk,
918 0 : slot_ctx->funk_txn,
919 0 : 0,
920 0 : 0UL,
921 0 : &prepare )!=FD_ACC_MGR_SUCCESS ) ) {
922 0 : FD_LOG_ERR(( "Unable to modify stake account" ));
923 0 : }
924 :
925 0 : fd_lthash_value_t prev_hash[1];
926 0 : fd_hashes_account_lthash(
927 0 : stake_pubkey,
928 0 : fd_txn_account_get_meta( stake_acc_rec ),
929 0 : fd_txn_account_get_data( stake_acc_rec ),
930 0 : prev_hash );
931 :
932 0 : fd_txn_account_set_slot( stake_acc_rec, fd_bank_slot_get( slot_ctx->bank ) );
933 :
934 0 : fd_stake_state_v2_t stake_state[1] = {0};
935 0 : if( fd_stake_get_state( stake_acc_rec, stake_state ) != 0 ) {
936 0 : FD_LOG_DEBUG(( "failed to read stake state for %s", FD_BASE58_ENC_32_ALLOCA( stake_pubkey ) ));
937 0 : return 1;
938 0 : }
939 :
940 0 : if ( !fd_stake_state_v2_is_stake( stake_state ) ) {
941 0 : FD_LOG_DEBUG(( "non-stake stake account, this should never happen" ));
942 0 : return 1;
943 0 : }
944 :
945 0 : if( fd_txn_account_checked_add_lamports( stake_acc_rec, reward_lamports ) ) {
946 0 : FD_LOG_DEBUG(( "failed to add lamports to stake account" ));
947 0 : return 1;
948 0 : }
949 :
950 0 : ulong old_credits_observed = stake_state->inner.stake.stake.credits_observed;
951 0 : stake_state->inner.stake.stake.credits_observed = new_credits_observed;
952 0 : stake_state->inner.stake.stake.delegation.stake = fd_ulong_sat_add( stake_state->inner.stake.stake.delegation.stake,
953 0 : reward_lamports );
954 :
955 : /* The stake account has just been updated, so we need to update the
956 : stake delegations stored in the bank. */
957 0 : fd_stake_delegations_t * stake_delegations = fd_bank_stake_delegations_delta_locking_modify( slot_ctx->bank );
958 0 : fd_stake_delegations_update(
959 0 : stake_delegations,
960 0 : stake_pubkey,
961 0 : &stake_state->inner.stake.stake.delegation.voter_pubkey,
962 0 : stake_state->inner.stake.stake.delegation.stake,
963 0 : stake_state->inner.stake.stake.delegation.activation_epoch,
964 0 : stake_state->inner.stake.stake.delegation.deactivation_epoch,
965 0 : stake_state->inner.stake.stake.credits_observed,
966 0 : stake_state->inner.stake.stake.delegation.warmup_cooldown_rate );
967 0 : fd_bank_stake_delegations_delta_end_locking_modify( slot_ctx->bank );
968 :
969 0 : if( capture_ctx ) {
970 0 : fd_solcap_write_stake_account_payout( capture_ctx->capture,
971 0 : stake_pubkey,
972 0 : fd_bank_slot_get( slot_ctx->bank ),
973 0 : fd_txn_account_get_lamports( stake_acc_rec ),
974 0 : (long)reward_lamports,
975 0 : new_credits_observed,
976 0 : (long)( new_credits_observed-old_credits_observed ),
977 0 : stake_state->inner.stake.stake.delegation.stake,
978 0 : (long)reward_lamports );
979 0 : }
980 :
981 0 : if( FD_UNLIKELY( write_stake_state( stake_acc_rec, stake_state ) != 0 ) ) {
982 0 : FD_LOG_ERR(( "write_stake_state failed" ));
983 0 : }
984 :
985 0 : fd_hashes_update_lthash( stake_acc_rec, prev_hash, slot_ctx->bank, capture_ctx );
986 0 : fd_txn_account_mutable_fini( stake_acc_rec, slot_ctx->funk, slot_ctx->funk_txn, &prepare );
987 :
988 0 : return 0;
989 0 : }
990 :
991 : /* Sets the epoch reward status to inactive, and destroys any allocated state associated with the active state. */
992 : static void
993 0 : set_epoch_reward_status_inactive( fd_bank_t * bank ) {
994 0 : fd_epoch_rewards_t * epoch_rewards = fd_bank_epoch_rewards_locking_modify( bank );
995 0 : if( fd_epoch_rewards_is_active( epoch_rewards ) ) {
996 0 : FD_LOG_NOTICE(( "Done partitioning rewards for current epoch" ));
997 0 : }
998 0 : fd_epoch_rewards_set_active( epoch_rewards, 0 );
999 0 : fd_bank_epoch_rewards_end_locking_modify( bank );
1000 0 : }
1001 :
1002 : /* Sets the epoch reward status to active.
1003 :
1004 : Takes ownership of the given stake_rewards_by_partition data structure,
1005 : which will be destroyed when set_epoch_reward_status_inactive is called. */
1006 : static void
1007 : set_epoch_reward_status_active( fd_exec_slot_ctx_t * slot_ctx,
1008 0 : ulong distribution_starting_block_height ) {
1009 0 : FD_LOG_NOTICE(( "Setting epoch reward status as active" ));
1010 :
1011 0 : fd_epoch_rewards_t * epoch_rewards = fd_bank_epoch_rewards_locking_modify( slot_ctx->bank );
1012 :
1013 0 : fd_epoch_rewards_set_active( epoch_rewards, 1 );
1014 0 : fd_epoch_rewards_set_starting_block_height( epoch_rewards, distribution_starting_block_height );
1015 0 : fd_bank_epoch_rewards_end_locking_modify( slot_ctx->bank );
1016 0 : }
1017 :
1018 : /* Process reward credits for a partition of rewards.
1019 : Store the rewards to AccountsDB, update reward history record and total capitalization
1020 :
1021 : https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/runtime/src/bank/partitioned_epoch_rewards/distribution.rs#L88 */
1022 : static void
1023 : distribute_epoch_rewards_in_partition( fd_epoch_stake_reward_dlist_t * stake_reward_dlist,
1024 : fd_epoch_stake_reward_t * stake_reward_pool,
1025 : fd_exec_slot_ctx_t * slot_ctx,
1026 0 : fd_capture_ctx_t * capture_ctx ) {
1027 :
1028 0 : ulong lamports_distributed = 0UL;
1029 0 : ulong lamports_burned = 0UL;
1030 :
1031 0 : for( fd_epoch_stake_reward_dlist_iter_t iter = fd_epoch_stake_reward_dlist_iter_fwd_init( stake_reward_dlist, stake_reward_pool );
1032 0 : !fd_epoch_stake_reward_dlist_iter_done( iter, stake_reward_dlist, stake_reward_pool );
1033 0 : iter = fd_epoch_stake_reward_dlist_iter_fwd_next( iter, stake_reward_dlist, stake_reward_pool ) ) {
1034 0 : fd_epoch_stake_reward_t * stake_reward = fd_epoch_stake_reward_dlist_iter_ele( iter, stake_reward_dlist, stake_reward_pool );
1035 0 : if( FD_LIKELY( !distribute_epoch_reward_to_stake_acc(
1036 0 : slot_ctx,
1037 0 : capture_ctx,
1038 0 : &stake_reward->stake_pubkey,
1039 0 : stake_reward->lamports,
1040 0 : stake_reward->credits_observed ) ) ) {
1041 0 : lamports_distributed += stake_reward->lamports;
1042 0 : } else {
1043 0 : lamports_burned += stake_reward->lamports;
1044 0 : }
1045 0 : }
1046 :
1047 : /* Update the epoch rewards sysvar with the amount distributed and burnt */
1048 0 : fd_sysvar_epoch_rewards_distribute( slot_ctx, lamports_distributed + lamports_burned );
1049 :
1050 0 : FD_LOG_DEBUG(( "lamports burned: %lu, lamports distributed: %lu", lamports_burned, lamports_distributed ));
1051 :
1052 0 : fd_bank_capitalization_set( slot_ctx->bank, fd_bank_capitalization_get( slot_ctx->bank ) + lamports_distributed );
1053 0 : }
1054 :
1055 : /* Process reward distribution for the block if it is inside reward interval.
1056 :
1057 : https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/runtime/src/bank/partitioned_epoch_rewards/distribution.rs#L42 */
1058 : void
1059 : fd_distribute_partitioned_epoch_rewards( fd_exec_slot_ctx_t * slot_ctx,
1060 0 : fd_capture_ctx_t * capture_ctx ) {
1061 :
1062 0 : fd_epoch_rewards_t const * epoch_rewards = fd_bank_epoch_rewards_locking_query( slot_ctx->bank );
1063 :
1064 0 : if( !fd_epoch_rewards_is_active( epoch_rewards ) ) {
1065 0 : fd_bank_epoch_rewards_end_locking_query( slot_ctx->bank );
1066 0 : return;
1067 0 : }
1068 :
1069 0 : ulong height = fd_bank_block_height_get( slot_ctx->bank );
1070 0 : ulong distribution_starting_block_height = fd_epoch_rewards_get_starting_block_height( epoch_rewards );
1071 0 : ulong distribution_end_exclusive = fd_epoch_rewards_get_exclusive_ending_block_height( epoch_rewards );
1072 :
1073 0 : fd_epoch_schedule_t const * epoch_schedule = fd_bank_epoch_schedule_query( slot_ctx->bank );
1074 0 : ulong epoch = fd_bank_epoch_get( slot_ctx->bank );
1075 :
1076 0 : if( FD_UNLIKELY( get_slots_in_epoch( epoch, epoch_schedule ) <= fd_epoch_rewards_get_num_partitions( epoch_rewards ) ) ) {
1077 0 : FD_LOG_CRIT(( "Should not be distributing rewards" ));
1078 0 : }
1079 :
1080 0 : if( (height>=distribution_starting_block_height) && (height < distribution_end_exclusive) ) {
1081 :
1082 0 : fd_epoch_stake_reward_t * stake_reward_pool = fd_epoch_rewards_get_stake_reward_pool( epoch_rewards );
1083 0 : if( FD_UNLIKELY( !stake_reward_pool ) ) {
1084 0 : FD_LOG_CRIT(( "failed to get stake reward pool" ));
1085 0 : }
1086 :
1087 0 : ulong partition_index = height - distribution_starting_block_height;
1088 0 : fd_epoch_stake_reward_dlist_t * stake_reward_dlist = fd_epoch_rewards_get_partition_index( epoch_rewards, partition_index );
1089 0 : if( FD_UNLIKELY( !stake_reward_dlist ) ) {
1090 0 : FD_LOG_CRIT(( "failed to get partition dlist" ));
1091 0 : }
1092 :
1093 0 : distribute_epoch_rewards_in_partition( stake_reward_dlist,
1094 0 : stake_reward_pool,
1095 0 : slot_ctx,
1096 0 : capture_ctx );
1097 0 : }
1098 :
1099 0 : fd_bank_epoch_rewards_end_locking_query( slot_ctx->bank );
1100 :
1101 : /* If we have finished distributing rewards, set the status to inactive */
1102 0 : if( fd_ulong_sat_add( height, 1UL ) >= distribution_end_exclusive ) {
1103 0 : set_epoch_reward_status_inactive( slot_ctx->bank );
1104 0 : fd_sysvar_epoch_rewards_set_inactive( slot_ctx );
1105 0 : }
1106 0 : }
1107 :
1108 : /* Partitioned epoch rewards entry-point.
1109 :
1110 : https://github.com/anza-xyz/agave/blob/7117ed9653ce19e8b2dea108eff1f3eb6a3378a7/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L41
1111 : */
1112 : void
1113 : fd_begin_partitioned_rewards( fd_exec_slot_ctx_t * slot_ctx,
1114 : fd_stake_delegations_t const * stake_delegations,
1115 : fd_capture_ctx_t * capture_ctx,
1116 : fd_hash_t const * parent_blockhash,
1117 : ulong parent_epoch,
1118 0 : fd_spad_t * runtime_spad ) {
1119 :
1120 : /* https://github.com/anza-xyz/agave/blob/7117ed9653ce19e8b2dea108eff1f3eb6a3378a7/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L55 */
1121 0 : calculate_rewards_and_distribute_vote_rewards(
1122 0 : slot_ctx,
1123 0 : stake_delegations,
1124 0 : capture_ctx,
1125 0 : parent_epoch,
1126 0 : parent_blockhash,
1127 0 : runtime_spad );
1128 :
1129 : /* https://github.com/anza-xyz/agave/blob/9a7bf72940f4b3cd7fc94f54e005868ce707d53d/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L62 */
1130 0 : ulong distribution_starting_block_height = fd_bank_block_height_get( slot_ctx->bank ) + REWARD_CALCULATION_NUM_BLOCKS;
1131 :
1132 : /* Set the epoch reward status to be active */
1133 0 : set_epoch_reward_status_active( slot_ctx, distribution_starting_block_height );
1134 :
1135 : /* Initialize the epoch rewards sysvar
1136 : https://github.com/anza-xyz/agave/blob/9a7bf72940f4b3cd7fc94f54e005868ce707d53d/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L78 */
1137 0 : fd_epoch_rewards_t const * epoch_rewards = fd_bank_epoch_rewards_locking_query( slot_ctx->bank );
1138 0 : fd_sysvar_epoch_rewards_init(
1139 0 : slot_ctx,
1140 0 : fd_epoch_rewards_get_distributed_rewards( epoch_rewards ),
1141 0 : distribution_starting_block_height,
1142 0 : fd_epoch_rewards_get_num_partitions( epoch_rewards ),
1143 0 : fd_epoch_rewards_get_total_rewards( epoch_rewards ),
1144 0 : fd_epoch_rewards_get_total_points( epoch_rewards ),
1145 0 : parent_blockhash );
1146 0 : fd_bank_epoch_rewards_end_locking_query( slot_ctx->bank );
1147 0 : }
1148 :
1149 : /*
1150 : Re-calculates partitioned stake rewards.
1151 : This updates the slot context's epoch reward status with the recalculated partitioned rewards.
1152 :
1153 : https://github.com/anza-xyz/agave/blob/v2.2.14/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L521 */
1154 : void
1155 : fd_rewards_recalculate_partitioned_rewards( fd_exec_slot_ctx_t * slot_ctx,
1156 : fd_capture_ctx_t * capture_ctx,
1157 0 : fd_spad_t * runtime_spad ) {
1158 0 : FD_SPAD_FRAME_BEGIN( runtime_spad ) {
1159 :
1160 0 : fd_sysvar_epoch_rewards_t epoch_rewards[1];
1161 0 : if( FD_UNLIKELY( !fd_sysvar_epoch_rewards_read( slot_ctx->funk, slot_ctx->funk_txn, epoch_rewards ) ) ) {
1162 0 : FD_LOG_DEBUG(( "Failed to read or decode epoch rewards sysvar - may not have been created yet" ));
1163 0 : set_epoch_reward_status_inactive( slot_ctx->bank );
1164 0 : return;
1165 0 : }
1166 :
1167 0 : FD_LOG_NOTICE(( "recalculating partitioned rewards" ));
1168 :
1169 0 : if( FD_UNLIKELY( epoch_rewards->active ) ) {
1170 :
1171 : /* If partitioned rewards are active, the rewarded epoch is always the immediately
1172 : preceeding epoch.
1173 :
1174 : https://github.com/anza-xyz/agave/blob/2316fea4c0852e59c071f72d72db020017ffd7d0/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L566 */
1175 0 : FD_LOG_NOTICE(( "epoch rewards is active" ));
1176 :
1177 0 : ulong const slot = fd_bank_slot_get( slot_ctx->bank );
1178 0 : ulong const epoch = fd_bank_epoch_get( slot_ctx->bank );
1179 0 : ulong const rewarded_epoch = fd_ulong_sat_sub( epoch, 1UL );
1180 :
1181 0 : int _err[1] = {0};
1182 0 : ulong new_warmup_cooldown_rate_epoch_;
1183 0 : ulong * new_warmup_cooldown_rate_epoch = &new_warmup_cooldown_rate_epoch_;
1184 0 : int is_some = fd_new_warmup_cooldown_rate_epoch(
1185 0 : fd_bank_epoch_schedule_query( slot_ctx->bank ),
1186 0 : fd_bank_features_query( slot_ctx->bank ),
1187 0 : slot,
1188 0 : new_warmup_cooldown_rate_epoch,
1189 0 : _err );
1190 0 : if( FD_UNLIKELY( !is_some ) ) {
1191 0 : new_warmup_cooldown_rate_epoch = NULL;
1192 0 : }
1193 :
1194 0 : fd_stake_history_t const * stake_history = fd_sysvar_stake_history_read( slot_ctx->funk, slot_ctx->funk_txn, runtime_spad );
1195 0 : if( FD_UNLIKELY( !stake_history ) ) {
1196 0 : FD_LOG_ERR(( "Unable to read and decode stake history sysvar" ));
1197 0 : }
1198 :
1199 0 : fd_point_value_t point_value = { .points = epoch_rewards->total_points,
1200 0 : .rewards = epoch_rewards->total_rewards };
1201 :
1202 0 : fd_stake_history_entry_t _accumulator = {
1203 0 : .effective = 0UL,
1204 0 : .activating = 0UL,
1205 0 : .deactivating = 0UL
1206 0 : };
1207 :
1208 0 : fd_stake_delegations_t const * stake_delegations = fd_bank_stake_delegations_frontier_query( slot_ctx->banks, slot_ctx->bank );
1209 0 : if( FD_UNLIKELY( !stake_delegations ) ) {
1210 0 : FD_LOG_CRIT(( "stake_delegations is NULL" ));
1211 0 : }
1212 :
1213 0 : fd_accumulate_stake_infos(
1214 0 : epoch,
1215 0 : stake_delegations,
1216 0 : stake_history,
1217 0 : new_warmup_cooldown_rate_epoch,
1218 0 : &_accumulator );
1219 :
1220 : /* Make sure is_recalculation is ==1 since we are booting up in the
1221 : middle of rewards distribution (so we should use the epoch
1222 : stakes for the end of epoch E-1 since we are still distributing
1223 : rewards for the previous epoch). */
1224 0 : fd_calculate_stake_vote_rewards_result_t calculate_stake_vote_rewards_result[1];
1225 0 : calculate_stake_vote_rewards(
1226 0 : slot_ctx,
1227 0 : stake_delegations,
1228 0 : capture_ctx,
1229 0 : stake_history,
1230 0 : rewarded_epoch,
1231 0 : &point_value,
1232 0 : calculate_stake_vote_rewards_result,
1233 0 : runtime_spad,
1234 0 : 1 /* is_recalculation */ );
1235 :
1236 : /* The vote reward map isn't actually used in this code path and
1237 : will only be freed after rewards have been distributed. */
1238 :
1239 :
1240 : /* Use the epoch rewards sysvar parent_blockhash and num_partitions.
1241 : https://github.com/anza-xyz/agave/blob/v2.2.14/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L579 */
1242 0 : hash_rewards_into_partitions(
1243 0 : slot_ctx->bank,
1244 0 : &calculate_stake_vote_rewards_result->stake_reward_calculation,
1245 0 : &epoch_rewards->parent_blockhash,
1246 0 : epoch_rewards->num_partitions );
1247 :
1248 : /* Update the epoch reward status with the newly re-calculated partitions. */
1249 0 : set_epoch_reward_status_active( slot_ctx, epoch_rewards->distribution_starting_block_height );
1250 0 : } else {
1251 0 : set_epoch_reward_status_inactive( slot_ctx->bank );
1252 0 : }
1253 :
1254 0 : } FD_SPAD_FRAME_END;
1255 0 : }
|