Line data Source code
1 : #include "fd_rewards.h"
2 : #include "fd_stake_rewards.h"
3 :
4 : #include "../runtime/sysvar/fd_sysvar_epoch_rewards.h"
5 : #include "../runtime/sysvar/fd_sysvar_epoch_schedule.h"
6 : #include "../runtime/sysvar/fd_sysvar_rent.h"
7 : #include "../runtime/fd_hashes.h"
8 : #include "../../ballet/lthash/fd_lthash_adder.h"
9 : #include "../stakes/fd_stakes.h"
10 : #include "../runtime/sysvar/fd_sysvar_stake_history.h"
11 : #include "../runtime/fd_system_ids.h"
12 : #include "../capture/fd_capture_ctx.h"
13 : #include "../runtime/fd_runtime_stack.h"
14 : #include "../runtime/fd_accdb_svm.h"
15 : #include "fd_rewards_base.h"
16 :
17 : #include <math.h>
18 :
19 : /* A note on the calculation of points for inflation rewards at the
20 : epoch boundary.
21 :
22 : As of today there are more than 1.5 million stake delegations on
23 : mainnet (1,583,562 at the 987->988 boundary). Each and every one of
24 : them could in theory earn some lamports of inflation rewards, so at
25 : the boundary each and every delegation is looked at to compute its
26 : share. A delegation's share is directly proportional to its
27 : "points", and the points are essentially the area under a curve where
28 : the y axis is the delegation's effective stake and the x axis is the
29 : monotonically increasing vote credits.
30 :
31 : points = SUM over eligible epochs e of ( effective(e) * credits_owed(e) )
32 :
33 : There are two dimensions to this summation formula. (1) A vote
34 : account monotonically ticks up its vote credit as it votes, and
35 : records its per-epoch credit history as (epoch,final,initial) tuples,
36 : where the [initial,final] ranges are contiguous and non-overlapping.
37 : That is to say, initial[n]=final[n-1]. A stake delegation stores a
38 : watermark, credits_observed, of how far it has already been paid
39 : along the corresponding vote account's credit history. What the
40 : stake delegation is owed for an epoch is the part of that epoch's
41 : [initial,final] that sits above the credits_observed. The credit
42 : history is capped at 64 epochs, so the sum is never more than 64
43 : terms long. The epochs that overlap [credits_observed,final[last]]
44 : are the ones that contribute to the sum. This is the x axis. (2)
45 : Delegated stake ramps up to the full delegated amount over one or
46 : more epochs when it's activating, and ramps down gradually to 0 when
47 : it's deactivating. So the effective stake at an epoch, aka
48 : effective(e), is not always simply the delegated amount. Computing
49 : effective(e) involves running a simulation of the ramp, encoded in
50 : the activating_and_deactivating() function. This is the y axis.
51 :
52 : stake | ______________P________________
53 : | / \
54 : | L U/ warmup cooldown \D R
55 : 0 |_____/ \____________
56 : +-----+---------------------------------+---------------> vote credits
57 : activation activated deactivation cooled down
58 : (ramps up) (effective=delegated) (ramps down) (effective=0)
59 :
60 : So under a reference implementation, the worst case is 64 effective()
61 : computations per delegation. The delegations that tend to hit this
62 : case are dust delegations, either activated or deactivated, whose
63 : tiny or zero points share keeps rounding their rewards down to zero.
64 : As a result, reward doesn't pay out to a dust delegation, so it never
65 : advances its credits_observed, and over time it gets pegged at the
66 : worst case 64-term sum. Roughly 14% of delegations are multi-term
67 : evaluations like this, and they account for ~89% (11.3M of 12.6M) of
68 : all the effective() invocations the points pass does. A minority of
69 : delegations demanding the overwhelming majority of work, and they
70 : barely get any rewards, if at all.
71 :
72 : The good news is that the shape of the warmup/cooldown curve isn't
73 : arbitrary. Observe that within a given boundary, a delegation's
74 : (activation_epoch,deactivation_epoch,stake) are fixed, and on this
75 : frozen tuple the effective stake curve is a single hump aka at most
76 : one peak. The plot above shows the fullest warmup/cooldown curve
77 : within a boundary. It rises during warmup (U) from 0 (L,
78 : epoch<=activation), sits on a flat plateau (P) at exactly the height
79 : of delegated stake, optionally ramps back down (D) after
80 : deactivation, and rests on a flat zero floor (R). It never goes down
81 : and then back up within a given boundary. In practice, the summation
82 : usually runs over just a subsection of this full curve. Depending on
83 : which of the five zones {L,U,P,D,R} the first and the last
84 : contributing terms (the "o"s below) sit on the warmup/cooldown curve,
85 : there can be up to 15 unique possible subsection spans. As of the
86 : 987->988 boundary, the following three cases cover almost the entire
87 : points pass of a reference implementation. The other spans are
88 : either rare or already cheap to evaluate. We exploit the shape of
89 : each case to short circuit step-by-step summation.
90 :
91 : Case 1: Every contributing term sits on the zero floor. The
92 : delegation deactivated at or before the epoch its watermark froze.
93 : This is the common fate of deactivated and abandoned dust stake whose
94 : reward payout stopped at deactivation. This accounts for 79.8% of
95 : the effective() invocations and 88.4% of the effective() iterations
96 : in the points pass. This is the R->R span.
97 :
98 : stake |
99 : |
100 : 0 | o--o--o--o--o
101 : '---------------> vote credits
102 : ^ every contributing term sits past full deactivation
103 :
104 : Case 2: The contributing terms straddle the hump. A few terms ride
105 : the hump, and the rest sit on the zero floor. This accounts for just
106 : 0.05% of the effective() invocations and 0.04% of the effective()
107 : iterations in the points pass. This is a tiny population (230
108 : delegations) but catching the 0-tail of this case is a free side
109 : effect of trying to short circuit Case 1. This is the {L,U,P,D}->R
110 : spans.
111 :
112 : stake | o--o--.
113 : | \
114 : 0 | o--o--o
115 : '-----------------> vote credits
116 : ^ first full deactivation term
117 :
118 : Case 3: Every contributing term sits on the plateau. The delegation
119 : is fully activated and, almost always, never deactivated. This is
120 : the fate of activated dust whose reward keeps rounding down to zero
121 : while its vote account keeps voting. This accounts for 19.8% of the
122 : effective() invocations and 11.6% of the effective() iterations in
123 : the points pass. This is the P->P span. As a side note, P->P also
124 : includes a small sliver, ~14.8K delegations here, that deactivated no
125 : earlier than the epoch of its last contributing term. The stake is
126 : still fully effective at the deactivation epoch itself. The fast
127 : path below doesn't cover these.
128 :
129 : stake | o--o--o--o--o
130 : |
131 : 0 |________________
132 : '---------------> vote credits
133 :
134 : We try to short circuit multi-term Cases 1 and 3, as well as the
135 : multi-term tail floor of Case 2. The short circuit conditions do not
136 : have to be fully precise, they just need to be conservative but not
137 : overly conservative and ideally cheap so as to net a performance win
138 : for most of the case population.
139 :
140 : Further observe that the vast majority (84%, 1.33M of 1.58M) of stake
141 : delegations have an up-to-date (>=initial[last]) credits watermark
142 : and they are almost all either single-term Case 1 or single-term Case
143 : 3. We fast path 1.27M of these with delegation state tags. The
144 : small delta is almost entirely fresh delegations in the just-ended
145 : epoch.
146 :
147 : Note that VAT doesn't make the problem of dust points go away. If we
148 : were to apply VAT on the 987->988 boundary, 96.3% of effective() and
149 : 98.5% of iterations in effective() would still survive.
150 : Unfortunately, there's just a lot of abandoned dust stake pointing at
151 : validators that are still live and voting.
152 :
153 : ===
154 :
155 : For the data minded, the full census of the possible spans at the
156 : reference points pass of the 987->988 boundary:
157 :
158 : span delegations invocations iterations
159 : R->R 182,920 10,067,849 (79.8%) 14,585,005 (88.4%) Case 1
160 : P->R 225 5,745 (0.05%) 6,902 (0.04%) Case 2
161 : L->R 5 164 0 Case 2
162 : P->P 1,327,803 2,500,408 (19.8%) 1,905,636 (11.6%) Case 3
163 : L->L 49,087 49,087 (0.4%) 0
164 : L->P 3 42 11
165 : other 0 0 0
166 : none 23,519 0 0
167 : total 1,583,562 12,623,295 16,497,554
168 :
169 : The L->L span is fresh delegations from the just-ended epoch. Their
170 : contributing term sits at or before the activation epoch, so every
171 : effective() invocation early exits from the all-activating branch
172 : without entering the simulation loop, hence zero iterations. "other"
173 : is the nine spans that include either the U or D ramp zones. They
174 : are all empty, because under today's mainnet warmup/cooldown budget
175 : both ramps complete in a single epoch step, so no term ever observes
176 : a partially warmed or partially cooled stake. "none" is delegations
177 : with no contributing terms, i.e. their watermark already caught up to
178 : the vote credits.
179 :
180 : And broken down by how much each fast/slow path covers:
181 :
182 : span fast/slow path delegations invocations iterations
183 : P->P single-term state tag 1,268,371 / 1,268,371 / 1,094,607
184 : single-term effective() 12,745 / 12,745 / 12,265
185 : multi-term is_warmed_plateau() 44,683 / 1,179,761 / 777,412
186 : slow path 2,004 / 39,531 / 21,352
187 : R->R early exit at 0-tail 181,263 / 10,066,192 / 14,581,965
188 : single-term state tag 22 / 22 / 22
189 : single-term effective() 1,635 / 1,635 / 3,018
190 : L->L single-term effective() 49,006 / 49,006 / 0
191 : single-term state tag 81 / 81 / 0
192 : P->R early exit at 0-tail 225 / 5,745 / 6,902
193 : L->R early exit at 0-tail 5 / 164 / 0
194 : L->P slow path 3 / 42 / 11
195 :
196 : The single-term effective() branch gets taken on state tag misses,
197 : including fresh L->L delegations whose tag is still WARMING/UNKNOWN,
198 : P->P stakes that deactivated during the rewarded epoch (tag COOLING,
199 : though every term is still fully effective) or whose delinquent
200 : vote's last credit entry predates the rewarded epoch, and a few stale
201 : R->R. The 81 L->L state tag hits are accounts that delegated and
202 : deactivated in the same epoch because activation==deactivation
203 : classifies as COOLED with effective 0. The P->P slow path is taken
204 : by multi-term stakes that deactivated no earlier than their last
205 : contributing term, so they get rejected by is_warmed_plateau() and
206 : the 0-tail exit doesn't happen either. */
207 :
208 : /* https://github.com/anza-xyz/agave/blob/7117ed9653ce19e8b2dea108eff1f3eb6a3378a7/sdk/src/inflation.rs#L85 */
209 : static double
210 849 : total( fd_inflation_t const * inflation, double year ) {
211 849 : double tapered = inflation->initial * pow( (1.0 - inflation->taper), year );
212 849 : return (tapered > inflation->terminal) ? tapered : inflation->terminal;
213 849 : }
214 :
215 : /* https://github.com/anza-xyz/agave/blob/7117ed9653ce19e8b2dea108eff1f3eb6a3378a7/sdk/src/inflation.rs#L102 */
216 : static double
217 831 : foundation( fd_inflation_t const * inflation, double year ) {
218 831 : return (year < inflation->foundation_term) ? inflation->foundation * total(inflation, year) : 0.0;
219 831 : }
220 :
221 : /* https://github.com/anza-xyz/agave/blob/7117ed9653ce19e8b2dea108eff1f3eb6a3378a7/sdk/src/inflation.rs#L97 */
222 : static double
223 282 : validator( fd_inflation_t const * inflation, double year) {
224 : /* https://github.com/firedancer-io/solana/blob/dab3da8e7b667d7527565bddbdbecf7ec1fb868e/sdk/src/inflation.rs#L96-L99 */
225 282 : FD_LOG_DEBUG(("Validator Rate: %.16f %.16f %.16f %.16f %.16f", year, total( inflation, year ), foundation( inflation, year ), inflation->taper, inflation->initial));
226 : /* We need 2 independent rounded ops like in Agave. Volatile blocks FMA contraction.
227 : https://github.com/anza-xyz/solana-sdk/blob/inflation%40v3.1.1/inflation/src/lib.rs#L105-L116 */
228 282 : volatile double foundation_portion = foundation( inflation, year );
229 282 : return total( inflation, year ) - foundation_portion;
230 282 : }
231 :
232 : /* Calculates the starting slot for inflation from the activation slot. The activation slot is the earliest
233 : activation slot of the following features:
234 : - devnet_and_testnet
235 : - full_inflation_enable, if full_inflation_vote has been activated
236 :
237 : https://github.com/anza-xyz/agave/blob/7117ed9653ce19e8b2dea108eff1f3eb6a3378a7/runtime/src/bank.rs#L2095 */
238 : static FD_FN_CONST ulong
239 528 : get_inflation_start_slot( fd_bank_t const * bank ) {
240 528 : ulong devnet_and_testnet = FD_FEATURE_ACTIVE_BANK( bank, devnet_and_testnet )
241 528 : ? bank->f.features.devnet_and_testnet
242 528 : : ULONG_MAX;
243 :
244 528 : ulong enable = bank->f.features.full_inflation_enable;
245 :
246 528 : ulong min_slot = fd_ulong_min( enable, devnet_and_testnet );
247 528 : if( min_slot == ULONG_MAX ) {
248 528 : if( FD_FEATURE_ACTIVE_BANK( bank, pico_inflation ) ) {
249 12 : min_slot = bank->f.features.pico_inflation;
250 516 : } else {
251 516 : min_slot = 0;
252 516 : }
253 528 : }
254 528 : return min_slot;
255 528 : }
256 :
257 : /* https://github.com/anza-xyz/agave/blob/v4.2/runtime/src/bank.rs#L2921-L2928 */
258 : static ulong
259 : inflation_start_slot_aligned_to_rewards( fd_bank_t const * bank,
260 528 : fd_epoch_schedule_t const * epoch_schedule ) {
261 528 : ulong inflation_activation_slot = get_inflation_start_slot( bank );
262 528 : return fd_epoch_slot0( epoch_schedule,
263 528 : fd_ulong_sat_sub( fd_slot_to_epoch( epoch_schedule, inflation_activation_slot, NULL ), 1UL ) );
264 528 : }
265 :
266 : /* https://github.com/anza-xyz/agave/blob/v4.2/runtime/src/bank.rs#L2915-L2918 */
267 : static ulong
268 : get_inflation_num_slots( fd_bank_t const * bank,
269 : fd_epoch_schedule_t const * epoch_schedule,
270 264 : ulong slot ) {
271 264 : ulong inflation_start_slot = inflation_start_slot_aligned_to_rewards( bank, epoch_schedule );
272 264 : return fd_epoch_slot0( epoch_schedule, fd_slot_to_epoch( epoch_schedule, slot, NULL ) ) - inflation_start_slot;
273 264 : }
274 :
275 : /* https://github.com/anza-xyz/agave/blob/v4.2/runtime/src/bank.rs#L2931-L2935 */
276 : static double
277 264 : slot_in_year_for_inflation( fd_bank_t const * bank ) {
278 264 : fd_epoch_schedule_t const * epoch_schedule = &bank->f.epoch_schedule;
279 264 : ulong num_slots = get_inflation_num_slots( bank, epoch_schedule, bank->f.slot );
280 264 : ulong inflation_start_slot = inflation_start_slot_aligned_to_rewards( bank, epoch_schedule );
281 264 : return fd_slot_params_slot_range_duration_years( bank,
282 264 : inflation_start_slot, inflation_start_slot + num_slots );
283 264 : }
284 :
285 : /* Returns 1 if effective stake is clearly a warmed plateau.
286 :
287 : This function seeks to conservatively prove that the given stake has
288 : effective=delegated throughout its entire multi-term points
289 : calculation. Concretely, this boils down to the following.
290 :
291 : - The stake is not slated for deactivation. This removes the
292 : downramp.
293 : - The first contributing epoch is > the activation epoch.
294 : - The stake easily activated at activation epoch+1. This removes the
295 : upramp.
296 :
297 : These conditions constrain the warmup/cooldown curve to a flat
298 : plateau at effective=delegated. What makes this conservative is that
299 : there could be false negatives only: this function says that a stake
300 : is not a multi-term plateau when in fact it is. We do a single-step
301 : warmup simulation in this function, and the stake is rejected if it
302 : failed to easily warm up in a single epoch one past the activation
303 : epoch. So a stake that took >=2 epochs to warm up may well be fully
304 : warmed up by the time of its first contributing epoch. We make this
305 : tradeoff because this function is meant to be a fast detector and in
306 : practice most stake activate quickly under today's mainnet warmup
307 : budget. Another case of false negative rejections is for multi-term
308 : stakes that deactivated no earlier than the last contributing epoch.
309 : We could easily eliminate this class of false negatives by passing in
310 : the last contributing epoch and comparing against deactivation epoch.
311 : We make this tradeoff because this class is empirically small and
312 : this function becomes that much easier to reason about by virtue of
313 : cleanly eliminating the deactivation simulation. */
314 : static inline int
315 : is_warmed_plateau( fd_stake_delegation_t const * stake,
316 : ulong first_contributing_epoch,
317 : fd_stake_history_t const * stake_history,
318 : ulong * new_rate_activation_epoch,
319 0 : int use_fixed_point_stake_math ) {
320 : /* Slated for deactivation. */
321 0 : if( stake->deactivation_epoch!=USHORT_MAX ) return 0;
322 :
323 : /* is_bootstrap(): https://github.com/solana-program/stake/blob/interface%40v4.3.1/interface/src/state.rs#L892
324 : Stake activated as per protocol. */
325 0 : if( stake->activation_epoch==USHORT_MAX ) {
326 0 : return 1;
327 0 : }
328 :
329 0 : ulong ae = stake->activation_epoch;
330 0 : if( ae>=first_contributing_epoch ) return 0;
331 :
332 : /* Dropped out of history: https://github.com/solana-program/stake/blob/interface%40v4.3.1/interface/src/state.rs#L969
333 : Stake activated as per protocol. */
334 0 : fd_stake_history_entry_t const * e = fd_sysvar_stake_history_query( stake_history, ae );
335 0 : if( FD_UNLIKELY( !e ) ) {
336 0 : return 1;
337 0 : }
338 :
339 : /* Note that e may not actually be epoch ae's entry on a
340 : non-contiguous window. That is fine here because the reference
341 : simulation's first step reads the exact same entry via the exact
342 : same query and the exact same allowance function below, and
343 : acceptance means the simulation completes warmup on that first
344 : step, before reading any other entry. Similarly, the NULL return
345 : from the query above may also be spurious, and that's fine because
346 : Agave also just assumes fully effective. */
347 :
348 : /* Agave claims this is a "should have been fully effective" branch.
349 : Practically this branch probably won't be taken and so we will
350 : conservatively reject the fast path in this branch. */
351 0 : if( FD_UNLIKELY( e->activating==0UL ) ) return 0;
352 :
353 : /* Run a single-step simulation and see if stake easily activated. */
354 0 : ulong newly_effective;
355 0 : if( use_fixed_point_stake_math ) {
356 0 : newly_effective = fd_ulong_max( fd_stake_calculate_activation_allowance( ae+1UL, stake->stake, e, new_rate_activation_epoch ), 1UL );
357 0 : } else {
358 0 : #if FD_HAS_DOUBLE
359 0 : newly_effective = fd_ulong_max( fd_stake_calculate_change_allowance_float( ae+1UL, stake->stake, e->activating, e->effective, new_rate_activation_epoch ), 1UL );
360 : #else
361 : return 0;
362 : #endif
363 0 : }
364 0 : if( newly_effective>=stake->stake ) {
365 0 : return 1;
366 0 : }
367 0 : return 0;
368 0 : }
369 :
370 : /* Inverted activation and deactivation epochs shouldn't really be
371 : possible on delegations created by the stake program. To be safe, we
372 : will fall back to the slow path if any of these are detected. */
373 : static inline int
374 327 : stake_epochs_are_normal( fd_stake_delegation_t const * stake ) {
375 327 : return stake->activation_epoch==USHORT_MAX ||
376 327 : stake->deactivation_epoch==USHORT_MAX ||
377 327 : stake->activation_epoch<=stake->deactivation_epoch;
378 327 : }
379 :
380 : /* For a given stake and epoch credit history, calculate how many
381 : points, aka (credits * stake) were earned and the new value for
382 : credits_observed if the points were to materialize to non-zero
383 : inflation rewards.
384 :
385 : https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/programs/stake/src/points.rs#L109 */
386 : static void
387 : calculate_stake_points_and_credits( fd_epoch_credits_t const * epoch_credits,
388 : fd_stake_history_t const * stake_history,
389 : fd_stake_delegation_t const * stake,
390 : ulong * new_rate_activation_epoch,
391 : int use_fixed_point_stake_math,
392 228 : fd_calculated_stake_points_t * result ) {
393 :
394 228 : ulong credits_in_stake = stake->credits_observed;
395 228 : ulong credits_cnt = epoch_credits->cnt;
396 228 : ulong base = epoch_credits->base_credits;
397 228 : ulong credits_in_vote = credits_cnt > 0UL ? base + epoch_credits->credits_delta[ credits_cnt - 1UL ] : 0UL;
398 :
399 :
400 : /* If the Vote account has less credits observed than the Stake account,
401 : something is wrong and we need to force an update.
402 :
403 : https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/programs/stake/src/points.rs#L142 */
404 228 : if( FD_UNLIKELY( credits_in_vote < credits_in_stake ) ) {
405 3 : result->points.ud = 0;
406 3 : result->new_credits_observed = credits_in_vote;
407 3 : result->force_credits_update_with_skipped_reward = 1;
408 3 : return;
409 3 : }
410 :
411 : /* If the Vote account has the same amount of credits observed as the Stake account,
412 : then the Vote account hasn't earned any credits and so there is nothing to update.
413 :
414 : https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/programs/stake/src/points.rs#L148 */
415 225 : if( FD_UNLIKELY( credits_in_vote == credits_in_stake ) ) {
416 225 : result->points.ud = 0;
417 225 : result->new_credits_observed = credits_in_vote;
418 225 : result->force_credits_update_with_skipped_reward = 0;
419 225 : return;
420 225 : }
421 :
422 0 : int coalesce_eligible = stake_epochs_are_normal( stake ) && epoch_credits->fast_path_ok;
423 :
424 : /* Calculate the points for each epoch credit */
425 0 : uint128 points = 0;
426 0 : ulong new_credits_observed = credits_in_stake;
427 0 : for( ulong i=0UL; i<epoch_credits->cnt; i++ ) {
428 :
429 0 : ulong final_epoch_credits = base + epoch_credits->credits_delta[ i ];
430 0 : ulong initial_epoch_credits = base + epoch_credits->prev_credits_delta[ i ];
431 :
432 : /* All production inputs should satisfy
433 : initial_epoch_credits <= final_epoch_credits
434 :
435 : If final_epoch_credits <= credits_in_stake, then:
436 : initial_epoch_credits <= final_epoch_credits <= credits_in_stake
437 :
438 : * earned_credits = 0 since both conditions are false.
439 : * new_credits_observed stays the same since it is already set
440 : to credits_in_stake and final_epoch_credits <= credits_in_stake
441 :
442 : Since earned_credits = 0 and new_credits_observed stays the same,
443 : points computation can be skipped. */
444 0 : if( FD_LIKELY( epoch_credits->fast_path_ok && final_epoch_credits<=credits_in_stake ) ) continue;
445 :
446 0 : uint128 earned_credits = 0;
447 0 : if( FD_LIKELY( credits_in_stake < initial_epoch_credits ) ) {
448 0 : earned_credits = (uint128)(final_epoch_credits - initial_epoch_credits);
449 0 : } else if( FD_UNLIKELY( credits_in_stake < final_epoch_credits ) ) {
450 0 : earned_credits = (uint128)(final_epoch_credits - new_credits_observed);
451 0 : }
452 :
453 0 : new_credits_observed = fd_ulong_max( new_credits_observed, final_epoch_credits );
454 :
455 0 : ulong stake_amount = fd_stakes_activating_and_deactivating( stake, epoch_credits->epoch[ i ], stake_history, new_rate_activation_epoch, use_fixed_point_stake_math ).effective;
456 0 : if( coalesce_eligible && stake_amount==0UL && epoch_credits->epoch[ i ]>stake->deactivation_epoch ) {
457 : /* Multi-term Cases 1 and 2. Note that
458 : deactivation_epoch!=USHORT_MAX is implied since epoch[ i ] is
459 : also a ushort. */
460 0 : new_credits_observed = credits_in_vote;
461 0 : break;
462 0 : }
463 :
464 0 : points += (uint128)stake_amount * earned_credits;
465 0 : }
466 :
467 0 : result->points.ud = points;
468 0 : result->new_credits_observed = new_credits_observed;
469 0 : result->force_credits_update_with_skipped_reward = 0;
470 0 : }
471 :
472 : /* Returns commission split as
473 : (voter_portion, staker_portion, was_split) tuple. If commission
474 : calculation is 10000 (100%) one way or other, indicate with false for
475 : was_split.
476 :
477 : https://github.com/anza-xyz/agave/blob/v4.0.0-beta.6/runtime/src/inflation_rewards/mod.rs#L237-L272 */
478 : void
479 : fd_vote_commission_split( ushort commission,
480 : ulong on,
481 129 : fd_commission_split_t * result ) {
482 : /* https://github.com/anza-xyz/agave/blob/v4.0.0-beta.6/runtime/src/inflation_rewards/mod.rs#L244-L245 */
483 324 : #define MAX_BPS (10000)
484 :
485 : /* https://github.com/anza-xyz/agave/blob/v4.0.0-beta.6/runtime/src/inflation_rewards/mod.rs#L246-L271 */
486 129 : ushort commission_split = fd_ushort_min( commission, MAX_BPS );
487 129 : switch( commission_split ) {
488 24 : case 0: {
489 : /* https://github.com/anza-xyz/agave/blob/v4.0.0-beta.6/runtime/src/inflation_rewards/mod.rs#L246 */
490 24 : result->voter_portion = 0UL;
491 24 : result->staker_portion = on;
492 24 : result->is_split = 0;
493 24 : break;
494 0 : }
495 60 : case MAX_BPS: {
496 : /* https://github.com/anza-xyz/agave/blob/v4.0.0-beta.6/runtime/src/inflation_rewards/mod.rs#L247 */
497 60 : result->voter_portion = on;
498 60 : result->staker_portion = 0UL;
499 60 : result->is_split = 0;
500 60 : break;
501 0 : }
502 45 : default: {
503 : /* https://github.com/anza-xyz/agave/blob/v4.0.0-beta.6/runtime/src/inflation_rewards/mod.rs#L256-L259 */
504 45 : result->voter_portion = (ulong)((uint128)on * (uint128)commission_split / (uint128)MAX_BPS);
505 45 : result->staker_portion = (ulong)((uint128)on * (uint128)(MAX_BPS-commission_split) / (uint128)MAX_BPS);
506 45 : result->is_split = 1;
507 45 : break;
508 0 : }
509 129 : }
510 :
511 129 : #undef MAX_BPS
512 129 : }
513 :
514 : /* Validates an external SIMD-0232 inflation commission collector
515 : after all rewards routed to it have been added. Returns 0 to
516 : deposit, 1 to burn. The vote account itself is always valid and
517 : must not be passed here.
518 : https://github.com/anza-xyz/agave/blob/v4.2.0-beta.1/runtime/src/bank/fee_distribution.rs#L231-L270 */
519 : static int
520 : fd_rewards_validate_commission_collector( fd_bank_t const * bank,
521 : fd_pubkey_t const * collector,
522 : fd_acc_t const * account,
523 42 : ulong rewards ) {
524 : /* Must be a system program owned account. */
525 42 : if( FD_UNLIKELY( memcmp( account->owner, fd_solana_system_program_id.uc, sizeof(fd_pubkey_t) ) ) ) return 1;
526 :
527 : /* Must not be a reserved account. */
528 33 : if( FD_UNLIKELY( fd_pubkey_is_active_reserved_key( collector ) ||
529 33 : fd_pubkey_is_pending_reserved_key( collector ) ) ) return 1;
530 :
531 30 : ulong post_balance;
532 30 : if( FD_UNLIKELY( __builtin_uaddl_overflow( account->lamports, rewards, &post_balance ) ) ) return 1;
533 :
534 : /* The incinerator is exempt from the rent check so the deposit
535 : always works. Incinerator funds are burned at the end of the
536 : rewards distribution block. */
537 27 : if( FD_UNLIKELY( fd_pubkey_eq( collector, &fd_sysvar_incinerator_id ) ) ) return 0;
538 :
539 : /* Must be rent-exempt after the deposit. With
540 : relax_post_exec_min_balance_check (SIMD-0392) a pre-existing
541 : account may stay rent-paying. */
542 24 : int is_rent_exempt = post_balance>=fd_rent_exempt_minimum_balance( &bank->f.rent, account->data_len );
543 24 : return !is_rent_exempt &&
544 24 : ( !FD_FEATURE_ACTIVE_BANK( bank, relax_post_exec_min_balance_check ) || !account->lamports );
545 27 : }
546 :
547 : /* Resolves the SIMD-0232 inflation commission collector from the vote
548 : account state at the start of the distribution epoch (tag
549 : bank->f.epoch); defaults to the vote account.
550 : https://github.com/anza-xyz/agave/blob/v4.2.0-beta.1/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L667-L672 */
551 : static void
552 : fd_rewards_inflation_collector( fd_bank_t * bank,
553 : fd_pubkey_t const * vote_pubkey,
554 60 : fd_pubkey_t * collector_out ) {
555 60 : *collector_out = *vote_pubkey;
556 60 : fd_collector_overrides_query( fd_bank_collector_overrides( bank ),
557 60 : bank->collector_overrides_fork_id,
558 60 : bank->f.epoch,
559 60 : vote_pubkey,
560 60 : collector_out,
561 60 : NULL );
562 60 : }
563 :
564 : /* https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/programs/stake/src/rewards.rs#L33 */
565 : static int
566 : redeem_rewards( fd_stake_delegation_t const * stake,
567 : ulong vote_state_idx,
568 : ulong rewarded_epoch,
569 : ulong total_rewards,
570 : uint128 total_points,
571 : fd_runtime_stack_t * runtime_stack,
572 : fd_calculated_stake_points_t * stake_points_result,
573 327 : fd_calculated_stake_rewards_t * result ) {
574 :
575 : /* The firedancer implementation of redeem_rewards inlines a lot of
576 : the helper functions that the Agave implementation uses.
577 : In Agave: redeem_rewards calls redeem_stake_rewards which calls
578 : calculate_stake_rewards. */
579 :
580 : // Drive credits_observed forward unconditionally when rewards are disabled
581 : // or when this is the stake's activation epoch
582 327 : if( total_rewards==0UL || stake->activation_epoch==rewarded_epoch ) {
583 240 : stake_points_result->force_credits_update_with_skipped_reward = 1;
584 240 : }
585 :
586 327 : if( stake_points_result->force_credits_update_with_skipped_reward ) {
587 240 : result->staker_rewards = 0;
588 240 : result->voter_rewards = 0;
589 240 : result->new_credits_observed = stake_points_result->new_credits_observed;
590 240 : return 0;
591 240 : }
592 87 : if( stake_points_result->points.ud==0 || total_points==0 ) {
593 0 : return 1;
594 0 : }
595 :
596 87 : uint128 rewards_u128;
597 87 : if( FD_UNLIKELY( __builtin_mul_overflow( stake_points_result->points.ud, (uint128)(total_rewards), &rewards_u128 ) ) ) {
598 0 : FD_LOG_ERR(( "Rewards intermediate calculation should fit within u128" ));
599 0 : }
600 :
601 87 : FD_TEST( total_points );
602 87 : rewards_u128 /= (uint128) total_points;
603 :
604 87 : if( FD_UNLIKELY( rewards_u128>(uint128)ULONG_MAX ) ) {
605 0 : FD_LOG_ERR(( "Rewards should fit within u64" ));
606 0 : }
607 :
608 87 : ulong rewards = (ulong)rewards_u128;
609 87 : if( rewards == 0 ) {
610 0 : return 1;
611 0 : }
612 :
613 87 : fd_commission_split_t split_result;
614 87 : fd_vote_commission_split( runtime_stack->stakes.vote_ele[ vote_state_idx ].commission, rewards, &split_result );
615 87 : if( split_result.is_split && (split_result.voter_portion == 0 || split_result.staker_portion == 0) ) {
616 3 : return 1;
617 3 : }
618 :
619 84 : result->staker_rewards = split_result.staker_portion;
620 84 : result->voter_rewards = split_result.voter_portion;
621 84 : result->new_credits_observed = stake_points_result->new_credits_observed;
622 84 : return 0;
623 87 : }
624 :
625 : /* Returns the length of the given epoch in slots
626 :
627 : https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/sdk/program/src/epoch_schedule.rs#L103 */
628 : static ulong
629 : get_slots_in_epoch( ulong epoch,
630 399 : fd_epoch_schedule_t const * epoch_schedule ) {
631 399 : return epoch < epoch_schedule->first_normal_epoch ?
632 0 : 1UL << fd_ulong_sat_add( epoch, FD_EPOCH_LEN_MIN_TRAILING_ZERO ) :
633 399 : epoch_schedule->slots_per_epoch;
634 399 : }
635 :
636 : /* https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/runtime/src/bank.rs#L2082 */
637 : static double
638 : epoch_duration_in_years( fd_bank_t const * bank,
639 264 : ulong prev_epoch ) {
640 264 : fd_epoch_schedule_t const * epoch_schedule = &bank->f.epoch_schedule;
641 264 : ulong slots_in_epoch = get_slots_in_epoch( prev_epoch, epoch_schedule );
642 264 : double slots_per_year = fd_slot_params_at_slot( bank,
643 264 : fd_epoch_slot0( epoch_schedule, prev_epoch ) ).slots_per_year;
644 264 : return (double)slots_in_epoch / slots_per_year;
645 264 : }
646 :
647 : /* https://github.com/anza-xyz/agave/blob/7117ed9653ce19e8b2dea108eff1f3eb6a3378a7/runtime/src/bank.rs#L2128 */
648 : static void
649 : calculate_previous_epoch_inflation_rewards( fd_bank_t const * bank,
650 : ulong prev_epoch_capitalization,
651 : ulong prev_epoch,
652 264 : fd_prev_epoch_inflation_rewards_t * rewards ) {
653 264 : double slot_in_year = slot_in_year_for_inflation( bank );
654 :
655 264 : rewards->validator_rate = validator( &bank->f.inflation, slot_in_year );
656 264 : rewards->foundation_rate = foundation( &bank->f.inflation, slot_in_year );
657 264 : rewards->prev_epoch_duration_in_years = epoch_duration_in_years( bank, prev_epoch );
658 264 : rewards->validator_rewards = (ulong)(rewards->validator_rate * (double)prev_epoch_capitalization * rewards->prev_epoch_duration_in_years);
659 264 : 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 ));
660 264 : }
661 :
662 : /* Calculate the number of blocks required to distribute rewards to all stake accounts.
663 :
664 : https://github.com/anza-xyz/agave/blob/9a7bf72940f4b3cd7fc94f54e005868ce707d53d/runtime/src/bank/partitioned_epoch_rewards/mod.rs#L214
665 : */
666 : static uint
667 : get_reward_distribution_num_blocks( fd_epoch_schedule_t const * epoch_schedule,
668 : ulong slot,
669 : ulong total_stake_accounts,
670 276 : ulong stake_account_stores_per_block ) {
671 : /* https://github.com/firedancer-io/solana/blob/dab3da8e7b667d7527565bddbdbecf7ec1fb868e/runtime/src/bank.rs#L1250-L1267 */
672 276 : if( epoch_schedule->warmup &&
673 276 : fd_slot_to_epoch( epoch_schedule, slot, NULL ) < epoch_schedule->first_normal_epoch ) {
674 3 : return 1UL;
675 3 : }
676 :
677 273 : FD_TEST( stake_account_stores_per_block );
678 273 : ulong num_chunks = total_stake_accounts / stake_account_stores_per_block + (total_stake_accounts % stake_account_stores_per_block != 0);
679 273 : num_chunks = fd_ulong_max( num_chunks, 1UL );
680 273 : num_chunks = fd_ulong_min( num_chunks,
681 273 : fd_ulong_max( epoch_schedule->slots_per_epoch / (ulong)MAX_FACTOR_OF_REWARD_BLOCKS_IN_EPOCH, 1UL ) );
682 273 : return (uint)num_chunks;
683 273 : }
684 :
685 : uint
686 : fd_rewards_get_reward_distribution_num_blocks( fd_epoch_schedule_t const * epoch_schedule,
687 : ulong slot,
688 : ulong total_stake_accounts,
689 12 : ulong stake_account_stores_per_block ) {
690 12 : return get_reward_distribution_num_blocks( epoch_schedule, slot, total_stake_accounts, stake_account_stores_per_block );
691 12 : }
692 :
693 : static void
694 : read_stake_history( fd_accdb_t * accdb,
695 : fd_accdb_fork_id_t fork_id,
696 : uchar data[ static FD_SYSVAR_STAKE_HISTORY_BINCODE_SZ ],
697 267 : fd_stake_history_t * stake_history ) {
698 267 : fd_acc_t ro = fd_accdb_read_one( accdb, fork_id, fd_sysvar_stake_history_id.uc );
699 267 : if( FD_UNLIKELY( !ro.lamports ) ) FD_LOG_ERR(( "Unable to read stake history sysvar" ));
700 267 : ulong copy_sz = fd_ulong_min( ro.data_len, FD_SYSVAR_STAKE_HISTORY_BINCODE_SZ );
701 267 : fd_memcpy( data, ro.data, copy_sz );
702 267 : fd_accdb_unread_one( accdb, &ro );
703 267 : if( FD_UNLIKELY( !fd_sysvar_stake_history_view( stake_history, data, copy_sz ) ) ) {
704 0 : FD_LOG_ERR(( "Unable to decode stake history sysvar" ));
705 0 : }
706 267 : }
707 :
708 : /* calculate_stake_points_and_credits() with some fast paths. */
709 : static inline void
710 : calculate_stake_points_fast( fd_epoch_credits_t * epoch_credits,
711 : fd_stake_history_t const * stake_history,
712 : fd_stake_delegation_t const * stake,
713 : ulong * new_rate_activation_epoch,
714 : int use_fixed_point_stake_math,
715 : ulong rewarded_epoch,
716 327 : fd_calculated_stake_points_t * result ) {
717 327 : if( FD_UNLIKELY( !stake_epochs_are_normal( stake ) ) ) {
718 0 : FD_BASE58_ENCODE_32_BYTES( stake->stake_account.uc, stake_account_str );
719 0 : FD_BASE58_ENCODE_32_BYTES( stake->vote_account.uc, vote_account_str );
720 0 : FD_LOG_INFO(( "stake delegation (stake_account=%s vote_account=%s delegated=%lu balance=%lu credits_observed=%lu) activation epoch %u > deactivation epoch %u",
721 0 : stake_account_str, vote_account_str, stake->stake, stake->lamports, stake->credits_observed, stake->activation_epoch, stake->deactivation_epoch ));
722 0 : calculate_stake_points_and_credits( epoch_credits, stake_history, stake, new_rate_activation_epoch, use_fixed_point_stake_math, result );
723 0 : return;
724 0 : }
725 :
726 327 : ulong cnt = epoch_credits->cnt;
727 327 : if( FD_LIKELY( epoch_credits->fast_path_ok && cnt ) ) {
728 102 : ulong base = epoch_credits->base_credits;
729 102 : ulong credits_in_stake = stake->credits_observed;
730 102 : ulong credits_in_vote = base+epoch_credits->credits_delta[ cnt-1UL ];
731 102 : if( FD_LIKELY( credits_in_vote>credits_in_stake ) ) {
732 99 : ulong initial_last = base+epoch_credits->prev_credits_delta[ cnt-1UL ];
733 99 : int fast = 0;
734 :
735 99 : if( FD_LIKELY( credits_in_stake>=initial_last ) ) {
736 : /* Single-term. */
737 99 : ulong target_epoch = epoch_credits->epoch[ cnt-1UL ];
738 99 : ulong effective_stake;
739 99 : if( FD_LIKELY( target_epoch==rewarded_epoch && (stake->state==FD_STAKE_DELEGATION_STATE_WARMED||stake->state==FD_STAKE_DELEGATION_STATE_COOLED) ) ) { /* See the block comment for state tags for why we need target_epoch==rewarded_epoch. */
740 : /* Single-term Case 3 or Case 1. */
741 0 : effective_stake = stake->state==FD_STAKE_DELEGATION_STATE_WARMED ? stake->stake : 0UL;
742 99 : } else {
743 : /* We could let this branch fall through to the slow path,
744 : whose loop will skip a whole bunch of epoch credits only to
745 : get to the final and only contributing term. Computing it
746 : right here reduces about 800 instructions retired per such
747 : delegation. */
748 99 : effective_stake = fd_stakes_activating_and_deactivating( stake, target_epoch, stake_history, new_rate_activation_epoch, use_fixed_point_stake_math ).effective;
749 99 : }
750 99 : result->points.ud = (uint128)effective_stake * (uint128)( credits_in_vote - credits_in_stake );
751 99 : result->new_credits_observed = credits_in_vote;
752 99 : fast = 1;
753 99 : } else {
754 : /* Multi-term. */
755 0 : ulong first_contributing_idx = 0UL;
756 0 : while( base+epoch_credits->credits_delta[ first_contributing_idx ]<=credits_in_stake ) first_contributing_idx++;
757 0 : FD_TEST( first_contributing_idx<cnt ); /* Guaranteed found because of the earlier credits_in_vote>credits_in_stake gate. */
758 : /* Multi-term Case 3. effective=delegated at the earliest
759 : contributing term, and no deactivation at all, so the plateau
760 : simplifies the points calculation to a single closed form
761 : multiplication. */
762 0 : if( FD_LIKELY( is_warmed_plateau( stake, epoch_credits->epoch[ first_contributing_idx ], stake_history, new_rate_activation_epoch, use_fixed_point_stake_math ) ) ) {
763 0 : ulong start_credits = fd_ulong_max( credits_in_stake, base+epoch_credits->prev_credits_delta[ 0UL ] );
764 0 : result->points.ud = (uint128)stake->stake*(uint128)(credits_in_vote-start_credits);
765 0 : result->new_credits_observed = credits_in_vote;
766 0 : fast = 1;
767 0 : }
768 0 : }
769 :
770 99 : if( FD_LIKELY( fast ) ) {
771 99 : result->force_credits_update_with_skipped_reward = 0;
772 99 : return;
773 99 : }
774 99 : }
775 102 : }
776 : /* Potentially term-by-term slow path fallback for anything we can't
777 : conservatively prove to take the fast paths so far. In this
778 : callee, early exit at the first fully deactivated term is the
779 : 0-tail short circuit fast path for multi-term Cases 1 and 2. */
780 228 : calculate_stake_points_and_credits( epoch_credits, stake_history, stake, new_rate_activation_epoch, use_fixed_point_stake_math, result );
781 228 : }
782 :
783 : /* Calculates epoch reward points from stake/vote accounts.
784 : https://github.com/anza-xyz/agave/blob/v2.3.1/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L445 */
785 : static uint128
786 : calculate_reward_points_partitioned( fd_bank_t * bank,
787 : fd_accdb_t * accdb,
788 : fd_stake_delegations_t const * stake_delegations,
789 : fd_stake_history_t const * stake_history,
790 : ulong rewarded_epoch,
791 264 : fd_runtime_stack_t * runtime_stack ) {
792 : /* Calculate the points for each stake delegation */
793 264 : uint128 total_points = 0;
794 :
795 264 : fd_vote_rewards_t * vote_ele = runtime_stack->stakes.vote_ele;
796 264 : fd_vote_rewards_map_t * vote_ele_map = runtime_stack->stakes.vote_map;
797 264 : fd_epoch_credits_t * epoch_credits_arr = fd_bank_epoch_credits( bank );
798 :
799 264 : fd_stake_delegations_iter_t iter_[1];
800 264 : for( fd_stake_delegations_iter_t * iter = fd_stake_delegations_iter_init( iter_, stake_delegations, accdb, bank->accdb_fork_id, bank->f.epoch, &bank->f.warmup_cooldown_rate_epoch );
801 597 : !fd_stake_delegations_iter_done( iter );
802 333 : fd_stake_delegations_iter_next( iter ) ) {
803 333 : fd_stake_delegation_t const * stake_delegation = fd_stake_delegations_iter_ele( iter );
804 333 : ulong stake_delegation_idx = fd_stake_delegations_iter_idx( iter );
805 :
806 : /* Note that we don't check minimum delegation here, as there are
807 : no plans to activate stake_minimum_delegation_for_rewards.
808 : If this changes we need to skip stake accounts that are
809 : below the minimum delegation here. However we don't do this yet,
810 : to ensure that we audit the feature properly if this happens. */
811 :
812 333 : uint idx = (uint)fd_vote_rewards_map_idx_query( vote_ele_map, &stake_delegation->vote_account, UINT_MAX, vote_ele );
813 :
814 333 : if( FD_LIKELY( stake_delegation_idx<runtime_stack->max_stake_accounts ) ) {
815 333 : runtime_stack->stakes.stake_points_result[ stake_delegation_idx ].vote_idx = idx;
816 333 : }
817 :
818 333 : if( FD_UNLIKELY( idx==UINT_MAX ) ) continue;
819 :
820 324 : fd_calculated_stake_points_t stake_points_result_[1];
821 324 : fd_calculated_stake_points_t * stake_points_result;
822 324 : if( FD_UNLIKELY( stake_delegation_idx>=runtime_stack->max_stake_accounts ) ) {
823 0 : stake_points_result = stake_points_result_;
824 324 : } else {
825 324 : stake_points_result = &runtime_stack->stakes.stake_points_result[ stake_delegation_idx ];
826 324 : }
827 :
828 324 : fd_epoch_credits_t * epoch_credits = &epoch_credits_arr[ idx ];
829 :
830 324 : calculate_stake_points_fast( epoch_credits,
831 324 : stake_history,
832 324 : stake_delegation,
833 324 : &bank->f.warmup_cooldown_rate_epoch,
834 324 : FD_FEATURE_ACTIVE_BANK( bank, upgrade_bpf_stake_program_to_v5_1 ),
835 324 : rewarded_epoch,
836 324 : stake_points_result );
837 :
838 324 : total_points += stake_points_result->points.ud;
839 324 : }
840 :
841 264 : return total_points;
842 264 : }
843 :
844 : /* https://github.com/anza-xyz/agave/blob/v4.2.0-beta.0/runtime/src/inflation_rewards/mod.rs#L161-L173 */
845 : static int
846 : delegation_may_need_adjustment( fd_bank_t * bank,
847 : fd_stake_delegation_t const * stake_delegation,
848 : fd_stake_history_t const * stake_history,
849 : ulong rewarded_epoch,
850 : ulong new_delegation_with_rewards,
851 : ulong lamports_with_rewards,
852 0 : ulong minimum_lamports ) {
853 0 : ulong new_delegation = fd_ulong_min(
854 0 : new_delegation_with_rewards,
855 0 : fd_ulong_sat_sub( lamports_with_rewards, minimum_lamports )
856 0 : );
857 :
858 0 : if( FD_LIKELY( new_delegation==stake_delegation->stake ) ) return 0;
859 :
860 : /* Delegations that are neither effective nor activating are left
861 : alone. This is checked last because it can walk many stake history
862 : entries. */
863 0 : fd_stake_history_entry_t status = fd_stakes_activating_and_deactivating(
864 0 : stake_delegation,
865 0 : rewarded_epoch,
866 0 : stake_history,
867 0 : &bank->f.warmup_cooldown_rate_epoch,
868 0 : FD_FEATURE_ACTIVE_BANK( bank, upgrade_bpf_stake_program_to_v5_1 ) );
869 :
870 0 : return !( status.effective==0UL && status.activating==0UL );
871 0 : }
872 :
873 : /* Calculates epoch rewards for stake/vote accounts.
874 : Returns vote rewards, stake rewards, and the sum of all stake rewards
875 : in lamports.
876 :
877 : In the future, the calculation will be cached in the snapshot, but
878 : for now we just re-calculate it (as Agave does).
879 : calculate_stake_vote_rewards is responsible for calculating
880 : stake account rewards based off of a combination of the
881 : stake delegation state as well as the vote account. If this
882 : calculation is done at the end of an epoch, we can just use the
883 : vote states at the end of the current epoch. However, because we
884 : are presumably booting up a node in the middle of rewards
885 : distribution, we need to make sure that we are using the vote
886 : states from the end of the previous epoch.
887 :
888 : https://github.com/anza-xyz/agave/blob/v2.3.1/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L323 */
889 : static void
890 : calculate_stake_vote_rewards( fd_bank_t * bank,
891 : fd_accdb_t * accdb,
892 : fd_stake_delegations_t const * stake_delegations,
893 : fd_capture_ctx_t * capture_ctx FD_PARAM_UNUSED,
894 : fd_stake_history_t const * stake_history,
895 : ulong rewarded_epoch,
896 : ulong total_rewards,
897 : uint128 total_points,
898 : fd_runtime_stack_t * runtime_stack,
899 267 : int is_recalculation ) {
900 :
901 267 : runtime_stack->stakes.stake_rewards_cnt = 0UL;
902 :
903 267 : fd_calculated_stake_rewards_t calculated_stake_rewards_[1];
904 267 : fd_epoch_credits_t * epoch_credits_arr = fd_bank_epoch_credits( bank );
905 :
906 267 : fd_stake_delegations_iter_t iter_[1];
907 267 : for( fd_stake_delegations_iter_t * iter = fd_stake_delegations_iter_init( iter_, stake_delegations, accdb, bank->accdb_fork_id, bank->f.epoch, &bank->f.warmup_cooldown_rate_epoch );
908 603 : !fd_stake_delegations_iter_done( iter );
909 336 : fd_stake_delegations_iter_next( iter ) ) {
910 336 : fd_stake_delegation_t const * stake_delegation = fd_stake_delegations_iter_ele( iter );
911 336 : ulong stake_delegation_idx = fd_stake_delegations_iter_idx( iter );
912 :
913 : /* Note that we don't check minimum delegation here, as there are
914 : no plans to activate stake_minimum_delegation_for_rewards.
915 : If this changes we need to skip stake accounts that are
916 : below the minimum delegation here. However we don't do this yet,
917 : to ensure that we audit the feature properly if this happens. */
918 :
919 336 : fd_calculated_stake_rewards_t * calculated_stake_rewards = NULL;
920 336 : if( stake_delegation_idx>=runtime_stack->max_stake_accounts ) {
921 0 : calculated_stake_rewards = calculated_stake_rewards_;
922 336 : } else {
923 336 : calculated_stake_rewards = &runtime_stack->stakes.stake_rewards_result[ stake_delegation_idx ];
924 336 : }
925 336 : calculated_stake_rewards->success = 0;
926 :
927 336 : int cached = !is_recalculation && stake_delegation_idx<runtime_stack->max_stake_accounts;
928 336 : uint idx;
929 336 : if( FD_LIKELY( cached ) ) {
930 333 : idx = runtime_stack->stakes.stake_points_result[ stake_delegation_idx ].vote_idx;
931 333 : } else {
932 3 : fd_vote_rewards_t * vote_ele = runtime_stack->stakes.vote_ele;
933 3 : fd_vote_rewards_map_t * vote_ele_map = runtime_stack->stakes.vote_map;
934 3 : idx = (uint)fd_vote_rewards_map_idx_query( vote_ele_map, &stake_delegation->vote_account, UINT_MAX, vote_ele );
935 3 : }
936 :
937 : /* Stake account may need to be adjusted to meet rent-exempt minimum
938 : balance requirements based on new rent and delegation parameters.
939 : https://github.com/anza-xyz/agave/blob/v4.2.0-beta.0/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L568-L608 */
940 336 : if( FD_UNLIKELY( idx==UINT_MAX ) ) {
941 9 : if( !FD_FEATURE_ACTIVE_BANK( bank, relax_post_exec_min_balance_check ) ) continue;
942 :
943 : /* If the stake account's resulting lamports would cause it to be
944 : below the rent exempt minimum balance, it needs to be queued
945 : for update (and thus affects the epoch reward partitions). */
946 0 : if( !delegation_may_need_adjustment(
947 0 : bank,
948 0 : stake_delegation,
949 0 : stake_history,
950 0 : rewarded_epoch,
951 0 : stake_delegation->stake,
952 0 : stake_delegation->lamports,
953 0 : fd_rent_exempt_minimum_balance( &bank->f.rent, stake_delegation->acc_dlen ) ) ) {
954 0 : continue;
955 0 : }
956 :
957 : /* Place an empty entry for this stake delegation idx so that
958 : the partitioning logic factors it in. */
959 0 : *calculated_stake_rewards = (fd_calculated_stake_rewards_t){
960 0 : .success = 1,
961 0 : .staker_rewards = 0,
962 0 : .voter_rewards = 0,
963 0 : .new_credits_observed = stake_delegation->credits_observed
964 0 : };
965 0 : runtime_stack->stakes.stake_rewards_cnt++;
966 0 : continue;
967 0 : }
968 :
969 327 : fd_calculated_stake_points_t stake_points_result_[1];
970 327 : fd_calculated_stake_points_t * stake_points_result;
971 327 : if( FD_LIKELY( cached ) ) {
972 324 : stake_points_result = &runtime_stack->stakes.stake_points_result[ stake_delegation_idx ];
973 324 : } else {
974 3 : fd_epoch_credits_t * epoch_credits = &epoch_credits_arr[ idx ];
975 :
976 : /* We have not cached the stake points yet if we are recalculating
977 : stake rewards so we need to recalculate them. ULONG_MAX
978 : disables the tag fast path. */
979 3 : calculate_stake_points_fast( epoch_credits,
980 3 : stake_history,
981 3 : stake_delegation,
982 3 : &bank->f.warmup_cooldown_rate_epoch,
983 3 : FD_FEATURE_ACTIVE_BANK( bank, upgrade_bpf_stake_program_to_v5_1 ),
984 3 : ULONG_MAX,
985 3 : stake_points_result_ );
986 3 : stake_points_result = stake_points_result_;
987 3 : }
988 :
989 : /* redeem_rewards is actually just responsible for calculating the
990 : vote and stake rewards for each stake account. It does not do
991 : rewards redemption: it is a misnomer. */
992 327 : int err = redeem_rewards(
993 327 : stake_delegation,
994 327 : idx,
995 327 : rewarded_epoch,
996 327 : total_rewards,
997 327 : total_points,
998 327 : runtime_stack,
999 327 : stake_points_result,
1000 327 : calculated_stake_rewards );
1001 :
1002 327 : if( FD_UNLIKELY( err!=0 ) ) {
1003 : /* Even if there is an error computing rewards for the stake
1004 : account, there may be a required balance update for the stake
1005 : account if rent increased.
1006 : https://github.com/anza-xyz/agave/blob/v4.2.0-beta.0/runtime/src/inflation_rewards/mod.rs#L132-L152 */
1007 3 : if( !FD_FEATURE_ACTIVE_BANK( bank, relax_post_exec_min_balance_check ) ) continue;
1008 :
1009 : /* staker rewards is 0 in the error case, so we can just use
1010 : the current stake and lamports in the function args. */
1011 0 : if( !delegation_may_need_adjustment(
1012 0 : bank,
1013 0 : stake_delegation,
1014 0 : stake_history,
1015 0 : rewarded_epoch,
1016 0 : stake_delegation->stake,
1017 0 : stake_delegation->lamports,
1018 0 : fd_rent_exempt_minimum_balance( &bank->f.rent, stake_delegation->acc_dlen ) ) ) {
1019 0 : continue;
1020 0 : }
1021 :
1022 0 : *calculated_stake_rewards = (fd_calculated_stake_rewards_t){
1023 0 : .success = 1,
1024 0 : .staker_rewards = 0,
1025 0 : .voter_rewards = 0,
1026 0 : .new_credits_observed = stake_delegation->credits_observed
1027 0 : };
1028 324 : } else {
1029 324 : calculated_stake_rewards->success = 1;
1030 324 : }
1031 :
1032 324 : if( capture_ctx && capture_ctx->capture_solcap ) {
1033 0 : fd_capture_link_write_stake_reward_event( capture_ctx,
1034 0 : bank->f.slot,
1035 0 : stake_delegation->stake_account,
1036 0 : stake_delegation->vote_account,
1037 0 : runtime_stack->stakes.vote_ele[ idx ].commission,
1038 0 : (long)calculated_stake_rewards->voter_rewards,
1039 0 : (long)calculated_stake_rewards->staker_rewards,
1040 0 : (long)calculated_stake_rewards->new_credits_observed );
1041 0 : }
1042 :
1043 324 : runtime_stack->stakes.vote_ele[ idx ].vote_rewards += calculated_stake_rewards->voter_rewards;
1044 324 : runtime_stack->stakes.stake_rewards_cnt++;
1045 324 : }
1046 267 : }
1047 :
1048 : /* setup_stake_partitions hashes every stake reward of the epoch into
1049 : its partition. Only the rewards landing inside the fork's current
1050 : window are retained; see fd_stake_rewards.h. The fork must already
1051 : have been initialized and its window positioned by the caller. */
1052 :
1053 : static void
1054 : setup_stake_partitions( fd_bank_t * bank,
1055 : fd_accdb_t * accdb,
1056 : fd_stake_history_t const * stake_history,
1057 : fd_stake_delegations_t const * stake_delegations,
1058 : fd_runtime_stack_t * runtime_stack,
1059 : uchar fork_idx,
1060 : ulong rewarded_epoch,
1061 : ulong total_rewards,
1062 267 : uint128 total_points ) {
1063 :
1064 267 : fd_stake_rewards_t * stake_rewards = fd_bank_stake_rewards_modify( bank );
1065 267 : fd_epoch_credits_t * epoch_credits_arr = fd_bank_epoch_credits( bank );
1066 :
1067 267 : fd_stake_delegations_iter_t iter_[1];
1068 267 : for( fd_stake_delegations_iter_t * iter = fd_stake_delegations_iter_init( iter_, stake_delegations, accdb, bank->accdb_fork_id, bank->f.epoch, &bank->f.warmup_cooldown_rate_epoch );
1069 603 : !fd_stake_delegations_iter_done( iter );
1070 336 : fd_stake_delegations_iter_next( iter ) ) {
1071 336 : fd_stake_delegation_t const * stake_delegation = fd_stake_delegations_iter_ele( iter );
1072 336 : ulong stake_delegation_idx = fd_stake_delegations_iter_idx( iter );
1073 :
1074 336 : fd_calculated_stake_rewards_t calculated_stake_rewards_[1];
1075 336 : fd_calculated_stake_rewards_t * calculated_stake_rewards = NULL;
1076 :
1077 336 : if( FD_UNLIKELY( stake_delegation_idx>=runtime_stack->max_stake_accounts ) ) {
1078 :
1079 0 : calculated_stake_rewards = calculated_stake_rewards_;
1080 :
1081 0 : fd_vote_rewards_t * vote_ele = runtime_stack->stakes.vote_ele;
1082 0 : fd_vote_rewards_map_t * vote_ele_map = runtime_stack->stakes.vote_map;
1083 0 : uint idx = (uint)fd_vote_rewards_map_idx_query( vote_ele_map, &stake_delegation->vote_account, UINT_MAX, vote_ele );
1084 0 : if( FD_UNLIKELY( idx==UINT_MAX ) ) {
1085 0 : if( !FD_FEATURE_ACTIVE_BANK( bank, relax_post_exec_min_balance_check ) ) continue;
1086 :
1087 : /* If the stake account's resulting lamports would cause it to be
1088 : below the rent exempt minimum balance, it needs to be queued
1089 : for update (and thus affects the epoch reward partitions). */
1090 0 : if( !delegation_may_need_adjustment(
1091 0 : bank,
1092 0 : stake_delegation,
1093 0 : stake_history,
1094 0 : rewarded_epoch,
1095 0 : stake_delegation->stake,
1096 0 : stake_delegation->lamports,
1097 0 : fd_rent_exempt_minimum_balance( &bank->f.rent, stake_delegation->acc_dlen ) ) ) {
1098 0 : continue;
1099 0 : }
1100 :
1101 0 : fd_stake_rewards_insert( stake_rewards, fork_idx, &stake_delegation->stake_account, 0UL, stake_delegation->credits_observed );
1102 0 : continue;
1103 0 : }
1104 :
1105 0 : fd_epoch_credits_t * epoch_credits = &epoch_credits_arr[ idx ];
1106 :
1107 0 : fd_calculated_stake_points_t stake_points_result[1];
1108 0 : calculate_stake_points_fast( epoch_credits,
1109 0 : stake_history,
1110 0 : stake_delegation,
1111 0 : &bank->f.warmup_cooldown_rate_epoch,
1112 0 : FD_FEATURE_ACTIVE_BANK( bank, upgrade_bpf_stake_program_to_v5_1 ),
1113 0 : ULONG_MAX,
1114 0 : stake_points_result );
1115 :
1116 : /* redeem_rewards is actually just responsible for calculating the
1117 : vote and stake rewards for each stake account. It does not do
1118 : rewards redemption: it is a misnomer. */
1119 0 : int err = redeem_rewards(
1120 0 : stake_delegation,
1121 0 : idx,
1122 0 : rewarded_epoch,
1123 0 : total_rewards,
1124 0 : total_points,
1125 0 : runtime_stack,
1126 0 : stake_points_result,
1127 0 : calculated_stake_rewards );
1128 :
1129 0 : if( FD_UNLIKELY( err!=0 ) ) {
1130 : /* Even if there is an error computing rewards for the stake
1131 : account, there may be a required balance update for the stake
1132 : account if rent increased.
1133 : https://github.com/anza-xyz/agave/blob/v4.2.0-beta.0/runtime/src/inflation_rewards/mod.rs#L132-L152 */
1134 0 : if( !FD_FEATURE_ACTIVE_BANK( bank, relax_post_exec_min_balance_check ) ) continue;
1135 :
1136 : /* staker rewards is 0 in the error case, so we can just use
1137 : the current stake and lamports in the function args. */
1138 0 : if( !delegation_may_need_adjustment(
1139 0 : bank,
1140 0 : stake_delegation,
1141 0 : stake_history,
1142 0 : rewarded_epoch,
1143 0 : stake_delegation->stake,
1144 0 : stake_delegation->lamports,
1145 0 : fd_rent_exempt_minimum_balance( &bank->f.rent, stake_delegation->acc_dlen ) ) ) {
1146 0 : continue;
1147 0 : }
1148 :
1149 0 : fd_stake_rewards_insert( stake_rewards, fork_idx, &stake_delegation->stake_account, 0UL, stake_delegation->credits_observed );
1150 0 : continue;
1151 0 : } else {
1152 0 : calculated_stake_rewards->success = 1;
1153 0 : }
1154 336 : } else {
1155 336 : calculated_stake_rewards = &runtime_stack->stakes.stake_rewards_result[ stake_delegation_idx ];
1156 336 : }
1157 :
1158 336 : if( FD_UNLIKELY( !calculated_stake_rewards->success ) ) continue;
1159 :
1160 324 : fd_stake_rewards_insert(
1161 324 : stake_rewards,
1162 324 : fork_idx,
1163 324 : &stake_delegation->stake_account,
1164 324 : calculated_stake_rewards->staker_rewards,
1165 324 : calculated_stake_rewards->new_credits_observed
1166 324 : );
1167 324 : }
1168 267 : }
1169 :
1170 : /* Calculate epoch reward and return vote and stake rewards.
1171 :
1172 : https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L273 */
1173 : static uint128
1174 : calculate_validator_rewards( fd_bank_t * bank,
1175 : fd_accdb_t * accdb,
1176 : fd_runtime_stack_t * runtime_stack,
1177 : fd_stake_delegations_t const * stake_delegations,
1178 : fd_capture_ctx_t * capture_ctx,
1179 : ulong rewarded_epoch,
1180 264 : ulong * rewards_out ) {
1181 264 : uchar stake_history_data[ FD_SYSVAR_STAKE_HISTORY_BINCODE_SZ ];
1182 264 : fd_stake_history_t stake_history[1];
1183 264 : read_stake_history( accdb, bank->accdb_fork_id, stake_history_data, stake_history );
1184 :
1185 : /* Calculate the epoch reward points from stake/vote accounts */
1186 264 : uint128 total_points = calculate_reward_points_partitioned(
1187 264 : bank,
1188 264 : accdb,
1189 264 : stake_delegations,
1190 264 : stake_history,
1191 264 : rewarded_epoch,
1192 264 : runtime_stack );
1193 :
1194 : /* If there are no points, then we set the rewards to 0. */
1195 264 : *rewards_out = total_points>0UL ? *rewards_out: 0UL;
1196 :
1197 264 : if( FD_UNLIKELY( capture_ctx && capture_ctx->capture_solcap ) ) {
1198 0 : ulong epoch = bank->f.epoch;
1199 0 : ulong slot = bank->f.slot;
1200 0 : fd_capture_link_write_stake_rewards_begin( capture_ctx,
1201 0 : slot,
1202 0 : epoch,
1203 0 : epoch-1UL, /* FIXME: this is not strictly correct */
1204 0 : *rewards_out,
1205 0 : (ulong)total_points );
1206 0 : }
1207 :
1208 : /* Calculate the stake and vote rewards for each account. We want to
1209 : use the vote states from the end of the current_epoch. */
1210 264 : calculate_stake_vote_rewards(
1211 264 : bank,
1212 264 : accdb,
1213 264 : stake_delegations,
1214 264 : capture_ctx,
1215 264 : stake_history,
1216 264 : rewarded_epoch,
1217 264 : *rewards_out,
1218 264 : total_points,
1219 264 : runtime_stack,
1220 264 : 0 );
1221 :
1222 264 : fd_hash_t const * parent_blockhash = fd_blockhashes_peek_last_hash( &bank->f.block_hash_queue );
1223 264 : ulong starting_block_height = bank->f.block_height + REWARD_CALCULATION_NUM_BLOCKS;
1224 264 : uint num_partitions = get_reward_distribution_num_blocks( &bank->f.epoch_schedule,
1225 264 : bank->f.slot,
1226 264 : runtime_stack->stakes.stake_rewards_cnt,
1227 264 : bank->f.slot_params.stake_account_stores_per_block );
1228 :
1229 264 : fd_stake_rewards_t * stake_rewards = fd_bank_stake_rewards_modify( bank );
1230 264 : uchar fork_idx = fd_stake_rewards_init( stake_rewards,
1231 264 : bank->f.epoch,
1232 264 : parent_blockhash,
1233 264 : starting_block_height,
1234 264 : num_partitions,
1235 264 : runtime_stack->stakes.stake_rewards_cnt );
1236 264 : if( FD_UNLIKELY( bank->stake_rewards_fork_id!=UCHAR_MAX ) ) {
1237 3 : fd_stake_rewards_release( stake_rewards, bank->stake_rewards_fork_id );
1238 3 : }
1239 264 : bank->stake_rewards_fork_id = fork_idx;
1240 :
1241 264 : setup_stake_partitions( bank,
1242 264 : accdb,
1243 264 : stake_history,
1244 264 : stake_delegations,
1245 264 : runtime_stack,
1246 264 : fork_idx,
1247 264 : rewarded_epoch,
1248 264 : *rewards_out,
1249 264 : total_points );
1250 :
1251 264 : return total_points;
1252 264 : }
1253 :
1254 : /* Calculate rewards from previous epoch to prepare for partitioned distribution.
1255 :
1256 : https://github.com/anza-xyz/agave/blob/v3.0.4/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L277 */
1257 : static void
1258 : calculate_rewards_for_partitioning( fd_bank_t * bank,
1259 : fd_accdb_t * accdb,
1260 : fd_runtime_stack_t * runtime_stack,
1261 : fd_stake_delegations_t const * stake_delegations,
1262 : fd_capture_ctx_t * capture_ctx,
1263 : ulong prev_epoch,
1264 264 : fd_partitioned_rewards_calculation_t * result ) {
1265 264 : fd_prev_epoch_inflation_rewards_t rewards;
1266 :
1267 264 : calculate_previous_epoch_inflation_rewards( bank,
1268 264 : bank->f.capitalization,
1269 264 : prev_epoch,
1270 264 : &rewards );
1271 :
1272 264 : ulong total_rewards = rewards.validator_rewards;
1273 :
1274 264 : uint128 points = calculate_validator_rewards( bank,
1275 264 : accdb,
1276 264 : runtime_stack,
1277 264 : stake_delegations,
1278 264 : capture_ctx,
1279 264 : prev_epoch,
1280 264 : &total_rewards );
1281 :
1282 : /* The agave client does not partition the stake rewards until the
1283 : first distribution block. We calculate the partitions during the
1284 : boundary. */
1285 264 : result->validator_points = points;
1286 264 : result->validator_rewards = total_rewards;
1287 264 : result->validator_rate = rewards.validator_rate;
1288 264 : result->foundation_rate = rewards.foundation_rate;
1289 264 : result->prev_epoch_duration_in_years = rewards.prev_epoch_duration_in_years;
1290 264 : result->capitalization = bank->f.capitalization;
1291 264 : }
1292 :
1293 : /* Calculate rewards from previous epoch and distribute vote rewards
1294 : https://github.com/anza-xyz/agave/blob/v3.0.4/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L148 */
1295 : static void
1296 : calculate_rewards_and_distribute_vote_rewards( fd_bank_t * bank,
1297 : fd_accdb_t * accdb,
1298 : fd_runtime_stack_t * runtime_stack,
1299 : fd_stake_delegations_t const * stake_delegations,
1300 : fd_capture_ctx_t * capture_ctx,
1301 264 : ulong prev_epoch ) {
1302 :
1303 264 : fd_vote_rewards_t * vote_ele_pool = runtime_stack->stakes.vote_ele;
1304 264 : fd_vote_rewards_map_t * vote_ele_map = runtime_stack->stakes.vote_map;
1305 :
1306 : /* First we must compute the stake and vote rewards for the just
1307 : completed epoch. We store the stake account rewards and vote
1308 : states rewards in the bank */
1309 :
1310 264 : fd_partitioned_rewards_calculation_t rewards_calc_result[1] = {0};
1311 264 : calculate_rewards_for_partitioning( bank,
1312 264 : accdb,
1313 264 : runtime_stack,
1314 264 : stake_delegations,
1315 264 : capture_ctx,
1316 264 : prev_epoch,
1317 264 : rewards_calc_result );
1318 :
1319 :
1320 : /* Distribute the commission rewards. distributed_rewards includes
1321 : burned and incinerated amounts: the epoch rewards sysvar counts
1322 : them as distributed.
1323 : https://github.com/anza-xyz/agave/blob/v4.2.0-beta.1/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L186-L217 */
1324 264 : ulong distributed_rewards = 0UL;
1325 264 : if( FD_FEATURE_ACTIVE_BANK( bank, custom_commission_collector ) ) {
1326 : /* Aggregate rewards per collector before validation: several small
1327 : rewards may collectively make a new collector rent-exempt.
1328 : stake_accum is free for reuse as scratch here. */
1329 54 : fd_stake_accum_t * collector_pool = runtime_stack->stakes.stake_accum;
1330 54 : fd_stake_accum_map_t * collector_map = runtime_stack->stakes.stake_accum_map;
1331 54 : fd_stake_accum_map_reset( collector_map );
1332 54 : ulong collector_cnt = 0UL;
1333 :
1334 54 : for( fd_vote_rewards_map_iter_t iter = fd_vote_rewards_map_iter_init( vote_ele_map, vote_ele_pool );
1335 117 : !fd_vote_rewards_map_iter_done( iter, vote_ele_map, vote_ele_pool );
1336 63 : iter = fd_vote_rewards_map_iter_next( iter, vote_ele_map, vote_ele_pool ) ) {
1337 63 : uint idx = (uint)fd_vote_rewards_map_iter_idx( iter, vote_ele_map, vote_ele_pool );
1338 63 : fd_vote_rewards_t * ele = &vote_ele_pool[idx];
1339 :
1340 63 : ulong rewards = ele->vote_rewards;
1341 63 : if( FD_UNLIKELY( !rewards ) ) continue;
1342 54 : distributed_rewards = fd_ulong_sat_add( distributed_rewards, rewards );
1343 :
1344 54 : fd_pubkey_t collector;
1345 54 : fd_rewards_inflation_collector( bank, &ele->pubkey, &collector );
1346 :
1347 54 : fd_stake_accum_t * collector_reward = fd_stake_accum_map_ele_query( collector_map, &collector, NULL, collector_pool );
1348 54 : if( FD_UNLIKELY( !collector_reward ) ) {
1349 48 : FD_TEST( collector_cnt<runtime_stack->max_vote_accounts );
1350 48 : collector_reward = &collector_pool[ collector_cnt++ ];
1351 48 : collector_reward->pubkey = collector;
1352 48 : collector_reward->stake = rewards;
1353 48 : fd_stake_accum_map_ele_insert( collector_map, collector_reward, collector_pool );
1354 48 : } else {
1355 6 : collector_reward->stake = fd_ulong_sat_add( collector_reward->stake, rewards );
1356 6 : }
1357 54 : }
1358 :
1359 54 : for( fd_stake_accum_map_iter_t iter = fd_stake_accum_map_iter_init( collector_map, collector_pool );
1360 102 : !fd_stake_accum_map_iter_done( iter, collector_map, collector_pool );
1361 54 : iter = fd_stake_accum_map_iter_next( iter, collector_map, collector_pool ) ) {
1362 48 : fd_stake_accum_t * collector_reward = fd_stake_accum_map_iter_ele( iter, collector_map, collector_pool );
1363 :
1364 : /* A collector that is itself a self-collecting vote account is
1365 : paid only its own reward; rewards routed to it by other vote
1366 : accounts are burned (it is not system-owned).
1367 : https://github.com/anza-xyz/agave/blob/v4.2.0-beta.1/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L70-L124 */
1368 48 : fd_vote_rewards_t * self_ele = fd_vote_rewards_map_ele_query( vote_ele_map, &collector_reward->pubkey, NULL, vote_ele_pool );
1369 48 : int is_vote_account = 0;
1370 48 : if( self_ele && self_ele->vote_rewards ) {
1371 6 : fd_pubkey_t self_collector;
1372 6 : fd_rewards_inflation_collector( bank, &self_ele->pubkey, &self_collector );
1373 6 : is_vote_account = fd_pubkey_eq( &self_collector, &self_ele->pubkey );
1374 6 : }
1375 48 : ulong rewards = is_vote_account ? self_ele->vote_rewards : collector_reward->stake;
1376 :
1377 48 : fd_acc_t account = fd_accdb_read_one( accdb, bank->accdb_fork_id, collector_reward->pubkey.uc );
1378 48 : int burn;
1379 48 : if( is_vote_account ) {
1380 : /* The vote account itself only needs the overflow check. */
1381 6 : ulong post_balance;
1382 6 : burn = __builtin_uaddl_overflow( account.lamports, rewards, &post_balance );
1383 42 : } else {
1384 42 : burn = fd_rewards_validate_commission_collector( bank, &collector_reward->pubkey, &account, rewards );
1385 42 : }
1386 48 : fd_accdb_unread_one( accdb, &account );
1387 :
1388 48 : if( FD_UNLIKELY( burn ) ) continue;
1389 :
1390 : /* Credit rewards to the collector (creating a new system account
1391 : if it does not exist). */
1392 27 : fd_accdb_svm_credit( bank, accdb, capture_ctx, &collector_reward->pubkey, rewards );
1393 27 : }
1394 210 : } else {
1395 210 : for( fd_vote_rewards_map_iter_t iter = fd_vote_rewards_map_iter_init( vote_ele_map, vote_ele_pool );
1396 468 : !fd_vote_rewards_map_iter_done( iter, vote_ele_map, vote_ele_pool );
1397 258 : iter = fd_vote_rewards_map_iter_next( iter, vote_ele_map, vote_ele_pool ) ) {
1398 :
1399 258 : uint idx = (uint)fd_vote_rewards_map_iter_idx( iter, vote_ele_map, vote_ele_pool );
1400 258 : fd_vote_rewards_t * ele = &vote_ele_pool[idx];
1401 :
1402 258 : ulong rewards = ele->vote_rewards;
1403 258 : if( rewards==0UL ) {
1404 252 : continue;
1405 252 : }
1406 :
1407 : /* Credit rewards to vote account (creating a new system account if
1408 : it does not exist) */
1409 6 : fd_accdb_svm_credit( bank, accdb, capture_ctx, &ele->pubkey, rewards );
1410 6 : distributed_rewards = fd_ulong_sat_add( distributed_rewards, rewards );
1411 6 : }
1412 210 : }
1413 :
1414 : /* Verify that we didn't pay any more than we expected to */
1415 264 : fd_stake_rewards_t * stake_rewards = fd_bank_stake_rewards_modify( bank );
1416 264 : ulong total_stake_rewards = fd_stake_rewards_total_rewards( stake_rewards, bank->stake_rewards_fork_id );
1417 :
1418 264 : ulong total_rewards = fd_ulong_sat_add( distributed_rewards, total_stake_rewards );
1419 264 : if( FD_UNLIKELY( rewards_calc_result->validator_rewards<total_rewards ) ) {
1420 0 : FD_LOG_CRIT(( "Unexpected rewards calculation result" ));
1421 0 : }
1422 :
1423 264 : runtime_stack->stakes.distributed_rewards = distributed_rewards;
1424 264 : runtime_stack->stakes.total_rewards = rewards_calc_result->validator_rewards;
1425 264 : runtime_stack->stakes.total_points.ud = rewards_calc_result->validator_points;
1426 264 : }
1427 :
1428 : /* Note: modifies delegation in-place, adjusting it for rent-exempt
1429 : minimum balance requirements.
1430 : https://github.com/anza-xyz/agave/blob/v4.2.0-beta.0/runtime/src/bank/partitioned_epoch_rewards/distribution.rs#L55-L76 */
1431 : static void
1432 : adjust_delegation_for_rent( fd_delegation_t * delegation,
1433 : ulong rewarded_epoch,
1434 : ulong new_delegation_with_rewards,
1435 : ulong lamports_with_rewards,
1436 3 : ulong minimum_lamports ) {
1437 3 : ulong new_delegation = fd_ulong_min( new_delegation_with_rewards,
1438 3 : fd_ulong_sat_sub( lamports_with_rewards, minimum_lamports ) );
1439 :
1440 3 : if( new_delegation!=delegation->stake ) {
1441 3 : delegation->stake = new_delegation;
1442 3 : if( FD_UNLIKELY( new_delegation==0UL ) ) {
1443 0 : delegation->deactivation_epoch = rewarded_epoch;
1444 0 : }
1445 3 : }
1446 3 : }
1447 :
1448 : /* Distributes a single partitioned reward to a single stake account. acc was
1449 : acquired by the caller as part of a batch and is released with it, so the
1450 : early returns below leave commit at 0, which fd_accdb_release skips. */
1451 :
1452 : static int
1453 : distribute_epoch_reward_to_stake_acc( fd_bank_t * bank,
1454 : fd_capture_ctx_t * capture_ctx,
1455 : ulong reward_lamports,
1456 : ulong new_credits_observed,
1457 : fd_acc_t * acc,
1458 : fd_lthash_adder_t * adder_pre,
1459 : fd_lthash_value_t * sum_pre,
1460 : fd_lthash_adder_t * adder_post,
1461 195 : fd_lthash_value_t * sum_post ) {
1462 195 : if( FD_UNLIKELY( !acc->lamports ) ) {
1463 0 : return 1; /* account does not exist */
1464 0 : }
1465 :
1466 195 : fd_stake_state_t const * stake_state_orig = fd_stakes_get_state( acc );
1467 195 : if( FD_UNLIKELY( !stake_state_orig || stake_state_orig->stake_type!=FD_STAKE_STATE_STAKE ) ) {
1468 0 : return 1; /* not a valid stake account */
1469 0 : }
1470 :
1471 195 : fd_pubkey_t const * stake_pubkey = fd_type_pun_const(acc->pubkey);
1472 195 : fd_stake_state_t stake_state[1] = { *stake_state_orig };
1473 :
1474 195 : fd_lthash_adder_push_solana_account( adder_pre, sum_pre, stake_pubkey->uc, acc->data, acc->data_len, acc->lamports, (uchar)!!acc->executable, acc->owner );
1475 :
1476 195 : FD_TEST( !__builtin_add_overflow( acc->lamports, reward_lamports, &acc->lamports ) );
1477 :
1478 195 : ulong old_credits_observed = stake_state->stake.stake.credits_observed;
1479 195 : stake_state->stake.stake.credits_observed = new_credits_observed;
1480 195 : stake_state->stake.stake.delegation.stake = fd_ulong_sat_add( stake_state->stake.stake.delegation.stake, reward_lamports );
1481 :
1482 : /* https://github.com/anza-xyz/agave/blob/v4.2.0-beta.0/runtime/src/bank/partitioned_epoch_rewards/distribution.rs#L259-L283 */
1483 195 : fd_stake_t * new_stake = &stake_state->stake.stake;
1484 195 : if( FD_FEATURE_ACTIVE_BANK( bank, relax_post_exec_min_balance_check ) ) {
1485 3 : ulong minimum_balance = fd_rent_exempt_minimum_balance( &bank->f.rent, acc->data_len );
1486 3 : adjust_delegation_for_rent(
1487 3 : &new_stake->delegation,
1488 3 : fd_ulong_sat_sub( bank->f.epoch, 1UL ),
1489 3 : new_stake->delegation.stake,
1490 3 : acc->lamports,
1491 3 : minimum_balance );
1492 3 : }
1493 :
1494 195 : fd_stake_delegations_t * stake_delegations_upd = fd_bank_stake_delegations_modify( bank );
1495 195 : fd_stake_delegations_fork_update( stake_delegations_upd,
1496 195 : bank->stake_delegations_fork_id,
1497 195 : stake_pubkey,
1498 195 : &stake_state->stake.stake.delegation.voter_pubkey,
1499 195 : stake_state->stake.stake.delegation.stake,
1500 195 : stake_state->stake.stake.delegation.activation_epoch,
1501 195 : stake_state->stake.stake.delegation.deactivation_epoch,
1502 195 : stake_state->stake.stake.credits_observed,
1503 195 : acc->lamports,
1504 195 : (uint)acc->data_len,
1505 195 : fd_stake_warmup_cooldown_rate( bank->f.epoch, &bank->f.warmup_cooldown_rate_epoch ) );
1506 :
1507 195 : if( FD_UNLIKELY( capture_ctx && capture_ctx->capture_solcap ) ) {
1508 0 : fd_capture_link_write_stake_account_payout( capture_ctx,
1509 0 : bank->f.slot,
1510 0 : *stake_pubkey,
1511 0 : bank->f.slot,
1512 0 : acc->lamports,
1513 0 : (long)reward_lamports,
1514 0 : new_credits_observed,
1515 0 : (long)( new_credits_observed - old_credits_observed ),
1516 0 : stake_state->stake.stake.delegation.stake,
1517 0 : (long)reward_lamports );
1518 0 : }
1519 :
1520 195 : FD_STORE( fd_stake_state_t, acc->data, *stake_state );
1521 195 : fd_lthash_adder_push_solana_account( adder_post, sum_post, stake_pubkey->uc, acc->data, acc->data_len, acc->lamports, (uchar)!!acc->executable, acc->owner );
1522 195 : fd_hashes_capture_account( stake_pubkey->uc, acc->owner, acc->lamports, acc->executable, acc->data, acc->data_len, bank, capture_ctx );
1523 195 : acc->commit = 1;
1524 :
1525 195 : return 0;
1526 195 : }
1527 :
1528 : /* Accdb batch size for updating stake accounts */
1529 5097 : #define STAKE_REWARD_ACC_BATCH_SZ (32UL)
1530 :
1531 : /* Process reward credits for a partition of rewards. Store the rewards
1532 : to AccountsDB, update reward history record and total capitalization
1533 : https://github.com/anza-xyz/agave/blob/cbc8320d35358da14d79ebcada4dfb6756ffac79/runtime/src/bank/partitioned_epoch_rewards/distribution.rs#L88 */
1534 : static void
1535 : distribute_epoch_rewards_in_partition( fd_stake_rewards_t * stake_rewards,
1536 : ulong partition_idx,
1537 : fd_bank_t * bank,
1538 : fd_accdb_t * accdb,
1539 135 : fd_capture_ctx_t * capture_ctx ) {
1540 :
1541 135 : ulong lamports_distributed = 0UL;
1542 135 : ulong lamports_burned = 0UL;
1543 :
1544 135 : fd_lthash_adder_t adder_pre[1], adder_post[1];
1545 135 : fd_lthash_adder_new( adder_pre );
1546 135 : fd_lthash_adder_new( adder_post );
1547 135 : fd_lthash_value_t sum_pre[1], sum_post[1];
1548 135 : fd_lthash_zero( sum_pre );
1549 135 : fd_lthash_zero( sum_post );
1550 :
1551 : /* Acquire and process stake accounts in batches of 32 */
1552 :
1553 135 : fd_pubkey_t pubkeys [ STAKE_REWARD_ACC_BATCH_SZ ];
1554 135 : uchar const * pubkey_ptrs [ STAKE_REWARD_ACC_BATCH_SZ ];
1555 135 : int writable [ STAKE_REWARD_ACC_BATCH_SZ ];
1556 135 : fd_acc_t accs [ STAKE_REWARD_ACC_BATCH_SZ ];
1557 135 : ulong reward_lamports [ STAKE_REWARD_ACC_BATCH_SZ ];
1558 135 : ulong credits_observed[ STAKE_REWARD_ACC_BATCH_SZ ];
1559 :
1560 4455 : for( ulong i=0UL; i<STAKE_REWARD_ACC_BATCH_SZ; i++ ) {
1561 4320 : pubkey_ptrs[ i ] = pubkeys[ i ].uc;
1562 4320 : writable [ i ] = 1;
1563 4320 : }
1564 :
1565 135 : fd_stake_rewards_iter_init( stake_rewards, bank->stake_rewards_fork_id, (ushort)partition_idx );
1566 261 : while( !fd_stake_rewards_iter_done( stake_rewards ) ) {
1567 :
1568 : /* Gather the next batch of rewards out of the partition. */
1569 126 : ulong batch_cnt = 0UL;
1570 321 : for( ; batch_cnt<STAKE_REWARD_ACC_BATCH_SZ && !fd_stake_rewards_iter_done( stake_rewards );
1571 195 : batch_cnt++, fd_stake_rewards_iter_next( stake_rewards, bank->stake_rewards_fork_id ) ) {
1572 195 : fd_stake_rewards_iter_ele( stake_rewards,
1573 195 : bank->stake_rewards_fork_id,
1574 195 : &pubkeys [ batch_cnt ],
1575 195 : &reward_lamports [ batch_cnt ],
1576 195 : &credits_observed[ batch_cnt ] );
1577 195 : }
1578 :
1579 126 : fd_accdb_acquire( accdb, bank->accdb_fork_id, batch_cnt, pubkey_ptrs, writable, accs );
1580 :
1581 : /* Calculate and flush stake account updates */
1582 321 : for( ulong i=0UL; i<batch_cnt; i++ ) {
1583 195 : if( FD_LIKELY( !distribute_epoch_reward_to_stake_acc( bank,
1584 195 : capture_ctx,
1585 195 : reward_lamports[ i ],
1586 195 : credits_observed[ i ],
1587 195 : &accs[ i ],
1588 195 : adder_pre, sum_pre,
1589 195 : adder_post, sum_post ) ) ) {
1590 195 : lamports_distributed += reward_lamports[ i ];
1591 195 : } else {
1592 0 : lamports_burned += reward_lamports[ i ];
1593 0 : }
1594 195 : }
1595 :
1596 126 : fd_accdb_release( accdb, batch_cnt, accs );
1597 126 : }
1598 :
1599 135 : fd_lthash_adder_flush( adder_pre, sum_pre );
1600 135 : fd_lthash_adder_flush( adder_post, sum_post );
1601 :
1602 135 : fd_lthash_value_t * bank_lthash = fd_bank_lthash_locking_modify( bank );
1603 135 : fd_lthash_sub( bank_lthash, sum_pre );
1604 135 : fd_lthash_add( bank_lthash, sum_post );
1605 135 : fd_bank_lthash_end_locking_modify( bank );
1606 :
1607 : /* Update the epoch rewards sysvar with the amount distributed and burnt */
1608 135 : fd_sysvar_epoch_rewards_distribute( bank, accdb, capture_ctx, lamports_distributed + lamports_burned );
1609 :
1610 135 : FD_LOG_DEBUG(( "lamports burned: %lu, lamports distributed: %lu", lamports_burned, lamports_distributed ));
1611 :
1612 135 : bank->f.capitalization = bank->f.capitalization + lamports_distributed;
1613 135 : }
1614 :
1615 : static void
1616 : recalculate_partitioned_rewards( fd_banks_t * banks,
1617 : fd_bank_t * bank,
1618 : fd_accdb_t * accdb,
1619 : fd_runtime_stack_t * runtime_stack,
1620 : fd_capture_ctx_t * capture_ctx,
1621 : int snapshot_boot,
1622 : uint win_lo );
1623 :
1624 : /* Process reward distribution for the block if it is inside reward interval.
1625 :
1626 : https://github.com/anza-xyz/agave/blob/v4.0.0-beta.6/runtime/src/bank/partitioned_epoch_rewards/distribution.rs#L45-L136 */
1627 : void
1628 : fd_distribute_partitioned_epoch_rewards( fd_banks_t * banks,
1629 : fd_bank_t * bank,
1630 : fd_accdb_t * accdb,
1631 : fd_runtime_stack_t * runtime_stack,
1632 4479 : fd_capture_ctx_t * capture_ctx ) {
1633 : /* https://github.com/anza-xyz/agave/blob/v4.0.0-beta.6/runtime/src/bank/partitioned_epoch_rewards/distribution.rs#L46-L48 */
1634 4479 : if( FD_LIKELY( bank->stake_rewards_fork_id==UCHAR_MAX ) ) return;
1635 :
1636 399 : fd_stake_rewards_t * stake_rewards = fd_bank_stake_rewards_modify( bank );
1637 :
1638 399 : ulong block_height = bank->f.block_height;
1639 399 : ulong distribution_starting_block_height = fd_stake_rewards_starting_block_height( stake_rewards, bank->stake_rewards_fork_id );
1640 399 : ulong distribution_end_exclusive = fd_stake_rewards_exclusive_ending_block_height( stake_rewards, bank->stake_rewards_fork_id );
1641 :
1642 : /* https://github.com/anza-xyz/agave/blob/v4.0.0-beta.6/runtime/src/bank/partitioned_epoch_rewards/distribution.rs#L55-L58 */
1643 399 : if( FD_UNLIKELY( block_height<distribution_starting_block_height ) ) {
1644 264 : return;
1645 264 : }
1646 :
1647 : /* The logic in Agave for EpochRewardPhase::Calculation has no direct
1648 : equivalent in Firedancer, because reward calculation is done
1649 : eagerly at the epoch boundary, whereas for Agave it's done at the
1650 : first distribution block.
1651 : https://github.com/anza-xyz/agave/blob/v4.0.0-beta.6/runtime/src/bank/partitioned_epoch_rewards/distribution.rs#L60-L90 */
1652 :
1653 135 : fd_epoch_schedule_t const * epoch_schedule = &bank->f.epoch_schedule;
1654 135 : ulong epoch = bank->f.epoch;
1655 :
1656 135 : if( FD_UNLIKELY( get_slots_in_epoch( epoch, epoch_schedule ) <= fd_stake_rewards_num_partitions( stake_rewards, bank->stake_rewards_fork_id ) ) ) {
1657 0 : FD_LOG_CRIT(( "Should not be distributing rewards" ));
1658 0 : }
1659 :
1660 : /* https://github.com/anza-xyz/agave/blob/v4.0.0-beta.6/runtime/src/bank/partitioned_epoch_rewards/distribution.rs#L110-L114 */
1661 135 : if( FD_LIKELY( block_height>=distribution_starting_block_height && block_height<distribution_end_exclusive ) ) {
1662 135 : ulong partition_idx = block_height-distribution_starting_block_height;
1663 :
1664 : /* The rewards of this partition are only in memory if the window
1665 : covers it. If they are not, re-derive the window that does. */
1666 135 : uchar fork_id = bank->stake_rewards_fork_id;
1667 135 : if( FD_UNLIKELY( partition_idx<(ulong)fd_stake_rewards_window_lo( stake_rewards, fork_id ) ||
1668 135 : partition_idx>(ulong)fd_stake_rewards_window_hi( stake_rewards, fork_id ) ) ) {
1669 0 : FD_LOG_INFO(( "reward partition is not in the window, recalculating" ));
1670 0 : recalculate_partitioned_rewards( banks, bank, accdb, runtime_stack, capture_ctx, 0, (uint)partition_idx );
1671 0 : stake_rewards = fd_bank_stake_rewards_modify( bank );
1672 0 : }
1673 :
1674 135 : distribute_epoch_rewards_in_partition( stake_rewards, partition_idx, bank, accdb, capture_ctx );
1675 135 : }
1676 :
1677 : /* If we have finished distributing rewards, set the status to inactive
1678 : https://github.com/anza-xyz/agave/blob/v4.0.0-beta.6/runtime/src/bank/partitioned_epoch_rewards/distribution.rs#L116-L135 */
1679 135 : if( fd_ulong_sat_add( block_height, 1UL )>=distribution_end_exclusive ) {
1680 129 : fd_sysvar_epoch_rewards_set_inactive( bank, accdb, capture_ctx );
1681 129 : fd_stake_rewards_release( stake_rewards, bank->stake_rewards_fork_id );
1682 129 : bank->stake_rewards_fork_id = UCHAR_MAX;
1683 129 : }
1684 135 : }
1685 :
1686 : /* Partitioned epoch rewards entry-point.
1687 :
1688 : https://github.com/anza-xyz/agave/blob/v3.0.4/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L102
1689 : */
1690 : void
1691 : fd_begin_partitioned_rewards( fd_bank_t * bank,
1692 : fd_accdb_t * accdb,
1693 : fd_runtime_stack_t * runtime_stack,
1694 : fd_capture_ctx_t * capture_ctx,
1695 : fd_stake_delegations_t const * stake_delegations,
1696 : fd_hash_t const * parent_blockhash,
1697 264 : ulong parent_epoch ) {
1698 :
1699 264 : calculate_rewards_and_distribute_vote_rewards(
1700 264 : bank,
1701 264 : accdb,
1702 264 : runtime_stack,
1703 264 : stake_delegations,
1704 264 : capture_ctx,
1705 264 : parent_epoch );
1706 :
1707 : /* Once the rewards for vote accounts have been distributed and stake
1708 : account rewards have been calculated, we can now set our epoch
1709 : reward status to be active and we can initialize the epoch rewards
1710 : sysvar. This sysvar is then deleted once all of the partitioned
1711 : stake rewards have been distributed.
1712 :
1713 : The Agave client calculates the partitions for each stake reward
1714 : when the first distribution block is reached. The Firedancer
1715 : client differs here since we hash the partitions during the epoch
1716 : boundary. */
1717 :
1718 264 : ulong distribution_starting_block_height = bank->f.block_height + REWARD_CALCULATION_NUM_BLOCKS;
1719 264 : uint num_partitions = fd_stake_rewards_num_partitions( fd_bank_stake_rewards_query( bank ), bank->stake_rewards_fork_id );
1720 :
1721 264 : fd_sysvar_epoch_rewards_init(
1722 264 : bank,
1723 264 : accdb,
1724 264 : capture_ctx,
1725 264 : runtime_stack->stakes.distributed_rewards,
1726 264 : distribution_starting_block_height,
1727 264 : num_partitions,
1728 264 : runtime_stack->stakes.total_rewards,
1729 264 : runtime_stack->stakes.total_points.ud,
1730 264 : parent_blockhash );
1731 264 : }
1732 :
1733 : /*
1734 : Re-calculates partitioned stake rewards.
1735 : This updates the slot context's epoch reward status with the recalculated partitioned rewards.
1736 :
1737 : Everything the calculation needs survives in the epoch rewards
1738 : sysvar and in the bank, so the partitions can be rebuilt at any
1739 : point during the distribution interval. The rebuilt partitions
1740 : always go into a freshly acquired fork, with its window starting at
1741 : win_lo, so that banks reading the fork this bank was using keep the
1742 : entries they computed for themselves. snapshot_boot selects where
1743 : the vote commissions come from: a bank restored from a snapshot has
1744 : to resolve them, every other caller reuses the ones the epoch
1745 : boundary resolved.
1746 :
1747 : https://github.com/anza-xyz/agave/blob/v2.2.14/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L521 */
1748 : static void
1749 : recalculate_partitioned_rewards( fd_banks_t * banks,
1750 : fd_bank_t * bank,
1751 : fd_accdb_t * accdb,
1752 : fd_runtime_stack_t * runtime_stack,
1753 : fd_capture_ctx_t * capture_ctx,
1754 : int snapshot_boot,
1755 3 : uint win_lo ) {
1756 :
1757 3 : fd_vote_rewards_map_t * vote_ele_map = runtime_stack->stakes.vote_map;
1758 3 : fd_vote_rewards_map_reset( vote_ele_map );
1759 :
1760 3 : ulong epoch_credits_len = *fd_bank_epoch_credits_len( bank );
1761 3 : fd_epoch_credits_t * epoch_credits_arr = fd_bank_epoch_credits( bank );
1762 :
1763 3 : if( FD_LIKELY( !snapshot_boot ) ) {
1764 :
1765 : /* The rewards of this epoch were computed once already, at the epoch
1766 : boundary or at snapshot load, and only the window is moving. The
1767 : commissions resolved back then are stored with the epoch credits
1768 : and must be reused: they come from a vote account snapshot two
1769 : epochs behind the rewarded epoch, which the top votes sets no
1770 : longer retain. */
1771 :
1772 0 : for( ulong i=0UL; i<epoch_credits_len; i++ ) {
1773 0 : fd_epoch_credits_t const * epoch_credits = &epoch_credits_arr[i];
1774 :
1775 0 : fd_vote_rewards_t * vote_ele = &runtime_stack->stakes.vote_ele[i];
1776 0 : vote_ele->pubkey = *(fd_pubkey_t const *)epoch_credits->pubkey;
1777 0 : vote_ele->vote_rewards = 0UL;
1778 0 : vote_ele->commission = epoch_credits->commission;
1779 0 : fd_vote_rewards_map_idx_insert( vote_ele_map, i, runtime_stack->stakes.vote_ele );
1780 0 : }
1781 :
1782 3 : } else {
1783 :
1784 : /* If the snapshot was loaded while partitioned epoch rewards is
1785 : active, then the vote rewards map must be populated with the state
1786 : of the vote accounts as of the end of the previous epoch boundary.
1787 : The epoch credits for these accounts are stored in the bank along
1788 : with the t-3 commission. With this, it's possible to recalculate
1789 : the rewards for the previous epoch boundary. We need the
1790 : commission from the end of the t-3 epoch if we are calculating
1791 : rewards for the transition from epoch t-1 to t since there needs to
1792 : be a 2 epoch commission gap for the delay_commission_updates
1793 : feature. */
1794 :
1795 3 : fd_vote_stakes_t * vote_stakes = fd_bank_vote_stakes( bank );
1796 3 : ulong fork_id = bank->vote_stakes_fork_id;
1797 :
1798 6 : for( ulong i=0UL; i<epoch_credits_len; i++ ) {
1799 3 : fd_epoch_credits_t * epoch_credits = &epoch_credits_arr[i];
1800 3 : fd_pubkey_t const * pubkey = (fd_pubkey_t const *)epoch_credits->pubkey;
1801 :
1802 : /* Get the t-1 stake account information. This is guaranteed to be
1803 : valid since the epoch credits are populated from the t-1 stakes
1804 : in the snapshot manifest. */
1805 3 : ushort commission_t_1 = 0;
1806 3 : FD_TEST( fd_vote_stakes_query_t_1( vote_stakes, fork_id, pubkey, NULL, NULL, &commission_t_1 ) );
1807 :
1808 : /* Now get the t-2 information (if it exists). This is not
1809 : guaranteed to be valid since it's possible for a vote account to
1810 : have been created in the last epoch. */
1811 3 : int exists_t_2 = 0;
1812 3 : ushort commission_t_2 = 0;
1813 3 : exists_t_2 = fd_vote_stakes_query_t_2( vote_stakes, fork_id, pubkey, NULL, NULL, NULL, NULL, &commission_t_2, NULL );
1814 :
1815 3 : fd_vote_rewards_t * vote_ele = &runtime_stack->stakes.vote_ele[i];
1816 3 : vote_ele->pubkey = *(fd_pubkey_t *)epoch_credits->pubkey;
1817 3 : vote_ele->vote_rewards = 0UL;
1818 3 : if( FD_FEATURE_ACTIVE_BANK( bank, delay_commission_updates ) ) {
1819 0 : vote_ele->commission = exists_t_2 ? commission_t_2 : commission_t_1;
1820 3 : } else {
1821 3 : vote_ele->commission = commission_t_1;
1822 3 : }
1823 3 : fd_vote_rewards_map_idx_insert( vote_ele_map, i, runtime_stack->stakes.vote_ele );
1824 3 : }
1825 :
1826 : /* Copy in historical commission information if it exists. */
1827 3 : if( FD_FEATURE_ACTIVE_BANK( bank, delay_commission_updates ) ) {
1828 0 : ulong commission_t_3_len = *fd_bank_snapshot_commission_t_3_len( bank );
1829 0 : fd_stashed_commission_t * commission_t_3 = fd_bank_snapshot_commission_t_3( bank );
1830 0 : for( ulong i=0UL; i<commission_t_3_len; i++ ) {
1831 0 : fd_stashed_commission_t const * ele = &commission_t_3[i];
1832 0 : fd_vote_rewards_t * vote_ele = fd_vote_rewards_map_ele_query( vote_ele_map, (fd_pubkey_t *)ele->pubkey, NULL, runtime_stack->stakes.vote_ele );
1833 0 : if( FD_LIKELY( vote_ele ) ) vote_ele->commission = ele->commission;
1834 0 : }
1835 0 : }
1836 :
1837 : /* Publish the resolved commissions so that any later repositioning
1838 : of this fork's window reuses them. */
1839 6 : for( ulong i=0UL; i<epoch_credits_len; i++ ) {
1840 3 : epoch_credits_arr[i].commission = runtime_stack->stakes.vote_ele[i].commission;
1841 3 : }
1842 3 : }
1843 :
1844 3 : fd_sysvar_epoch_rewards_t epoch_rewards_sysvar[1];
1845 3 : if( FD_UNLIKELY( !fd_sysvar_epoch_rewards_read( accdb, bank->accdb_fork_id, epoch_rewards_sysvar ) ) ) {
1846 0 : FD_LOG_DEBUG(( "Failed to read or decode epoch rewards sysvar - may not have been created yet" ));
1847 0 : return;
1848 0 : }
1849 :
1850 3 : FD_LOG_DEBUG(( "recalculating partitioned rewards" ));
1851 :
1852 3 : if( FD_UNLIKELY( !epoch_rewards_sysvar->active ) ) {
1853 0 : FD_LOG_DEBUG(( "epoch rewards is inactive" ));
1854 0 : return;
1855 0 : }
1856 :
1857 : /* If partitioned rewards are active, the rewarded epoch is always the immediately
1858 : preceding epoch.
1859 :
1860 : https://github.com/anza-xyz/agave/blob/2316fea4c0852e59c071f72d72db020017ffd7d0/runtime/src/bank/partitioned_epoch_rewards/calculation.rs#L566 */
1861 3 : FD_LOG_DEBUG(( "epoch rewards is active" ));
1862 :
1863 3 : ulong const epoch = bank->f.epoch;
1864 3 : ulong const rewarded_epoch = fd_ulong_sat_sub( epoch, 1UL );
1865 :
1866 3 : uchar stake_history_data[ FD_SYSVAR_STAKE_HISTORY_BINCODE_SZ ];
1867 3 : fd_stake_history_t stake_history[1];
1868 3 : read_stake_history( accdb, bank->accdb_fork_id, stake_history_data, stake_history );
1869 :
1870 3 : fd_stake_delegations_t const * stake_delegations = fd_bank_stake_delegations_frontier_query( banks, bank );
1871 :
1872 3 : calculate_stake_vote_rewards(
1873 3 : bank,
1874 3 : accdb,
1875 3 : stake_delegations,
1876 3 : capture_ctx,
1877 3 : stake_history,
1878 3 : rewarded_epoch,
1879 3 : epoch_rewards_sysvar->total_rewards,
1880 3 : epoch_rewards_sysvar->total_points.ud,
1881 3 : runtime_stack,
1882 3 : 1 );
1883 :
1884 : /* The recomputed rewards go into a fork of their own rather than over
1885 : the window the bank is holding: banks that branched off this one
1886 : share that window and are entitled to the entries they already have,
1887 : which were computed from their own delegations. */
1888 :
1889 3 : fd_stake_rewards_t * stake_rewards = fd_bank_stake_rewards_modify( bank );
1890 3 : uchar fork_idx = fd_stake_rewards_init( stake_rewards,
1891 3 : bank->f.epoch,
1892 3 : &epoch_rewards_sysvar->parent_blockhash,
1893 3 : epoch_rewards_sysvar->distribution_starting_block_height,
1894 3 : (uint)epoch_rewards_sysvar->num_partitions,
1895 3 : runtime_stack->stakes.stake_rewards_cnt );
1896 3 : if( FD_LIKELY( win_lo ) ) {
1897 0 : fd_stake_rewards_window_advance( stake_rewards,
1898 0 : fork_idx,
1899 0 : &epoch_rewards_sysvar->parent_blockhash,
1900 0 : win_lo,
1901 0 : runtime_stack->stakes.stake_rewards_cnt );
1902 0 : }
1903 3 : if( FD_LIKELY( bank->stake_rewards_fork_id!=UCHAR_MAX ) ) {
1904 0 : fd_stake_rewards_release( stake_rewards, bank->stake_rewards_fork_id );
1905 0 : }
1906 3 : bank->stake_rewards_fork_id = fork_idx;
1907 :
1908 3 : setup_stake_partitions(
1909 3 : bank,
1910 3 : accdb,
1911 3 : stake_history,
1912 3 : stake_delegations,
1913 3 : runtime_stack,
1914 3 : fork_idx,
1915 3 : rewarded_epoch,
1916 3 : epoch_rewards_sysvar->total_rewards,
1917 3 : epoch_rewards_sysvar->total_points.ud );
1918 :
1919 3 : fd_bank_stake_delegations_end_frontier_query( banks, bank );
1920 3 : }
1921 :
1922 : void
1923 : fd_rewards_recalculate_partitioned_rewards( fd_banks_t * banks,
1924 : fd_bank_t * bank,
1925 : fd_accdb_t * accdb,
1926 : fd_runtime_stack_t * runtime_stack,
1927 3 : fd_capture_ctx_t * capture_ctx ) {
1928 3 : recalculate_partitioned_rewards( banks, bank, accdb, runtime_stack, capture_ctx, 1, 0U );
1929 3 : }
|