Line data Source code
1 : #include <limits.h>
2 : #include <unistd.h>
3 : #include <fcntl.h>
4 : #include <sys/stat.h>
5 :
6 : #include "fd_tower.h"
7 : #include "../voter/fd_voter.h"
8 : #include "../voter/fd_voter_private.h"
9 : #include "fd_tower_forks.h"
10 : #include "fd_tower_serde.h"
11 : #include "../../flamenco/txn/fd_txn_generate.h"
12 : #include "../../flamenco/runtime/fd_system_ids.h"
13 :
14 : #define LOGGING 0
15 :
16 0 : #define THRESHOLD_DEPTH (8)
17 0 : #define THRESHOLD_RATIO (2.0 / 3.0)
18 : #define SWITCH_RATIO (0.38)
19 :
20 : /* expiration calculates the expiration slot of vote given a slot and
21 : confirmation count. */
22 :
23 : static inline ulong
24 132 : expiration_slot( fd_tower_t const * vote ) {
25 132 : ulong lockout = 1UL << vote->conf;
26 132 : return vote->slot + lockout;
27 132 : }
28 :
29 : /* simulate_vote simulates voting for slot, popping all votes from the
30 : top that would be consecutively expired by voting for slot. */
31 :
32 : ulong
33 : simulate_vote( fd_tower_t const * tower,
34 141 : ulong slot ) {
35 141 : ulong cnt = fd_tower_cnt( tower );
36 147 : while( cnt ) {
37 132 : fd_tower_t const * top_vote = fd_tower_peek_index_const( tower, cnt - 1 );
38 132 : if( FD_LIKELY( expiration_slot( top_vote ) >= slot ) ) break; /* expire only if consecutive */
39 6 : cnt--;
40 6 : }
41 141 : return cnt;
42 141 : }
43 :
44 : /* push_vote pushes a new vote for slot onto the tower. Pops and
45 : returns the new root (bottom of the tower) if it reaches max lockout
46 : as a result of the new vote. Otherwise, returns ULONG_MAX.
47 :
48 : Max lockout is equivalent to 1 << FD_TOWER_VOTE_MAX + 1 (which
49 : implies confirmation count is FD_TOWER_VOTE_MAX + 1). As a result,
50 : fd_tower_vote also maintains the invariant that the tower contains at
51 : most FD_TOWER_VOTE_MAX votes, because (in addition to vote expiry)
52 : there will always be a pop before reaching FD_TOWER_VOTE_MAX + 1. */
53 :
54 : ulong
55 : push_vote( fd_tower_t * tower,
56 141 : ulong slot ) {
57 :
58 141 : # if FD_TOWER_PARANOID
59 141 : fd_tower_t const * vote = fd_tower_peek_tail_const( tower );
60 141 : if( FD_UNLIKELY( vote && slot <= vote->slot ) ) FD_LOG_ERR(( "[%s] slot %lu <= vote->slot %lu", __func__, slot, vote->slot )); /* caller error*/
61 141 : # endif
62 :
63 : /* Use simulate_vote to determine how many expired votes to pop. */
64 :
65 141 : ulong cnt = simulate_vote( tower, slot );
66 :
67 : /* Pop everything that got expired. */
68 :
69 147 : while( FD_LIKELY( fd_tower_cnt( tower ) > cnt ) ) {
70 6 : fd_tower_pop_tail( tower );
71 6 : }
72 :
73 : /* If the tower is still full after expiring, then pop and return the
74 : bottom vote slot as the new root because this vote has incremented
75 : it to max lockout. Otherwise this is a no-op and there is no new
76 : root (FD_SLOT_NULL). */
77 :
78 141 : ulong root = FD_SLOT_NULL;
79 141 : if( FD_LIKELY( fd_tower_full( tower ) ) ) { /* optimize for full tower */
80 0 : root = fd_tower_pop_head( tower ).slot;
81 0 : }
82 :
83 : /* Increment confirmations (double lockouts) for consecutive
84 : confirmations in prior votes. */
85 :
86 141 : ulong prev_conf = 0;
87 141 : for( fd_tower_iter_t iter = fd_tower_iter_init_rev( tower );
88 1635 : !fd_tower_iter_done_rev( tower, iter );
89 1494 : iter = fd_tower_iter_prev ( tower, iter ) ) {
90 1494 : fd_tower_t * vote = fd_tower_iter_ele( tower, iter );
91 1494 : if( FD_UNLIKELY( vote->conf != ++prev_conf ) ) break;
92 1494 : vote->conf++;
93 1494 : }
94 :
95 : /* Add the new vote to the tower. */
96 :
97 141 : fd_tower_push_tail( tower, (fd_tower_t){ .slot = slot, .conf = 1 } );
98 :
99 : /* Return the new root (FD_SLOT_NULL if there is none). */
100 :
101 141 : return root;
102 141 : }
103 :
104 : /* lockout_check checks if we are locked out from voting for slot.
105 : Returns 1 if we can vote for slot without violating lockout, 0
106 : otherwise.
107 :
108 : After voting for a slot n, we are locked out for 2^k slots, where k
109 : is the confirmation count of that vote. Once locked out, we cannot
110 : vote for a different fork until that previously-voted fork expires at
111 : slot n+2^k. This implies the earliest slot in which we can switch
112 : from the previously-voted fork is (n+2^k)+1. We use `ghost` to
113 : determine whether `slot` is on the same or different fork as previous
114 : vote slots.
115 :
116 : In the case of the tower, every vote has its own expiration slot
117 : depending on confirmations. The confirmation count is the max number
118 : of consecutive votes that have been pushed on top of the vote, and
119 : not necessarily its current height in the tower.
120 :
121 : For example, the following is a diagram of a tower pushing and
122 : popping with each vote:
123 :
124 :
125 : slot | confirmation count
126 : -----|-------------------
127 : 4 | 1 <- vote
128 : 3 | 2
129 : 2 | 3
130 : 1 | 4
131 :
132 :
133 : slot | confirmation count
134 : -----|-------------------
135 : 9 | 1 <- vote
136 : 2 | 3
137 : 1 | 4
138 :
139 :
140 : slot | confirmation count
141 : -----|-------------------
142 : 10 | 1 <- vote
143 : 9 | 2
144 : 2 | 3
145 : 1 | 4
146 :
147 :
148 : slot | confirmation count
149 : -----|-------------------
150 : 11 | 1 <- vote
151 : 10 | 2
152 : 9 | 3
153 : 2 | 4
154 : 1 | 5
155 :
156 :
157 : slot | confirmation count
158 : -----|-------------------
159 : 18 | 1 <- vote
160 : 2 | 4
161 : 1 | 5
162 :
163 :
164 : In the final tower, note the gap in confirmation counts between slot
165 : 18 and slot 2, even though slot 18 is directly above slot 2. */
166 :
167 : int
168 : lockout_check( fd_tower_t const * tower,
169 : fd_forks_t * forks,
170 0 : ulong slot ) {
171 :
172 0 : if( FD_UNLIKELY( fd_tower_empty( tower ) ) ) return 1; /* always not locked out if we haven't voted. */
173 0 : if( FD_UNLIKELY( slot <= fd_tower_peek_tail_const( tower )->slot ) ) return 0; /* always locked out from voting for slot <= last vote slot */
174 :
175 : /* Simulate a vote to pop off all the votes that would be expired by
176 : voting for slot. Then check if the newly top-of-tower vote is on
177 : the same fork as slot (if so this implies we can vote for it). */
178 :
179 0 : ulong cnt = simulate_vote( tower, slot ); /* pop off votes that would be expired */
180 0 : if( FD_UNLIKELY( !cnt ) ) return 1; /* tower is empty after popping expired votes */
181 :
182 0 : fd_tower_t const * vote = fd_tower_peek_index_const( tower, cnt - 1 ); /* newly top-of-tower */
183 0 : int lockout = fd_forks_is_slot_descendant( forks, vote->slot, slot ); /* check if on same fork */
184 0 : FD_LOG_INFO(( "[%s] lockout? %d. last_vote_slot: %lu. slot: %lu", __func__, lockout, vote->slot, slot ));
185 0 : return lockout;
186 0 : }
187 :
188 : /* switch_check checks if we can switch to the fork of `slot`. Returns
189 : 1 if we can switch, 0 otherwise. Assumes tower is non-empty.
190 :
191 : There are two forks of interest: our last vote fork ("vote fork") and
192 : the fork we want to switch to ("switch fork"). The switch fork is on
193 : the fork of `slot`.
194 :
195 : In order to switch, FD_TOWER_SWITCH_PCT of stake must have voted for
196 : a slot that satisfies the following conditions: the
197 : GCA(slot, last_vote) is an ancestor of the switch_slot
198 :
199 : Recall from the lockout check a validator is locked out from voting
200 : for our last vote slot when their last vote slot is on a different
201 : fork, and that vote's expiration slot > our last vote slot.
202 :
203 : The following pseudocode describes the algorithm:
204 :
205 : ```
206 : for every fork f in the fork tree, take the most recently executed
207 : slot `s` (the leaf of the fork).
208 :
209 : Take the greatest common ancestor of the `s` and the our last vote
210 : slot. If the switch_slot is a descendant of this GCA, then votes for
211 : `s` can count towards the switch threshold.
212 :
213 : query banks(`s`) for vote accounts in `s`
214 : for all vote accounts v in `s`
215 : if v's locked out[1] from voting for our latest vote slot
216 : add v's stake to switch stake
217 :
218 : return switch stake >= FD_TOWER_SWITCH_PCT
219 : ```
220 :
221 : The switch check is used to safeguard optimistic confirmation.
222 : Specifically: FD_TOWER_OPT_CONF_PCT + FD_TOWER_SWITCH_PCT >= 1. */
223 :
224 : int
225 : switch_check( fd_tower_t const * tower,
226 : fd_forks_t * forks,
227 : fd_epoch_stakes_t * epoch_stakes,
228 : ulong total_stake,
229 33 : ulong switch_slot ) {
230 33 : ulong switch_stake = 0;
231 33 : ulong last_vote_slot = fd_tower_peek_tail_const( tower )->slot;
232 33 : ulong root_slot = fd_tower_peek_head_const( tower )->slot;
233 33 : for ( fd_tower_leaves_dlist_iter_t iter = fd_tower_leaves_dlist_iter_fwd_init( forks->tower_leaves_dlist, forks->tower_leaves_pool );
234 138 : !fd_tower_leaves_dlist_iter_done( iter, forks->tower_leaves_dlist, forks->tower_leaves_pool );
235 120 : iter = fd_tower_leaves_dlist_iter_fwd_next( iter, forks->tower_leaves_dlist, forks->tower_leaves_pool ) ) {
236 :
237 : /* Iterate over all the leaves of all forks */
238 120 : fd_tower_leaf_t * leaf = fd_tower_leaves_dlist_iter_ele( iter, forks->tower_leaves_dlist, forks->tower_leaves_pool );
239 120 : ulong candidate_slot = leaf->slot;
240 120 : ulong lca = fd_forks_lowest_common_ancestor( forks, candidate_slot, last_vote_slot );
241 :
242 120 : if( lca != ULONG_MAX && fd_forks_is_slot_descendant( forks, lca, switch_slot ) ) {
243 :
244 : /* This candidate slot may be considered for the switch proof, if
245 : it passes the following conditions:
246 :
247 : https://github.com/anza-xyz/agave/blob/c7b97bc77addacf03b229c51b47c18650d909576/core/src/consensus.rs#L1117
248 :
249 : Now for this candidate slot, look at the lockouts that were created at
250 : the time that we processed the bank for this candidate slot. */
251 :
252 78 : for( fd_lockout_slots_t const * slot = fd_lockout_slots_map_ele_query_const( forks->lockout_slots_map, &candidate_slot, NULL, forks->lockout_slots_pool );
253 84 : slot;
254 78 : slot = fd_lockout_slots_map_ele_next_const ( slot, NULL, forks->lockout_slots_pool ) ) {
255 21 : ulong interval_end = slot->interval_end;
256 21 : ulong key = fd_lockout_interval_key( candidate_slot, interval_end );
257 :
258 : /* Intervals are keyed by the end of the interval. If the end of
259 : the interval is < the last vote slot, then these vote
260 : accounts with this particular lockout are NOT locked out from
261 : voting for the last vote slot, which means we can skip this
262 : set of intervals. */
263 :
264 21 : if( interval_end < last_vote_slot ) continue;
265 :
266 : /* At this point we can actually query for the intervals by
267 : end interval to get the vote accounts. */
268 :
269 15 : for( fd_lockout_intervals_t const * interval = fd_lockout_intervals_map_ele_query_const( forks->lockout_intervals_map, &key, NULL, forks->lockout_intervals_pool );
270 24 : interval;
271 24 : interval = fd_lockout_intervals_map_ele_next_const( interval, NULL, forks->lockout_intervals_pool ) ) {
272 24 : ulong vote_slot = interval->interval_start;
273 24 : fd_hash_t const * vote_acc = &interval->vote_account_pubkey;
274 :
275 24 : if( FD_UNLIKELY( !fd_forks_is_slot_descendant( forks, vote_slot, last_vote_slot ) &&
276 24 : vote_slot > root_slot ) ) {
277 24 : fd_voter_stake_key_t key = { .vote_account = *vote_acc, .slot = switch_slot };
278 24 : fd_voter_stake_t const * voter_stake = fd_voter_stake_map_ele_query_const( epoch_stakes->voter_stake_map, &key, NULL, epoch_stakes->voter_stake_pool );
279 24 : if( FD_UNLIKELY( !voter_stake ) ) {
280 0 : FD_BASE58_ENCODE_32_BYTES( vote_acc->key, vote_acc_b58 );
281 0 : FD_LOG_CRIT(( "missing voter stake for vote account %s on slot %lu. Is this an error?", vote_acc_b58, switch_slot ));
282 0 : }
283 24 : ulong voter_idx = fd_voter_stake_pool_idx( epoch_stakes->voter_stake_pool, voter_stake );
284 :
285 : /* Don't count this vote account towards the switch cqheck if it has already been used. */
286 24 : if( FD_UNLIKELY( fd_used_acc_scratch_test( epoch_stakes->used_acc_scratch, voter_idx ) ) ) continue;
287 :
288 24 : fd_used_acc_scratch_insert( epoch_stakes->used_acc_scratch, voter_idx );
289 24 : switch_stake += voter_stake->stake;
290 24 : if( FD_LIKELY( (double)switch_stake >= (double)total_stake * SWITCH_RATIO ) ) {
291 15 : fd_used_acc_scratch_null( epoch_stakes->used_acc_scratch );
292 15 : FD_LOG_INFO(( "[%s] switch? 1. last_vote_slot: %lu. switch_slot: %lu. pct: %.0lf%%", __func__, last_vote_slot, switch_slot, (double)switch_stake / (double)total_stake * 100.0 ));
293 15 : return 1;
294 15 : }
295 24 : }
296 24 : }
297 15 : }
298 78 : }
299 120 : }
300 18 : fd_used_acc_scratch_null( epoch_stakes->used_acc_scratch );
301 18 : FD_LOG_INFO(( "[%s] switch? 0. last_vote_slot: %lu. switch_slot: %lu. pct: %.0lf%%", __func__, last_vote_slot, switch_slot, (double)switch_stake / (double)total_stake * 100.0 ));
302 18 : return 0;
303 33 : }
304 :
305 : /* threshold_check checks if we pass the threshold required to vote for
306 : `slot`. Returns 1 if we pass the threshold check, 0 otherwise.
307 :
308 : The following psuedocode describes the algorithm:
309 :
310 : ```
311 : simulate that we have voted for `slot`
312 :
313 : for all vote accounts in the current epoch
314 :
315 : simulate that the vote account has voted for `slot`
316 :
317 : pop all votes expired by that simulated vote
318 :
319 : if the validator's latest tower vote after expiry >= our threshold
320 : slot ie. our vote from THRESHOLD_DEPTH back also after simulating,
321 : then add validator's stake to threshold_stake.
322 :
323 : return threshold_stake >= FD_TOWER_THRESHOLD_RATIO
324 : ```
325 :
326 : The threshold check simulates voting for the current slot to expire
327 : stale votes. This is to prevent validators that haven't voted in a
328 : long time from counting towards the threshold stake. */
329 :
330 : int
331 : threshold_check( fd_tower_t const * tower,
332 : fd_tower_accts_t const * accts,
333 : ulong total_stake,
334 0 : ulong slot ) {
335 :
336 0 : uchar __attribute__((aligned(FD_TOWER_ALIGN))) scratch[ FD_TOWER_FOOTPRINT ];
337 0 : fd_tower_t * scratch_tower = fd_tower_join( fd_tower_new( scratch ) );
338 :
339 : /* First, simulate a vote on our tower, popping off everything that
340 : would be expired by voting for slot. */
341 :
342 0 : ulong cnt = simulate_vote( tower, slot );
343 :
344 : /* We can always vote if our tower is not at least THRESHOLD_DEPTH
345 : deep after simulating. */
346 :
347 0 : if( FD_UNLIKELY( cnt < THRESHOLD_DEPTH ) ) return 1;
348 :
349 : /* Get the vote slot from THRESHOLD_DEPTH back. Note THRESHOLD_DEPTH
350 : is the 8th index back _including_ the simulated vote at index 0. */
351 :
352 0 : ulong threshold_slot = fd_tower_peek_index_const( tower, cnt - THRESHOLD_DEPTH )->slot;
353 0 : ulong threshold_stake = 0;
354 0 : for( fd_tower_accts_iter_t iter = fd_tower_accts_iter_init( accts );
355 0 : !fd_tower_accts_iter_done( accts, iter );
356 0 : iter = fd_tower_accts_iter_next( accts, iter ) ) {
357 0 : fd_tower_accts_t const * acct = fd_tower_accts_iter_ele_const( accts, iter );
358 0 : fd_tower_remove_all( scratch_tower );
359 0 : fd_tower_from_vote_acc( scratch_tower, acct->data );
360 :
361 0 : ulong cnt = simulate_vote( scratch_tower, slot ); /* expire votes */
362 0 : if( FD_UNLIKELY( !cnt ) ) continue; /* no votes left after expiry */
363 :
364 : /* Count their stake towards the threshold check if their last vote
365 : slot >= our threshold slot.
366 :
367 : We know these votes are for our own fork because towers are
368 : sourced from vote _accounts_, not vote _transactions_.
369 :
370 : Because we are iterating vote accounts on the same fork that we
371 : we want to vote for, we know these slots must all occur along the
372 : same fork ancestry.
373 :
374 : Therefore, if their latest vote slot >= our threshold slot, we
375 : know that vote must be for the threshold slot itself or one of
376 : threshold slot's descendants. */
377 :
378 0 : ulong last_vote = fd_tower_peek_index_const( scratch_tower, cnt - 1 )->slot;
379 0 : if( FD_LIKELY( last_vote >= threshold_slot ) ) threshold_stake += acct->stake;
380 0 : }
381 :
382 0 : double threshold_pct = (double)threshold_stake / (double)total_stake;
383 0 : int threshold = threshold_pct > THRESHOLD_RATIO;
384 0 : FD_LOG_INFO(( "[%s] threshold? %d. top: %lu. threshold: %lu. pct: %.0lf%%.", __func__, threshold, fd_tower_peek_tail_const( tower )->slot, threshold_slot, threshold_pct * 100.0 ));
385 0 : return threshold;
386 0 : }
387 :
388 : int
389 : propagated_check( fd_notar_t * notar,
390 0 : ulong slot ) {
391 :
392 0 : fd_notar_slot_t * notar_slot = fd_notar_slot_query( notar->slot_map, slot, NULL );
393 0 : if( FD_UNLIKELY( !notar_slot ) ) return 1;
394 :
395 0 : if( FD_LIKELY( notar_slot->is_leader ) ) return 1; /* can always vote for slot in which we're leader */
396 0 : if( FD_LIKELY( notar_slot->prev_leader_slot==ULONG_MAX ) ) return 1; /* haven't been leader yet */
397 :
398 0 : fd_notar_slot_t * prev_leader_notar_slot = fd_notar_slot_query( notar->slot_map, notar_slot->prev_leader_slot, NULL );
399 0 : if( FD_LIKELY( !prev_leader_notar_slot ) ) return 1; /* already pruned rooted */
400 :
401 0 : return prev_leader_notar_slot->is_propagated;
402 0 : }
403 :
404 : fd_tower_out_t
405 : fd_tower_vote_and_reset( fd_tower_t * tower,
406 : fd_tower_accts_t * accts,
407 : fd_epoch_stakes_t * epoch_stakes,
408 : fd_forks_t * forks,
409 : fd_ghost_t * ghost,
410 0 : fd_notar_t * notar ) {
411 :
412 0 : uchar flags = 0;
413 0 : fd_ghost_blk_t const * best_blk = fd_ghost_best( ghost, fd_ghost_root( ghost ) );
414 0 : fd_ghost_blk_t const * reset_blk = NULL;
415 0 : fd_ghost_blk_t const * vote_blk = NULL;
416 :
417 : /* Case 0: if we haven't voted yet then we can always vote and reset
418 : to ghost_best.
419 :
420 : https://github.com/anza-xyz/agave/blob/v2.3.7/core/src/consensus.rs#L933-L935 */
421 :
422 0 : if( FD_UNLIKELY( fd_tower_empty( tower ) ) ) {
423 0 : fd_tower_forks_t * fork = fd_forks_query( forks, best_blk->slot );
424 0 : fork->voted = 1;
425 0 : fork->voted_block_id = best_blk->id;
426 0 : return (fd_tower_out_t){
427 0 : .flags = flags,
428 0 : .reset_slot = best_blk->slot,
429 0 : .reset_block_id = best_blk->id,
430 0 : .vote_slot = best_blk->slot,
431 0 : .vote_block_id = best_blk->id,
432 0 : .root_slot = push_vote( tower, best_blk->slot )
433 0 : };
434 0 : }
435 :
436 0 : ulong prev_vote_slot = fd_tower_peek_tail_const( tower )->slot;
437 0 : fd_tower_forks_t * prev_vote_fork = fd_forks_query( forks, prev_vote_slot );
438 :
439 :
440 0 : if( FD_UNLIKELY( !prev_vote_fork->voted ) ) {
441 :
442 : /* It's possible prev_vote_fork->voted is not set even though we
443 : popped it from the top of our tower. This can happen when there
444 : are multiple nodes operating with the same vote account.
445 :
446 : In a typical setup involving a primary staked node and backup
447 : unstaked node, the two nodes' towers will usually be identical
448 : but occassionally diverge when one node observes a minority fork
449 : the other doesn't. As a result, one node might be locked out
450 : from voting for a fork that the other node is not.
451 :
452 : This becomes a problem in our primary-backup setup when the
453 : unstaked node is locked out but the staked node is not. The
454 : staked node ultimately lands the vote into the on-chain vote
455 : account, so it's possible when the unstaked node reads back their
456 : on-chain vote account to diff against their local tower, there
457 : are votes in there they themselves did not vote for due to
458 : lockout (fd_tower_reconcile).
459 :
460 : As a result, `voted_block_id` will not be set for slots in their
461 : tower, which normally would be an invariant violation because the
462 : node must have set this value when they voted for the slot (and
463 : pushed it to their tower).
464 :
465 : So here we manually set the voted_block_id to replayed_block_id
466 : if not already set. We know we must have replayed it, because to
467 : observe the on-chain tower you must have replayed all the slots
468 : in the tower. */
469 :
470 : /* FIXME this needs to be thought through more carefully for set-identity. */
471 :
472 0 : prev_vote_fork->voted = 1;
473 0 : prev_vote_fork->voted_block_id = prev_vote_fork->replayed_block_id;
474 0 : }
475 :
476 0 : fd_hash_t * prev_vote_block_id = &prev_vote_fork->voted_block_id;
477 0 : fd_ghost_blk_t * prev_vote_blk = fd_ghost_query( ghost, prev_vote_block_id );
478 :
479 : /* Case 1: if an ancestor of our prev vote (including prev vote
480 : itself) is an unconfirmed duplicate, then our prev vote was on a
481 : duplicate fork.
482 :
483 : There are two subcases to check. */
484 :
485 0 : int invalid_ancestor = !!fd_ghost_invalid_ancestor( ghost, prev_vote_blk );
486 0 : if( FD_UNLIKELY( invalid_ancestor ) ) { /* do we have an invalid ancestor? */
487 :
488 : /* Case 1a: ghost_best is an ancestor of prev vote. This means
489 : ghost_best is rolling back to an ancestor that precedes the
490 : duplicate ancestor on the same fork as our prev vote. In this
491 : case, we can't vote on our ancestor, but we do reset to that
492 : ancestor.
493 :
494 : https://github.com/anza-xyz/agave/blob/v2.3.7/core/src/consensus.rs#L1016-L1019 */
495 :
496 0 : int ancestor_rollback = prev_vote_blk != best_blk && !!fd_ghost_ancestor( ghost, prev_vote_blk, &best_blk->id );
497 0 : if( FD_LIKELY( ancestor_rollback ) ) {
498 0 : flags = fd_uchar_set_bit( flags, FD_TOWER_FLAG_ANCESTOR_ROLLBACK );
499 0 : reset_blk = best_blk;
500 0 : }
501 :
502 : /* Case 1b: ghost_best is not an ancestor, but prev_vote is a
503 : duplicate and we've confirmed its duplicate sibling. In this
504 : case, we allow switching to ghost_best without a switch proof.
505 :
506 : Example: slot 5 is a duplicate. We first receive, replay and
507 : vote for block 5, so that is our prev vote. We later receive
508 : block 5' and observe that it is duplicate confirmed. ghost_best
509 : now returns block 5' and we both vote and reset to block 5'
510 : regardless of the switch check.
511 :
512 : https://github.com/anza-xyz/agave/blob/v2.3.7/core/src/consensus.rs#L1021-L1024 */
513 :
514 0 : int sibling_confirmed = 0!=memcmp( &prev_vote_fork->voted_block_id, &prev_vote_fork->confirmed_block_id, sizeof(fd_hash_t) );
515 0 : if( FD_LIKELY( sibling_confirmed ) ) {
516 0 : flags = fd_uchar_set_bit( flags, FD_TOWER_FLAG_SIBLING_CONFIRMED );
517 0 : reset_blk = best_blk;
518 0 : vote_blk = best_blk;
519 0 : }
520 :
521 : /* At this point our prev vote was on a duplicate fork but didn't
522 : match either of the above subcases.
523 :
524 : In this case, we have to pass the switch check to reset to a
525 : different fork from prev vote (same as non-duplicate case). */
526 0 : }
527 :
528 : /* Case 2: if our prev vote slot is an ancestor of the best slot, then
529 : they are on the same fork and we can both reset to it. We can also
530 : vote for it if we pass the can_vote checks.
531 :
532 : https://github.com/anza-xyz/agave/blob/v2.3.7/core/src/consensus.rs#L1057 */
533 :
534 0 : else if( FD_LIKELY( best_blk->slot == prev_vote_slot || fd_forks_is_slot_ancestor( forks, best_blk->slot, prev_vote_slot ) ) ) {
535 0 : flags = fd_uchar_set_bit( flags, FD_TOWER_FLAG_SAME_FORK );
536 0 : reset_blk = best_blk;
537 0 : vote_blk = best_blk;
538 0 : }
539 :
540 : /* Case 3: if our prev vote is not an ancestor of the best block, then
541 : it is on a different fork. If we pass the switch check, we can
542 : reset to it. If we additionally pass the lockout check, we can
543 : also vote for it.
544 :
545 : https://github.com/anza-xyz/agave/blob/v2.3.7/core/src/consensus.rs#L1208-L1215
546 :
547 : Note also Agave uses the best blk's total stake for checking the
548 : threshold.
549 :
550 : https://github.com/anza-xyz/agave/blob/v2.3.7/core/src/consensus/fork_choice.rs#L443-L445 */
551 :
552 0 : else if( FD_LIKELY( switch_check( tower, forks, epoch_stakes, best_blk->total_stake, best_blk->slot ) ) ) {
553 0 : flags = fd_uchar_set_bit( flags, FD_TOWER_FLAG_SWITCH_PASS );
554 0 : reset_blk = best_blk;
555 0 : vote_blk = best_blk;
556 0 : }
557 :
558 : /* Case 4: same as case 3 but we didn't pass the switch check. In
559 : this case we reset to either ghost_best or ghost_deepest beginning
560 : from our prev vote blk.
561 :
562 : We must reset to a block beginning from our prev vote fork to
563 : ensure votes get a chance to propagate. Because in order for votes
564 : to land, someone needs to build a block on that fork.
565 :
566 : We reset to ghost_best or ghost_deepest depending on whether our
567 : prev vote is valid. When it's invalid we use ghost_deepest instead
568 : of ghost_best, because ghost_best won't be able to return a valid
569 : block beginning from our prev_vote because by definition the entire
570 : subtree will be invalid.
571 :
572 : When our prev vote fork is not a duplicate, we want to propagate
573 : votes that might allow others to switch to our fork. In addition,
574 : if our prev vote fork is a duplicate, we want to propagate votes
575 : that might "duplicate confirm" that block (reach 52% of stake).
576 :
577 : See top-level documentation in fd_tower.h for more details on vote
578 : propagation. */
579 :
580 0 : else {
581 :
582 : /* Case 4a: failed switch check and last vote's fork is invalid.
583 :
584 : https://github.com/anza-xyz/agave/blob/v2.3.7/core/src/consensus/heaviest_subtree_fork_choice.rs#L1187 */
585 :
586 0 : if( FD_UNLIKELY( invalid_ancestor ) ) {
587 0 : flags = fd_uchar_set_bit( flags, FD_TOWER_FLAG_SWITCH_FAIL );
588 0 : reset_blk = fd_ghost_deepest( ghost, prev_vote_blk );
589 0 : }
590 :
591 : /* Case 4b: failed switch check and last vote's fork is valid.
592 :
593 : https://github.com/anza-xyz/agave/blob/v2.3.7/core/src/consensus/fork_choice.rs#L200 */
594 :
595 0 : else {
596 0 : flags = fd_uchar_set_bit( flags, FD_TOWER_FLAG_SWITCH_FAIL );
597 0 : reset_blk = fd_ghost_best( ghost, prev_vote_blk );
598 0 : }
599 0 : }
600 :
601 : /* If there is a block to vote for, there are a few additional checks
602 : to make sure we can actually vote for it.
603 :
604 : Specifically, we need to make sure we're not locked out, pass the
605 : threshold check and that our previous leader block has propagated
606 : (reached the prop threshold according to fd_notar).
607 :
608 : https://github.com/firedancer-io/agave/blob/master/core/src/consensus/fork_choice.rs#L382-L385
609 :
610 : Agave uses the total stake on the fork being threshold checked
611 : (vote_blk) for determining whether it meets the stake threshold. */
612 :
613 0 : if( FD_LIKELY( vote_blk ) ) {
614 0 : if ( FD_UNLIKELY( !lockout_check( tower, forks, vote_blk->slot ) ) ) {
615 0 : flags = fd_uchar_set_bit( flags, FD_TOWER_FLAG_LOCKOUT_FAIL );
616 0 : vote_blk = NULL;
617 0 : }
618 0 : else if( FD_UNLIKELY( !threshold_check( tower, accts, vote_blk->total_stake, vote_blk->slot ) ) ) {
619 0 : flags = fd_uchar_set_bit( flags, FD_TOWER_FLAG_THRESHOLD_FAIL );
620 0 : vote_blk = NULL;
621 0 : }
622 0 : else if( FD_UNLIKELY( !propagated_check( notar, vote_blk->slot ) ) ) {
623 0 : flags = fd_uchar_set_bit( flags, FD_TOWER_FLAG_PROPAGATED_FAIL );
624 0 : vote_blk = NULL;
625 0 : }
626 0 : }
627 :
628 0 : FD_TEST( reset_blk ); /* always a reset_blk */
629 0 : fd_tower_out_t out = {
630 0 : .flags = flags,
631 0 : .reset_slot = reset_blk->slot,
632 0 : .reset_block_id = reset_blk->id,
633 0 : .vote_slot = ULONG_MAX,
634 0 : .root_slot = ULONG_MAX
635 0 : };
636 :
637 : /* Finally, if our vote passed all the checks, we actually push the
638 : vote onto the tower. */
639 :
640 0 : if( FD_LIKELY( vote_blk ) ) {
641 0 : out.vote_slot = vote_blk->slot;
642 0 : out.vote_block_id = vote_blk->id;
643 0 : out.root_slot = push_vote( tower, vote_blk->slot );
644 :
645 : /* Query our tower fork for this slot we're voting for. Note this
646 : can never be NULL because we record tower forks as we replay, and
647 : we should never be voting on something we haven't replayed. */
648 :
649 0 : fd_tower_forks_t * fork = fd_forks_query( forks, vote_blk->slot );
650 0 : fork->voted = 1;
651 0 : fork->voted_block_id = vote_blk->id;
652 :
653 : /* Query the root slot's block id from tower forks. This block id
654 : may not necessarily be confirmed, because confirmation requires
655 : votes on the block itself (vs. block and its descendants).
656 :
657 : So if we have a confirmed block id, we return that. Otherwise
658 : we return our own vote block id for that slot, which we assume
659 : is the cluster converged on by the time we're rooting it.
660 :
661 : The only way it is possible for us to root the wrong version of
662 : a block (ie. not the one the cluster confirmed) is if there is
663 : mass equivocation (>2/3 of threshold check stake has voted for
664 : two versions of a block). This exceeds the equivocation safety
665 : threshold and we would eventually detect this via a bank hash
666 : mismatch and error out. */
667 :
668 0 : if( FD_LIKELY( out.root_slot!=ULONG_MAX ) ) {
669 0 : fd_tower_forks_t * root_fork = fd_forks_query( forks, out.root_slot );
670 0 : out.root_block_id = *fd_ptr_if( root_fork->confirmed, &root_fork->confirmed_block_id, &root_fork->voted_block_id );
671 0 : }
672 0 : }
673 :
674 0 : FD_BASE58_ENCODE_32_BYTES( out.reset_block_id.uc, reset_block_id );
675 0 : FD_BASE58_ENCODE_32_BYTES( out.vote_block_id.uc, vote_block_id );
676 0 : FD_BASE58_ENCODE_32_BYTES( out.root_block_id.uc, root_block_id );
677 0 : FD_LOG_INFO(( "[%s] flags: %d. reset_slot: %lu (%s). vote_slot: %lu (%s). root_slot: %lu (%s).", __func__, out.flags, out.reset_slot, reset_block_id, out.vote_slot, vote_block_id, out.root_slot, root_block_id ));
678 0 : return out;
679 0 : }
680 :
681 : void
682 : fd_tower_reconcile( fd_tower_t * tower,
683 : ulong root,
684 0 : uchar const * vote_account_data ) {
685 0 : ulong on_chain_vote = fd_voter_vote_slot( vote_account_data );
686 0 : ulong on_chain_root = fd_voter_root_slot( vote_account_data );
687 :
688 0 : fd_tower_vote_t const * last_vote = fd_tower_peek_tail_const( tower );
689 0 : ulong last_vote_slot = last_vote ? last_vote->slot : ULONG_MAX;
690 :
691 0 : if( FD_UNLIKELY( ( on_chain_vote==ULONG_MAX && last_vote_slot==ULONG_MAX ) ) ) return;
692 0 : if( FD_LIKELY ( ( on_chain_vote!=ULONG_MAX && last_vote_slot!=ULONG_MAX
693 0 : && on_chain_vote <= last_vote_slot ) ) ) return;
694 :
695 : /* At this point our local tower is too old, and we need to replace it
696 : with our on-chain tower. However, it's possible our local root is
697 : newer than the on-chain root (even though the tower is older). The
698 : most likely reason this happens is because we just booted from a
699 : snapshot and the snapshot slot > on-chain root.
700 :
701 : So we need to filter out the stale votes < snapshot slot. This
702 : mirrors the Agave logic:
703 : https://github.com/firedancer-io/agave/blob/master/core/src/replay_stage.rs#L3690-L3719 */
704 :
705 0 : if( FD_LIKELY( on_chain_root == ULONG_MAX || root > on_chain_root ) ) {
706 0 : fd_tower_remove_all( tower );
707 0 : fd_voter_t const * voter = (fd_voter_t const *)fd_type_pun_const( vote_account_data );
708 0 : uint kind = fd_uint_load_4_fast( vote_account_data ); /* skip node_pubkey */
709 0 : for( ulong i=0; i<fd_voter_votes_cnt( vote_account_data ); i++ ) {
710 0 : switch( kind ) {
711 0 : case FD_VOTER_V4: fd_tower_push_tail( tower, (fd_tower_vote_t){ .slot = v4_off( voter )[i].slot, .conf = v4_off( voter )[i].conf } ); break;
712 0 : case FD_VOTER_V3: fd_tower_push_tail( tower, (fd_tower_vote_t){ .slot = voter->v3.votes[i].slot, .conf = voter->v3.votes[i].conf } ); break;
713 0 : case FD_VOTER_V2: fd_tower_push_tail( tower, (fd_tower_vote_t){ .slot = voter->v2.votes[i].slot, .conf = voter->v2.votes[i].conf } ); break;
714 0 : default: FD_LOG_ERR(( "unsupported voter account version: %u", kind ));
715 0 : }
716 0 : }
717 :
718 : /* Fast forward our tower to tower_root by retaining only votes >
719 : local tower root. */
720 :
721 0 : while( FD_LIKELY( !fd_tower_empty( tower ) ) ) {
722 0 : fd_tower_t const * vote = fd_tower_peek_head_const( tower );
723 0 : if( FD_LIKELY( vote->slot > root ) ) break;
724 0 : fd_tower_pop_head( tower );
725 0 : }
726 0 : }
727 0 : }
728 :
729 : void
730 : fd_tower_from_vote_acc( fd_tower_t * tower,
731 132 : uchar const * vote_acc ) {
732 132 : fd_voter_t const * voter = (fd_voter_t const *)fd_type_pun_const( vote_acc );
733 132 : uint kind = fd_uint_load_4_fast( vote_acc ); /* skip node_pubkey */
734 264 : for( ulong i=0; i<fd_voter_votes_cnt( vote_acc ); i++ ) {
735 132 : switch( kind ) {
736 0 : case FD_VOTER_V4: fd_tower_push_tail( tower, (fd_tower_vote_t){ .slot = v4_off( voter )[i].slot, .conf = v4_off( voter )[i].conf } ); break;
737 132 : case FD_VOTER_V3: fd_tower_push_tail( tower, (fd_tower_vote_t){ .slot = voter->v3.votes[i].slot, .conf = voter->v3.votes[i].conf } ); break;
738 0 : case FD_VOTER_V2: fd_tower_push_tail( tower, (fd_tower_vote_t){ .slot = voter->v2.votes[i].slot, .conf = voter->v2.votes[i].conf } ); break;
739 0 : default: FD_LOG_ERR(( "unsupported voter account version: %u", kind ));
740 132 : }
741 132 : }
742 132 : }
743 :
744 : ulong
745 : fd_tower_with_lat_from_vote_acc( fd_voter_vote_t tower[ static FD_TOWER_VOTE_MAX ],
746 0 : uchar const * vote_acc ) {
747 0 : fd_voter_t const * voter = (fd_voter_t const *)fd_type_pun_const( vote_acc );
748 0 : uint kind = fd_uint_load_4_fast( vote_acc ); /* skip node_pubkey */
749 0 : for( ulong i=0; i<fd_voter_votes_cnt( vote_acc ); i++ ) {
750 0 : switch( kind ) {
751 0 : case FD_VOTER_V4: tower[ i ] = (fd_voter_vote_t){ .latency = v4_off( voter )[i].latency, .slot = v4_off( voter )[i].slot, .conf = v4_off( voter )[i].conf }; break;
752 0 : case FD_VOTER_V3: tower[ i ] = (fd_voter_vote_t){ .latency = voter->v3.votes[i].latency, .slot = voter->v3.votes[i].slot, .conf = voter->v3.votes[i].conf }; break;
753 0 : case FD_VOTER_V2: tower[ i ] = (fd_voter_vote_t){ .latency = UCHAR_MAX, .slot = voter->v2.votes[i].slot, .conf = voter->v2.votes[i].conf }; break;
754 0 : default: FD_LOG_ERR(( "unsupported voter account version: %u", kind ));
755 0 : }
756 0 : }
757 :
758 0 : return fd_voter_votes_cnt( vote_acc );
759 0 : }
760 :
761 : void
762 : fd_tower_to_vote_txn( fd_tower_t const * tower,
763 : ulong root,
764 : fd_hash_t const * bank_hash,
765 : fd_hash_t const * block_id,
766 : fd_hash_t const * recent_blockhash,
767 : fd_pubkey_t const * validator_identity,
768 : fd_pubkey_t const * vote_authority,
769 : fd_pubkey_t const * vote_acc,
770 3 : fd_txn_p_t * vote_txn ) {
771 :
772 3 : FD_TEST( fd_tower_cnt( tower )<=FD_TOWER_VOTE_MAX );
773 3 : fd_compact_tower_sync_serde_t tower_sync_serde = {
774 3 : .root = fd_ulong_if( root == ULONG_MAX, 0UL, root ),
775 3 : .lockouts_cnt = (ushort)fd_tower_cnt( tower ),
776 : /* .lockouts populated below */
777 3 : .hash = *bank_hash,
778 3 : .timestamp_option = 1,
779 3 : .timestamp = fd_log_wallclock() / (long)1e9, /* seconds */
780 3 : .block_id = *block_id
781 3 : };
782 :
783 3 : ulong i = 0UL;
784 3 : ulong prev = tower_sync_serde.root;
785 3 : for( fd_tower_iter_t iter = fd_tower_iter_init( tower );
786 96 : !fd_tower_iter_done( tower, iter );
787 93 : iter = fd_tower_iter_next( tower, iter ) ) {
788 93 : fd_tower_t const * vote = fd_tower_iter_ele_const( tower, iter );
789 93 : tower_sync_serde.lockouts[i].offset = vote->slot - prev;
790 93 : tower_sync_serde.lockouts[i].confirmation_count = (uchar)vote->conf;
791 93 : prev = vote->slot;
792 93 : i++;
793 93 : }
794 :
795 3 : uchar * txn_out = vote_txn->payload;
796 3 : uchar * txn_meta_out = vote_txn->_;
797 :
798 3 : int same_addr = !memcmp( validator_identity, vote_authority, sizeof(fd_pubkey_t) );
799 3 : if( FD_LIKELY( same_addr ) ) {
800 :
801 : /* 0: validator identity
802 : 1: vote account address
803 : 2: vote program */
804 :
805 3 : fd_txn_accounts_t votes;
806 3 : votes.signature_cnt = 1;
807 3 : votes.readonly_signed_cnt = 0;
808 3 : votes.readonly_unsigned_cnt = 1;
809 3 : votes.acct_cnt = 3;
810 3 : votes.signers_w = validator_identity;
811 3 : votes.signers_r = NULL;
812 3 : votes.non_signers_w = vote_acc;
813 3 : votes.non_signers_r = &fd_solana_vote_program_id;
814 3 : FD_TEST( fd_txn_base_generate( txn_meta_out, txn_out, votes.signature_cnt, &votes, recent_blockhash->uc ) );
815 :
816 3 : } else {
817 :
818 : /* 0: validator identity
819 : 1: vote authority
820 : 2: vote account address
821 : 3: vote program */
822 :
823 0 : fd_txn_accounts_t votes;
824 0 : votes.signature_cnt = 2;
825 0 : votes.readonly_signed_cnt = 1;
826 0 : votes.readonly_unsigned_cnt = 1;
827 0 : votes.acct_cnt = 4;
828 0 : votes.signers_w = validator_identity;
829 0 : votes.signers_r = vote_authority;
830 0 : votes.non_signers_w = vote_acc;
831 0 : votes.non_signers_r = &fd_solana_vote_program_id;
832 0 : FD_TEST( fd_txn_base_generate( txn_meta_out, txn_out, votes.signature_cnt, &votes, recent_blockhash->uc ) );
833 0 : }
834 :
835 : /* Add the vote instruction to the transaction. */
836 :
837 3 : uchar vote_ix_buf[FD_TXN_MTU];
838 3 : ulong vote_ix_sz = 0;
839 3 : FD_STORE( uint, vote_ix_buf, FD_VOTE_IX_KIND_TOWER_SYNC );
840 3 : FD_TEST( 0==fd_compact_tower_sync_serialize( &tower_sync_serde, vote_ix_buf + sizeof(uint), FD_TXN_MTU - sizeof(uint), &vote_ix_sz ) ); // cannot fail if fd_tower_cnt( tower ) <= FD_TOWER_VOTE_MAX
841 3 : vote_ix_sz += sizeof(uint);
842 3 : uchar program_id;
843 3 : uchar ix_accs[2];
844 3 : if( FD_LIKELY( same_addr ) ) {
845 3 : ix_accs[0] = 1; /* vote account address */
846 3 : ix_accs[1] = 0; /* vote authority */
847 3 : program_id = 2; /* vote program */
848 3 : } else {
849 0 : ix_accs[0] = 2; /* vote account address */
850 0 : ix_accs[1] = 1; /* vote authority */
851 0 : program_id = 3; /* vote program */
852 0 : }
853 3 : vote_txn->payload_sz = fd_txn_add_instr( txn_meta_out, txn_out, program_id, ix_accs, 2, vote_ix_buf, vote_ix_sz );
854 3 : }
855 :
856 : int
857 0 : fd_tower_verify( fd_tower_t const * tower ) {
858 0 : if( FD_UNLIKELY( fd_tower_cnt( tower )>=FD_TOWER_VOTE_MAX ) ) {
859 0 : FD_LOG_WARNING(( "[%s] invariant violation: cnt %lu >= FD_TOWER_VOTE_MAX %lu", __func__, fd_tower_cnt( tower ), (ulong)FD_TOWER_VOTE_MAX ));
860 0 : return -1;
861 0 : }
862 :
863 0 : fd_tower_t const * prev = NULL;
864 0 : for( fd_tower_iter_t iter = fd_tower_iter_init( tower );
865 0 : !fd_tower_iter_done( tower, iter );
866 0 : iter = fd_tower_iter_next( tower, iter ) ) {
867 0 : fd_tower_t const * vote = fd_tower_iter_ele_const( tower, iter );
868 0 : if( FD_UNLIKELY( prev && ( vote->slot < prev->slot || vote->conf < prev->conf ) ) ) {
869 0 : FD_LOG_WARNING(( "[%s] invariant violation: vote (slot:%lu conf:%lu) prev (slot:%lu conf:%lu)", __func__, vote->slot, vote->conf, prev->slot, prev->conf ));
870 0 : return -1;
871 0 : }
872 0 : prev = vote;
873 0 : }
874 0 : return 0;
875 0 : }
876 :
877 : #include <stdio.h>
878 : #include <string.h>
879 :
880 : static void
881 0 : print( fd_tower_t const * tower, ulong root, char * s, ulong len ) {
882 0 : ulong off = 0;
883 0 : int n;
884 :
885 0 : n = snprintf( s + off, len - off, "[Tower]\n\n" );
886 0 : if( FD_UNLIKELY( n < 0 )) FD_LOG_CRIT(( "snprintf: %d", n ));
887 0 : off += (ulong)n;
888 :
889 0 : if( FD_UNLIKELY( fd_tower_empty( tower ) ) ) return;
890 :
891 0 : ulong max_slot = 0;
892 :
893 : /* Determine spacing. */
894 :
895 0 : for( fd_tower_iter_t iter = fd_tower_iter_init_rev( tower );
896 0 : !fd_tower_iter_done_rev( tower, iter );
897 0 : iter = fd_tower_iter_prev ( tower, iter ) ) {
898 0 : max_slot = fd_ulong_max( max_slot, fd_tower_iter_ele_const( tower, iter )->slot );
899 0 : }
900 :
901 : /* Calculate the number of digits in the maximum slot value. */
902 :
903 :
904 0 : int digit_cnt = (int)fd_ulong_base10_dig_cnt( max_slot );
905 :
906 : /* Print the column headers. */
907 :
908 0 : if( off < len ) {
909 0 : n = snprintf( s + off, len - off, "slot%*s | %s\n", digit_cnt - (int)strlen("slot"), "", "confirmation count" );
910 0 : if( FD_UNLIKELY( n < 0 )) FD_LOG_CRIT(( "snprintf: %d", n ));
911 0 : off += (ulong)n;
912 0 : }
913 :
914 : /* Print the divider line. */
915 :
916 0 : for( int i = 0; i < digit_cnt && off < len; i++ ) {
917 0 : s[off++] = '-';
918 0 : }
919 0 : if( off < len ) {
920 0 : n = snprintf( s + off, len - off, " | " );
921 0 : if( FD_UNLIKELY( n < 0 )) FD_LOG_CRIT(( "snprintf: %d", n ));
922 0 : off += (ulong)n;
923 0 : }
924 0 : for( ulong i = 0; i < strlen( "confirmation count" ) && off < len; i++ ) {
925 0 : s[off++] = '-';
926 0 : }
927 0 : if( off < len ) {
928 0 : s[off++] = '\n';
929 0 : }
930 :
931 : /* Print each vote as a table. */
932 :
933 0 : for( fd_tower_iter_t iter = fd_tower_iter_init_rev( tower );
934 0 : !fd_tower_iter_done_rev( tower, iter );
935 0 : iter = fd_tower_iter_prev ( tower, iter ) ) {
936 0 : fd_tower_t const * vote = fd_tower_iter_ele_const( tower, iter );
937 0 : if( off < len ) {
938 0 : n = snprintf( s + off, len - off, "%*lu | %lu\n", digit_cnt, vote->slot, vote->conf );
939 0 : if( FD_UNLIKELY( n < 0 )) FD_LOG_CRIT(( "snprintf: %d", n ));
940 0 : off += (ulong)n;
941 0 : }
942 0 : }
943 :
944 0 : if( FD_UNLIKELY( root == ULONG_MAX ) ) {
945 0 : if( off < len ) {
946 0 : n = snprintf( s + off, len - off, "%*s | root\n", digit_cnt, "NULL" );
947 0 : if( FD_UNLIKELY( n < 0 )) FD_LOG_CRIT(( "snprintf: %d", n ));
948 0 : off += (ulong)n;
949 0 : }
950 0 : } else {
951 0 : if( off < len ) {
952 0 : n = snprintf( s + off, len - off, "%*lu | root\n", digit_cnt, root );
953 0 : if( FD_UNLIKELY( n < 0 )) FD_LOG_CRIT(( "snprintf: %d", n ));
954 0 : off += (ulong)n;
955 0 : }
956 0 : }
957 :
958 : /* Ensure null termination */
959 0 : if( off < len ) {
960 0 : s[off] = '\0';
961 0 : } else {
962 0 : s[len - 1] = '\0';
963 0 : }
964 0 : }
965 :
966 : void
967 0 : fd_tower_print( fd_tower_t const * tower, ulong root ) {
968 0 : char buf[4096];
969 0 : print( tower, root, buf, sizeof(buf) );
970 0 : FD_LOG_NOTICE(( "\n\n%s", buf ));
971 0 : }
|