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