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