Line data Source code
1 : #include "fd_alpenglow.h"
2 : #include "../rewards/fd_epoch_inflation_account.h"
3 :
4 : #include "../runtime/fd_accdb_svm.h"
5 : #include "../runtime/fd_pubkey_utils.h"
6 : #include "../runtime/program/vote/fd_vote_state_versioned.h"
7 : #include "../runtime/program/vote/fd_vote_codec_tmpl.h"
8 : #include "../runtime/sysvar/fd_sysvar_epoch_schedule.h"
9 :
10 : FD_STATIC_ASSERT( MAX_EPOCH_CREDITS_HISTORY==64UL, epoch_credits_bound );
11 :
12 : static int
13 : vote_stakes_iter_kind_for_epoch( ulong fork_id,
14 12 : ulong epoch ) {
15 12 : ulong fork_epoch = (ulong)fd_vote_stakes_fork_epoch( fork_id );
16 12 : if( FD_LIKELY( epoch==fork_epoch ) ) return FD_VOTE_STAKES_ITER_T_2;
17 6 : if( FD_LIKELY( fork_epoch && epoch==fork_epoch-1UL ) ) return FD_VOTE_STAKES_ITER_T_3;
18 0 : return 0;
19 6 : }
20 :
21 : /* Footer cert verification */
22 :
23 : /* TBD: this is a copy of the votor's ag_vote_signing_ser so the runtime
24 : does not link fd_choreo; keep the two in sync. */
25 :
26 0 : #define VOTE_TAG_NOTAR (1U) /* WireConsensusMessageKind::NotarVote */
27 : #define VOTE_TAG_FINAL (2U) /* WireConsensusMessageKind::FinalizeVote */
28 : #define VOTE_TAG_SKIP (3U) /* WireConsensusMessageKind::SkipVote */
29 :
30 : #define VOTE_SIGNING_SER_MAX ( sizeof(uchar) + sizeof(ulong) + sizeof(fd_hash_t) + sizeof(ushort) )
31 :
32 : static ulong
33 : vote_signing_ser( uint tag,
34 : ulong slot,
35 : uchar const * block_hash,
36 : ushort shred_version,
37 0 : uchar buf[ static VOTE_SIGNING_SER_MAX ] ) {
38 0 : ulong off = 0UL;
39 0 : buf[ off ] = (uchar)tag; off += sizeof(uchar);
40 0 : FD_STORE( ulong, buf+off, slot ); off += sizeof(ulong);
41 0 : if( FD_LIKELY( block_hash ) ) { memcpy( buf+off, block_hash, sizeof(fd_hash_t) ); off += sizeof(fd_hash_t); }
42 0 : FD_STORE( ushort, buf+off, shred_version ); off += sizeof(ushort);
43 0 : return off;
44 0 : }
45 :
46 : struct validator_set {
47 : ulong epoch;
48 : ulong validator_cnt; /* 0 if not built */
49 : ulong total_stake;
50 : blst_p1_affine bls_keys[ AG_VAT_MAX ]; /* indexed by rank */
51 : ulong stakes [ AG_VAT_MAX ]; /* indexed by rank */
52 : };
53 : typedef struct validator_set validator_set_t;
54 :
55 : /* validator_set_for_slot makes set hold the validators of slot's epoch,
56 : rebuilding it only when the epoch differs from the one it holds.
57 : Returns 1 on success, 0 if the bank holds no ranked validators for
58 : the epoch. */
59 :
60 : static int
61 : validator_set_for_slot( validator_set_t * set,
62 : fd_bank_t const * bank,
63 0 : ulong slot ) {
64 0 : ulong epoch = fd_slot_to_epoch( &bank->f.epoch_schedule, slot, NULL );
65 0 : if( FD_LIKELY( set->validator_cnt && set->epoch==epoch ) ) return 1;
66 :
67 0 : ulong fork_id = bank->vote_stakes_fork_id;
68 0 : int iter_kind = vote_stakes_iter_kind_for_epoch( fork_id, epoch );
69 0 : if( FD_UNLIKELY( !iter_kind ) ) {
70 0 : FD_LOG_WARNING(( "slot %lu: cert epoch %lu is not t-2 or t-3", bank->f.slot, epoch ));
71 0 : return 0;
72 0 : }
73 :
74 0 : set->validator_cnt = 0UL;
75 0 : fd_vote_stakes_t const * vote_stakes = fd_bank_vote_stakes( bank );
76 0 : ulong cnt = 0UL;
77 0 : ulong max_rank = 0UL;
78 0 : ulong total = 0UL;
79 0 : uchar __attribute__((aligned(FD_VOTE_STAKES_ITER_ALIGN))) iter_mem[ FD_VOTE_STAKES_ITER_FOOTPRINT ];
80 0 : for( fd_vote_stakes_iter_t * iter = fd_vote_stakes_iter_init( vote_stakes, fork_id, iter_kind, iter_mem );
81 0 : !fd_vote_stakes_iter_done( vote_stakes, fork_id, iter_kind, iter );
82 0 : fd_vote_stakes_iter_next( vote_stakes, fork_id, iter_kind, iter ) ) {
83 0 : fd_pubkey_t vote_key;
84 0 : ulong stake;
85 0 : ushort rank;
86 0 : uchar bls_key[ FD_BLS_PUBKEY_UNCOMPRESSED_SZ ];
87 0 : fd_vote_stakes_iter_ele( vote_stakes, fork_id, iter_kind, iter, &vote_key, NULL, &stake, NULL, NULL, NULL, NULL, &rank, NULL, bls_key );
88 0 : if( FD_UNLIKELY( rank==FD_VOTE_STAKES_ALPENGLOW_RANK_NULL ) ) continue;
89 0 : FD_TEST( rank<AG_VAT_MAX );
90 :
91 0 : if( FD_UNLIKELY( blst_p1_deserialize( set->bls_keys+rank, bls_key )!=BLST_SUCCESS ) ) {
92 0 : FD_LOG_WARNING(( "slot %lu: rank %u of epoch %lu has a malformed BLS key", bank->f.slot, rank, epoch ));
93 0 : return 0;
94 0 : }
95 0 : set->stakes[ rank ] = stake;
96 0 : max_rank = fd_ulong_max( max_rank, (ulong)rank );
97 0 : total += stake;
98 0 : cnt++;
99 0 : }
100 0 : if( FD_UNLIKELY( !cnt || cnt!=max_rank+1UL ) ) {
101 0 : FD_LOG_WARNING(( "slot %lu: epoch %lu has %lu ranked validators, highest rank %lu", bank->f.slot, epoch, cnt, max_rank ));
102 0 : return 0;
103 0 : }
104 0 : set->epoch = epoch;
105 0 : set->validator_cnt = cnt;
106 0 : set->total_stake = total;
107 0 : return 1;
108 0 : }
109 :
110 : /* cert_verify checks one footer cert against its epoch's validator set.
111 : The aggregate signature verifies over the vote with wire tag vote_tag the
112 : signers would have signed, and if quorum_numer is nonzero the signers
113 : hold quorum_numer/5 of the epoch's stake. i.e., for verifying
114 : reward certs, quorum_numer should be 0.
115 :
116 : Returns 1 if the cert verifies, 0 otherwise. */
117 :
118 : static int
119 : cert_verify( validator_set_t * set,
120 : fd_bank_t const * bank,
121 : fd_block_footer_cert_t const * cert,
122 : uint vote_tag,
123 : ulong quorum_numer,
124 0 : ushort shred_version ) {
125 0 : ulong bank_slot = bank->f.slot;
126 0 : ulong cert_slot = cert->slot;
127 :
128 0 : if( FD_UNLIKELY( !validator_set_for_slot( set, bank, cert_slot ) ) ) return 0;
129 :
130 0 : ulong last_rank = fd_bls_set_last( cert->signer_set ); /* ULONG_MAX when empty */
131 0 : if( FD_UNLIKELY( cert->nbits>set->validator_cnt || last_rank>=set->validator_cnt ) ) {
132 0 : FD_LOG_WARNING(( "slot %lu: footer cert for slot %lu names %u ranks (highest signer %lu) but its epoch has %lu validators",
133 0 : bank_slot, cert_slot, cert->nbits, last_rank, set->validator_cnt ));
134 0 : return 0;
135 0 : }
136 :
137 0 : fd_bls_pub_t pub[1]; memset( pub, 0, sizeof(fd_bls_pub_t) );
138 0 : ulong stake = 0UL;
139 0 : for( ulong rank=0UL; rank<=last_rank; rank++ ) {
140 0 : if( !fd_bls_set_test( cert->signer_set, rank ) ) continue;
141 0 : blst_p1_add_or_double_affine( pub, pub, set->bls_keys+rank );
142 0 : stake += set->stakes[ rank ];
143 0 : }
144 0 : if( FD_UNLIKELY( quorum_numer && (uint128)stake*(uint128)AG_QUORUM_THRESHOLD_DENOM<(uint128)set->total_stake*(uint128)quorum_numer ) ) {
145 0 : FD_LOG_WARNING(( "slot %lu: footer cert for slot %lu has %lu of %lu stake, below %lu/%lu",
146 0 : bank_slot, cert_slot, stake, set->total_stake, quorum_numer, AG_QUORUM_THRESHOLD_DENOM ));
147 0 : return 0;
148 0 : }
149 :
150 0 : blst_p2_affine sig_affine[1];
151 0 : fd_bls_sig_t sig[1];
152 0 : if( FD_UNLIKELY( blst_p2_uncompress( sig_affine, cert->sig )!=BLST_SUCCESS || !blst_p2_affine_in_g2( sig_affine ) ) ) {
153 0 : FD_LOG_WARNING(( "slot %lu: footer cert for slot %lu has a malformed signature", bank_slot, cert_slot ));
154 0 : return 0;
155 0 : }
156 0 : blst_p2_from_affine( sig, sig_affine );
157 :
158 0 : uchar payload[ VOTE_SIGNING_SER_MAX ];
159 0 : ulong payload_sz = vote_signing_ser( vote_tag, cert_slot, vote_tag==VOTE_TAG_NOTAR ? cert->block_id.uc : NULL, shred_version, payload );
160 0 : if( FD_UNLIKELY( !fd_bls_agg_verify( payload, payload_sz, pub, sig ) ) ) {
161 0 : FD_LOG_WARNING(( "slot %lu: footer (is_reward %d) cert for slot %lu failed signature verification", bank_slot, !quorum_numer, cert_slot ));
162 0 : return 0;
163 0 : }
164 0 : return 1;
165 0 : }
166 :
167 : int
168 : fd_alpenglow_footer_verify( fd_bank_t const * bank,
169 : fd_block_footer_t const * footer,
170 0 : ushort shred_version ) {
171 0 : int has_certs = footer->has_fast_final_cert | footer->has_final_cert | footer->has_skip_reward_cert | footer->has_notar_reward_cert;
172 0 : if( FD_LIKELY( !has_certs ) ) return 0;
173 0 : if( FD_UNLIKELY( !shred_version ) ) {
174 0 : FD_LOG_WARNING(( "slot %lu: footer carries certs but the shred version is not known yet; cannot verify them", bank->f.slot ));
175 0 : return -1;
176 0 : }
177 :
178 : /* ~200 KiB, kept across calls: consecutive blocks' certs almost
179 : always share an epoch */
180 0 : static FD_TL validator_set_t set[1];
181 :
182 0 : if( footer->has_fast_final_cert ) {
183 0 : if( FD_UNLIKELY( !cert_verify( set, bank, &footer->fast_final_cert, VOTE_TAG_NOTAR, AG_STRONG_QUORUM_THRESHOLD_NUMER, shred_version ) ) ) return -1;
184 0 : }
185 0 : if( footer->has_final_cert ) {
186 0 : if( FD_UNLIKELY( !cert_verify( set, bank, &footer->final_cert, VOTE_TAG_FINAL, AG_QUORUM_THRESHOLD_NUMER, shred_version ) ) ) return -1;
187 0 : if( FD_UNLIKELY( !cert_verify( set, bank, &footer->notar_cert, VOTE_TAG_NOTAR, AG_QUORUM_THRESHOLD_NUMER, shred_version ) ) ) return -1;
188 0 : }
189 0 : if( footer->has_skip_reward_cert ) {
190 0 : if( FD_UNLIKELY( !cert_verify( set, bank, &footer->skip_reward_cert, VOTE_TAG_SKIP, 0UL, shred_version ) ) ) return -1;
191 0 : }
192 0 : if( footer->has_notar_reward_cert ) {
193 0 : if( FD_UNLIKELY( !cert_verify( set, bank, &footer->notar_reward_cert, VOTE_TAG_NOTAR, 0UL, shred_version ) ) ) return -1;
194 0 : }
195 0 : return 0;
196 0 : }
197 :
198 : /* credits_increment mirrors Agave's alpenglow increment_credits
199 : (vote_reward.rs): accumulate credits into the tail epoch_credits
200 : entry, starting a new entry on epoch changes and inserting the
201 : tower->alpenglow migration marker in the migration epoch. The
202 : deque's capacity is MAX_EPOCH_CREDITS_HISTORY+1 while Agave pushes
203 : into an unbounded Vec before trimming, so we drop the head early
204 : when full; the final trimmed state is the same. */
205 :
206 : static void
207 : credits_increment( fd_vote_epoch_credits_t * ec,
208 : ulong migration_epoch,
209 : ulong epoch,
210 6 : ulong credits /* nonzero */ ) {
211 6 : if( epoch==migration_epoch ) {
212 6 : int have_marker = 0;
213 6 : for( deq_fd_vote_epoch_credits_t_iter_t iter = deq_fd_vote_epoch_credits_t_iter_init( ec );
214 6 : !deq_fd_vote_epoch_credits_t_iter_done( ec, iter );
215 6 : iter = deq_fd_vote_epoch_credits_t_iter_next( ec, iter ) ) {
216 3 : if( fd_vote_epoch_credits_is_alpenglow_marker( deq_fd_vote_epoch_credits_t_iter_ele( ec, iter ) ) ) { have_marker = 1; break; }
217 3 : }
218 6 : if( !have_marker ) {
219 3 : if( FD_UNLIKELY( deq_fd_vote_epoch_credits_t_full( ec ) ) ) deq_fd_vote_epoch_credits_t_pop_head( ec );
220 3 : deq_fd_vote_epoch_credits_t_push_tail( ec, (fd_vote_epoch_credits_t){ .epoch=ULONG_MAX, .credits=ULONG_MAX, .prev_credits=ULONG_MAX } );
221 3 : }
222 6 : }
223 :
224 6 : ulong cnt = deq_fd_vote_epoch_credits_t_cnt( ec );
225 6 : if( FD_UNLIKELY( !cnt ) ) {
226 0 : deq_fd_vote_epoch_credits_t_push_tail( ec, (fd_vote_epoch_credits_t){ .epoch=epoch, .credits=credits, .prev_credits=0UL } );
227 0 : return;
228 0 : }
229 :
230 6 : fd_vote_epoch_credits_t * last = deq_fd_vote_epoch_credits_t_peek_tail( ec );
231 :
232 6 : if( fd_vote_epoch_credits_is_alpenglow_marker( last ) ) {
233 : /* If there was a tower entry before the marker, its final credits
234 : form this entry's initial credits. */
235 3 : ulong final_tower_credits = 0UL;
236 3 : if( cnt>=2UL ) {
237 0 : ulong idx = 0UL;
238 0 : for( deq_fd_vote_epoch_credits_t_iter_t iter = deq_fd_vote_epoch_credits_t_iter_init( ec );
239 0 : !deq_fd_vote_epoch_credits_t_iter_done( ec, iter );
240 0 : iter = deq_fd_vote_epoch_credits_t_iter_next( ec, iter ) ) {
241 0 : if( idx==cnt-2UL ) { final_tower_credits = deq_fd_vote_epoch_credits_t_iter_ele( ec, iter )->credits; break; }
242 0 : idx++;
243 0 : }
244 0 : }
245 3 : if( FD_UNLIKELY( deq_fd_vote_epoch_credits_t_full( ec ) ) ) deq_fd_vote_epoch_credits_t_pop_head( ec );
246 3 : deq_fd_vote_epoch_credits_t_push_tail( ec, (fd_vote_epoch_credits_t){
247 3 : .epoch = epoch,
248 3 : .credits = fd_ulong_sat_add( credits, final_tower_credits ),
249 3 : .prev_credits = final_tower_credits } );
250 3 : while( deq_fd_vote_epoch_credits_t_cnt( ec )>MAX_EPOCH_CREDITS_HISTORY ) deq_fd_vote_epoch_credits_t_pop_head( ec );
251 3 : return;
252 3 : }
253 :
254 3 : if( last->epoch==epoch ) {
255 3 : last->credits = fd_ulong_sat_add( last->credits, credits );
256 3 : return;
257 3 : }
258 :
259 0 : if( last->credits==last->prev_credits ) {
260 : /* Different epochs but the latest epoch earned no credits, reuse
261 : the entry. */
262 0 : last->epoch = epoch;
263 0 : last->credits = fd_ulong_sat_add( last->credits, credits );
264 0 : return;
265 0 : }
266 :
267 0 : ulong final_credits = last->credits;
268 0 : if( FD_UNLIKELY( deq_fd_vote_epoch_credits_t_full( ec ) ) ) deq_fd_vote_epoch_credits_t_pop_head( ec );
269 0 : deq_fd_vote_epoch_credits_t_push_tail( ec, (fd_vote_epoch_credits_t){
270 0 : .epoch = epoch,
271 0 : .credits = fd_ulong_sat_add( credits, final_credits ),
272 0 : .prev_credits = final_credits } );
273 0 : while( deq_fd_vote_epoch_credits_t_cnt( ec )>MAX_EPOCH_CREDITS_HISTORY ) deq_fd_vote_epoch_credits_t_pop_head( ec );
274 0 : }
275 :
276 : /* vs_maybe_update_votes mirrors VoteState::maybe_update_votes: the
277 : votes deque is replaced by a single landed vote on the latest voted
278 : slot, and last_timestamp advances to the slot's timestamp if newer. */
279 :
280 : static void
281 : vs_maybe_update_votes( fd_vote_state_versioned_t * vs,
282 : ulong slot,
283 12 : long slot_timestamp_ns ) {
284 12 : ulong latest = slot;
285 12 : ulong const * last_voted = fd_vsv_get_last_voted_slot( vs );
286 12 : if( last_voted && *last_voted>latest ) latest = *last_voted;
287 12 : ulong const * root = fd_vsv_get_root_slot( vs );
288 12 : if( root && *root>latest ) latest = *root;
289 :
290 12 : fd_landed_vote_t * votes = fd_vsv_get_votes_mutable( vs );
291 12 : deq_fd_landed_vote_t_remove_all( votes );
292 12 : deq_fd_landed_vote_t_push_tail( votes, (fd_landed_vote_t){ .latency=0, .lockout={ .slot=latest, .confirmation_count=1U } } );
293 :
294 12 : long timestamp = slot_timestamp_ns/1000000000L;
295 12 : if( timestamp>fd_vsv_get_last_timestamp( vs )->timestamp ) {
296 0 : fd_vsv_set_last_timestamp( vs, &(fd_vote_block_timestamp_t){ .slot=slot, .timestamp=timestamp } );
297 0 : }
298 12 : }
299 :
300 : /* vote_update describes the mutations to apply to one vote account. */
301 :
302 : struct vote_update {
303 : int update_votes; ulong vote_slot; long vote_ts_ns;
304 : int update_root; ulong root_slot;
305 : ulong credits; /* 0 = none */
306 : ulong migration_epoch;
307 : ulong current_epoch;
308 : };
309 : typedef struct vote_update vote_update_t;
310 :
311 : static int
312 : vote_account_read( fd_bank_t * bank,
313 : fd_accdb_t * accdb,
314 : fd_pubkey_t const * pk,
315 : fd_vote_state_versioned_t * vs,
316 : ulong * out_data_len,
317 15 : fd_pubkey_t * out_owner ) {
318 15 : fd_acc_t acc = fd_accdb_read_one( accdb, bank->accdb_fork_id, pk->uc );
319 15 : if( FD_UNLIKELY( !acc.lamports || !fd_vsv_is_correct_size_owner_and_init( acc.owner, acc.data, acc.data_len ) ) ) {
320 0 : fd_accdb_unread_one( accdb, &acc );
321 0 : return 0;
322 0 : }
323 15 : if( FD_UNLIKELY( fd_vsv_deserialize( &acc, vs ) ) ) {
324 0 : fd_accdb_unread_one( accdb, &acc );
325 0 : return 0;
326 0 : }
327 15 : *out_data_len = acc.data_len;
328 15 : fd_memcpy( out_owner->uc, acc.owner, sizeof(fd_pubkey_t) );
329 15 : fd_accdb_unread_one( accdb, &acc );
330 15 : return 1;
331 15 : }
332 :
333 : static void
334 : vote_account_write( fd_bank_t * bank,
335 : fd_accdb_t * accdb,
336 : fd_capture_ctx_t * capture_ctx,
337 : fd_pubkey_t const * pk,
338 : fd_pubkey_t const * owner,
339 : ulong data_len,
340 : fd_vote_state_versioned_t * vs,
341 15 : vote_update_t const * upd ) {
342 15 : static FD_TL uchar buf[ 8192UL ];
343 :
344 15 : if( upd->update_votes ) vs_maybe_update_votes( vs, upd->vote_slot, upd->vote_ts_ns );
345 15 : if( upd->update_root ) {
346 6 : ulong const * root = fd_vsv_get_root_slot( vs );
347 6 : ulong latest_root = fd_ulong_max( root ? *root : upd->root_slot, upd->root_slot );
348 6 : fd_vsv_set_root_slot( vs, &latest_root );
349 6 : }
350 15 : if( upd->credits ) credits_increment( fd_vsv_get_epoch_credits_mutable( vs ), upd->migration_epoch, upd->current_epoch, upd->credits );
351 :
352 15 : fd_memset( buf, 0, data_len );
353 15 : if( FD_UNLIKELY( fd_vote_state_versioned_serialize( vs, buf, data_len ) ) ) {
354 0 : FD_BASE58_ENCODE_32_BYTES( pk->uc, pk_b58 );
355 0 : FD_LOG_WARNING(( "slot %lu: vote account %s failed to serialize; skipping", bank->f.slot, pk_b58 ));
356 0 : return;
357 0 : }
358 15 : fd_accdb_svm_write( bank, accdb, capture_ctx, pk, owner, buf, data_len, 0UL, 0, 1 );
359 15 : }
360 :
361 : static void
362 : vote_account_modify( fd_bank_t * bank,
363 : fd_accdb_t * accdb,
364 : fd_capture_ctx_t * capture_ctx,
365 : fd_pubkey_t const * pk,
366 9 : vote_update_t const * upd ) {
367 9 : static FD_TL fd_vote_state_versioned_t vs[1];
368 9 : ulong data_len;
369 9 : fd_pubkey_t owner;
370 9 : if( FD_UNLIKELY( !vote_account_read( bank, accdb, pk, vs, &data_len, &owner ) ) ) return;
371 9 : vote_account_write( bank, accdb, capture_ctx, pk, &owner, data_len, vs, upd );
372 9 : }
373 :
374 : void
375 : fd_alpenglow_pda( char const * seed,
376 84 : fd_pubkey_t * out ) {
377 84 : fd_pubkey_t const * feature_id = &ids[ offsetof( fd_features_t, alpenglow )>>3 ].id;
378 :
379 84 : uchar const * seed_ = (uchar const *)seed;
380 84 : ulong seed_sz = strlen( seed );
381 84 : uchar bump;
382 84 : uint custom_err;
383 84 : FD_TEST( fd_pubkey_find_program_address( feature_id, 1UL, &seed_, &seed_sz, out, &bump, &custom_err )==FD_PUBKEY_SUCCESS );
384 84 : }
385 :
386 : ulong
387 : fd_alpenglow_migration_slot( fd_bank_t * bank,
388 5898 : fd_accdb_t * accdb ) {
389 5898 : if( FD_UNLIKELY( !FD_FEATURE_ACTIVE_BANK( bank, alpenglow ) ) ) return ULONG_MAX;
390 :
391 84 : fd_pubkey_t genesis_cert_addr;
392 84 : fd_alpenglow_pda( "carlgration", &genesis_cert_addr );
393 :
394 84 : ulong migration_slot = ULONG_MAX;
395 84 : fd_acc_t acc = fd_accdb_read_one( accdb, bank->accdb_fork_id, genesis_cert_addr.uc );
396 84 : if( FD_LIKELY( acc.lamports && acc.data_len>=8UL ) ) migration_slot = FD_LOAD( ulong, acc.data );
397 84 : fd_accdb_unread_one( accdb, &acc );
398 84 : return migration_slot;
399 5898 : }
400 :
401 : /* slot_timestamp backdates the footer timestamp by the duration of
402 : slots (slot, bank_slot], taking into account reduced slot times.
403 :
404 : https://github.com/anza-xyz/agave/blob/v4.3/runtime/src/block_component_processor/vote_reward.rs#L505 */
405 : static long
406 : slot_timestamp( fd_bank_t * bank,
407 : ulong slot,
408 12 : ulong footer_time_nanos ) {
409 12 : ulong dur = fd_slot_params_slot_range_duration_ns( bank, slot+1UL, bank->f.slot+1UL );
410 12 : return fd_long_sat_sub( (long)footer_time_nanos, (long)dur );
411 12 : }
412 :
413 : int
414 : fd_alpenglow_rewards_apply( fd_bank_t * bank,
415 : fd_accdb_t * accdb,
416 : fd_capture_ctx_t * capture_ctx,
417 12 : fd_block_footer_t const * footer ) {
418 :
419 12 : ulong bank_slot = bank->f.slot;
420 12 : ulong footer_time_nanos = footer->block_producer_time_nanos;
421 12 : ulong current_epoch = fd_slot_to_epoch( &bank->f.epoch_schedule, bank_slot, NULL );
422 12 : ulong migration_epoch = ULONG_MAX;
423 12 : ulong leader_credits = 0UL;
424 :
425 : /* credits for the attested voters of the reward slot */
426 :
427 12 : if( footer->has_skip_reward_cert || footer->has_notar_reward_cert ) {
428 6 : fd_bls_set_t reward_set[ fd_bls_set_word_cnt ];
429 6 : fd_bls_set_null( reward_set );
430 6 : ulong skip_slot = ULONG_MAX, notar_slot = ULONG_MAX;
431 6 : if( footer->has_skip_reward_cert ) {
432 6 : skip_slot = footer->skip_reward_cert.slot;
433 6 : fd_bls_set_union( reward_set, reward_set, footer->skip_reward_cert.signer_set );
434 6 : }
435 6 : if( footer->has_notar_reward_cert ) {
436 0 : notar_slot = footer->notar_reward_cert.slot;
437 0 : fd_bls_set_union( reward_set, reward_set, footer->notar_reward_cert.signer_set );
438 0 : }
439 6 : if( FD_UNLIKELY( skip_slot!=ULONG_MAX && notar_slot!=ULONG_MAX && skip_slot!=notar_slot ) ) {
440 0 : FD_LOG_WARNING(( "slot %lu: reward cert slots differ: skip %lu, notar %lu", bank_slot, skip_slot, notar_slot ));
441 0 : return -1;
442 0 : }
443 6 : ulong reward_slot = fd_ulong_min( skip_slot, notar_slot );
444 :
445 6 : ulong migration_slot = fd_alpenglow_migration_slot( bank, accdb );
446 6 : if( FD_UNLIKELY( migration_slot==ULONG_MAX ) ) {
447 0 : FD_LOG_WARNING(( "slot %lu: reward cert but no genesis certificate account", bank_slot ));
448 0 : return -1;
449 0 : }
450 6 : migration_epoch = fd_slot_to_epoch( &bank->f.epoch_schedule, migration_slot, NULL );
451 :
452 6 : if( FD_UNLIKELY( fd_ulong_sat_add( reward_slot, FD_NUM_SLOTS_FOR_REWARD )!=bank_slot || reward_slot<=migration_slot ) ) {
453 0 : FD_LOG_WARNING(( "slot %lu: invalid reward cert slot %lu (migration slot %lu)", bank_slot, reward_slot, migration_slot ));
454 0 : return -1;
455 0 : }
456 :
457 6 : ulong reward_epoch = fd_slot_to_epoch( &bank->f.epoch_schedule, reward_slot, NULL );
458 :
459 : /* Changed to charge rewards against the processing bank's epoch
460 : inflation budget, not the reward slot's epoch. Handles rewards
461 : going over budget when inflation rate decreases.
462 : https://github.com/anza-xyz/agave/blob/386cf57c45e135d8a3a8b7d16877eb896f695c64/runtime/src/block_component_processor/vote_reward.rs#L194 */
463 6 : fd_epoch_inflation_account_state_t inflation[1];
464 6 : fd_epoch_inflation_state_t const * inflation_state = NULL;
465 6 : if( FD_LIKELY( fd_epoch_inflation_account_read( bank, accdb, inflation ) ) ) {
466 6 : if( inflation->current.epoch==current_epoch ) inflation_state = &inflation->current;
467 0 : else if( inflation->has_prev && inflation->prev.epoch==current_epoch ) inflation_state = &inflation->prev;
468 6 : }
469 6 : if( FD_UNLIKELY( !inflation_state ) ) {
470 0 : FD_LOG_WARNING(( "slot %lu: no epoch inflation state for epoch %lu", bank_slot, current_epoch ));
471 0 : return -1;
472 0 : }
473 6 : ulong max_reward = inflation_state->max_possible_validator_reward;
474 6 : ulong slots_per_epoch = inflation_state->slots_per_epoch;
475 :
476 6 : ulong total_stake = fd_vote_stakes_total_stake( fd_bank_vote_stakes( bank ), reward_epoch );
477 6 : if( FD_UNLIKELY( !total_stake ) ) {
478 0 : FD_LOG_WARNING(( "slot %lu: no epoch stakes for reward epoch %lu", bank_slot, reward_epoch ));
479 0 : return -1;
480 0 : }
481 :
482 6 : long ts_ns = slot_timestamp( bank, reward_slot, footer_time_nanos );
483 6 : int have_ranked_vote = 0;
484 6 : fd_vote_stakes_t const * vote_stakes = fd_bank_vote_stakes( bank );
485 6 : int iter_kind = vote_stakes_iter_kind_for_epoch( bank->vote_stakes_fork_id, reward_epoch );
486 6 : if( FD_UNLIKELY( !iter_kind ) ) {
487 0 : FD_LOG_WARNING(( "slot %lu: reward epoch %lu is not t-2 or t-3", bank_slot, reward_epoch ));
488 0 : return -1;
489 0 : }
490 6 : uchar __attribute__((aligned(FD_VOTE_STAKES_ITER_ALIGN))) iter_mem[ FD_VOTE_STAKES_ITER_FOOTPRINT ];
491 6 : for( fd_vote_stakes_iter_t * iter = fd_vote_stakes_iter_init( vote_stakes, bank->vote_stakes_fork_id, iter_kind, iter_mem );
492 18 : !fd_vote_stakes_iter_done( vote_stakes, bank->vote_stakes_fork_id, iter_kind, iter );
493 12 : fd_vote_stakes_iter_next( vote_stakes, bank->vote_stakes_fork_id, iter_kind, iter ) ) {
494 12 : fd_pubkey_t vote_key;
495 12 : ulong stake;
496 12 : ushort rank;
497 12 : fd_vote_stakes_iter_ele( vote_stakes, bank->vote_stakes_fork_id, iter_kind, iter,
498 12 : &vote_key, NULL, &stake, NULL, NULL, NULL, NULL, &rank, NULL, NULL );
499 12 : if( FD_UNLIKELY( rank==FD_VOTE_STAKES_ALPENGLOW_RANK_NULL ) ) continue;
500 12 : FD_TEST( rank<AG_VAT_MAX );
501 12 : have_ranked_vote = 1;
502 12 : ulong r = (ulong)rank;
503 12 : if( !fd_bls_set_test( reward_set, r ) ) continue;
504 : /* per-slot, stake-fractional reward; split half validator, half
505 : (rounded up) leader */
506 6 : uint128 numerator = (uint128)max_reward*(uint128)stake;
507 6 : uint128 denominator = (uint128)slots_per_epoch*(uint128)total_stake;
508 6 : ulong reward = denominator ? (ulong)( numerator/denominator ) : 0UL;
509 6 : ulong validator_reward = reward/2UL;
510 :
511 6 : vote_update_t upd = {
512 6 : .update_votes = 1, .vote_slot = reward_slot, .vote_ts_ns = ts_ns,
513 6 : .credits = validator_reward,
514 6 : .migration_epoch = migration_epoch,
515 6 : .current_epoch = current_epoch,
516 6 : };
517 6 : static FD_TL fd_vote_state_versioned_t vs[1];
518 6 : ulong data_len;
519 6 : fd_pubkey_t owner;
520 :
521 : /* Skip this node if the vote account cannot be deserialized
522 : https://github.com/anza-xyz/agave/blob/v4.3.0-beta.3/runtime/src/block_component_processor/vote_reward.rs#L378-L380 */
523 6 : if( FD_UNLIKELY( !vote_account_read( bank, accdb, &vote_key, vs, &data_len, &owner ) ) ) continue;
524 :
525 : /* Only accumulate the leader credits if the vote account could
526 : be successfully read.
527 :
528 : https://github.com/anza-xyz/agave/blob/v4.3.0-beta.3/runtime/src/block_component_processor/vote_reward.rs#L249 */
529 6 : leader_credits = fd_ulong_sat_add( leader_credits, reward-validator_reward );
530 6 : vote_account_write( bank, accdb, capture_ctx, &vote_key, &owner, data_len, vs, &upd );
531 6 : }
532 6 : if( FD_UNLIKELY( !have_ranked_vote ) ) {
533 0 : FD_LOG_WARNING(( "slot %lu: no ranked validators for reward slot %lu", bank_slot, reward_slot ));
534 0 : return -1;
535 0 : }
536 6 : }
537 :
538 : /* finalization cert: root/votes/timestamp for the signers */
539 :
540 12 : if( footer->has_fast_final_cert || footer->has_final_cert ) {
541 6 : ulong final_slot;
542 6 : fd_bls_set_t final_set[ fd_bls_set_word_cnt ];
543 6 : if( footer->has_fast_final_cert ) {
544 6 : final_slot = footer->fast_final_cert.slot;
545 6 : fd_bls_set_copy( final_set, footer->fast_final_cert.signer_set );
546 6 : } else {
547 0 : final_slot = footer->final_cert.slot;
548 0 : fd_bls_set_union( final_set, footer->final_cert.signer_set, footer->notar_cert.signer_set );
549 0 : }
550 :
551 6 : ulong final_epoch = fd_slot_to_epoch( &bank->f.epoch_schedule, final_slot, NULL );
552 :
553 6 : long ts_ns = slot_timestamp( bank, final_slot, footer_time_nanos );
554 6 : int have_ranked_vote = 0;
555 6 : fd_vote_stakes_t const * vote_stakes = fd_bank_vote_stakes( bank );
556 6 : int iter_kind = vote_stakes_iter_kind_for_epoch( bank->vote_stakes_fork_id, final_epoch );
557 6 : if( FD_UNLIKELY( !iter_kind ) ) {
558 0 : FD_LOG_WARNING(( "slot %lu: finalization epoch %lu is not t-2 or t-3", bank_slot, final_epoch ));
559 0 : return -1;
560 0 : }
561 6 : uchar __attribute__((aligned(FD_VOTE_STAKES_ITER_ALIGN))) iter_mem[ FD_VOTE_STAKES_ITER_FOOTPRINT ];
562 6 : for( fd_vote_stakes_iter_t * iter = fd_vote_stakes_iter_init( vote_stakes, bank->vote_stakes_fork_id, iter_kind, iter_mem );
563 18 : !fd_vote_stakes_iter_done( vote_stakes, bank->vote_stakes_fork_id, iter_kind, iter );
564 12 : fd_vote_stakes_iter_next( vote_stakes, bank->vote_stakes_fork_id, iter_kind, iter ) ) {
565 12 : fd_pubkey_t vote_key;
566 12 : ushort rank;
567 12 : fd_vote_stakes_iter_ele( vote_stakes, bank->vote_stakes_fork_id, iter_kind, iter,
568 12 : &vote_key, NULL, NULL, NULL, NULL, NULL, NULL, &rank, NULL, NULL );
569 12 : if( FD_UNLIKELY( rank==FD_VOTE_STAKES_ALPENGLOW_RANK_NULL ) ) continue;
570 12 : FD_TEST( rank<AG_VAT_MAX );
571 12 : have_ranked_vote = 1;
572 12 : ulong r = (ulong)rank;
573 12 : if( !fd_bls_set_test( final_set, r ) ) continue;
574 6 : vote_update_t upd = {
575 6 : .update_root = 1, .root_slot = final_slot,
576 6 : .update_votes = 1, .vote_slot = final_slot, .vote_ts_ns = ts_ns,
577 6 : };
578 6 : vote_account_modify( bank, accdb, capture_ctx, &vote_key, &upd );
579 6 : }
580 6 : if( FD_UNLIKELY( !have_ranked_vote ) ) {
581 0 : FD_LOG_WARNING(( "slot %lu: no ranked validators for finalized slot %lu", bank_slot, final_slot ));
582 0 : return -1;
583 0 : }
584 6 : }
585 :
586 : /* leader rewards */
587 :
588 12 : if( leader_credits ) {
589 3 : fd_pubkey_t const * leader_vote_pubkey = fd_epoch_leaders_get_vote( fd_bank_epoch_leaders_query( bank, current_epoch ), bank_slot );
590 3 : if( FD_UNLIKELY( !leader_vote_pubkey ) ) {
591 0 : FD_LOG_WARNING(( "slot %lu: leader vote account unknown; dropping %lu leader reward credits (bank hash will diverge)", bank_slot, leader_credits ));
592 3 : } else {
593 3 : vote_update_t upd = {
594 3 : .credits = leader_credits,
595 3 : .migration_epoch = migration_epoch,
596 3 : .current_epoch = current_epoch,
597 3 : };
598 3 : vote_account_modify( bank, accdb, capture_ctx, leader_vote_pubkey, &upd );
599 3 : }
600 3 : }
601 :
602 12 : return 0;
603 12 : }
|