Skip to content

runtime: rewrite blockhash queue #5728

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 9 additions & 10 deletions src/discof/replay/fd_replay_tile.c
Original file line number Diff line number Diff line change
Expand Up @@ -602,8 +602,9 @@ publish_slot_notifications( fd_replay_tile_ctx_t * ctx,

msg->slot_exec.bank_hash = fd_bank_bank_hash_get( ctx->slot_ctx->bank );

fd_block_hash_queue_global_t const * block_hash_queue = fd_bank_block_hash_queue_query( ctx->slot_ctx->bank );
fd_hash_t * last_hash = fd_block_hash_queue_last_hash_join( block_hash_queue );
fd_blockhashes_t const * block_hash_queue = fd_bank_block_hash_queue_query( ctx->slot_ctx->bank );
fd_hash_t const * last_hash = fd_blockhashes_peek_last( block_hash_queue );
FD_TEST( last_hash );
msg->slot_exec.block_hash = *last_hash;

memcpy( &msg->slot_exec.identity, ctx->validator_identity_pubkey, sizeof( fd_pubkey_t ) );
Expand Down Expand Up @@ -923,12 +924,12 @@ init_poh( fd_replay_tile_ctx_t * ctx ) {
msg->ticks_per_slot = fd_bank_ticks_per_slot_get( ctx->slot_ctx->bank );
msg->tick_duration_ns = (ulong)(fd_bank_ns_per_slot_get( ctx->slot_ctx->bank )) / fd_bank_ticks_per_slot_get( ctx->slot_ctx->bank );

fd_block_hash_queue_global_t * bhq = (fd_block_hash_queue_global_t *)&ctx->slot_ctx->bank->block_hash_queue[0];
fd_hash_t * last_hash = fd_block_hash_queue_last_hash_join( bhq );
fd_blockhashes_t const * bhq = fd_bank_block_hash_queue_query( ctx->slot_ctx->bank );
fd_hash_t const * last_hash = fd_blockhashes_peek_last( bhq );
if( last_hash ) {
memcpy(msg->last_entry_hash, last_hash, sizeof(fd_hash_t));
memcpy( msg->last_entry_hash, last_hash, sizeof(fd_hash_t) );
} else {
memset(msg->last_entry_hash, 0UL, sizeof(fd_hash_t));
memset( msg->last_entry_hash, 0UL, sizeof(fd_hash_t) );
}
msg->tick_height = fd_bank_slot_get( ctx->slot_ctx->bank ) * msg->ticks_per_slot;

Expand Down Expand Up @@ -1426,11 +1427,9 @@ exec_slice_fini_slot( fd_replay_tile_ctx_t * ctx, fd_stem_context_t * stem ) {
if( FD_LIKELY( ctx->tower_out_idx!=ULONG_MAX && !ctx->read_only ) ) {
uchar * chunk_laddr = fd_chunk_to_laddr( ctx->tower_out_mem, ctx->tower_out_chunk );
fd_hash_t const * bank_hash = fd_bank_bank_hash_query( ctx->slot_ctx->bank );
fd_block_hash_queue_global_t * block_hash_queue = (fd_block_hash_queue_global_t *)&ctx->slot_ctx->bank->block_hash_queue[0];
fd_hash_t * last_hash = fd_block_hash_queue_last_hash_join( block_hash_queue );

fd_blockhashes_t const * blockhashes = fd_bank_block_hash_queue_query( ctx->slot_ctx->bank );
memcpy( chunk_laddr, bank_hash, sizeof(fd_hash_t) );
memcpy( chunk_laddr+sizeof(fd_hash_t), last_hash, sizeof(fd_hash_t) );
memcpy( chunk_laddr+sizeof(fd_hash_t), fd_blockhashes_peek_last( blockhashes ), sizeof(fd_hash_t) );
fd_stem_publish( stem, ctx->tower_out_idx, fd_bank_slot_get( ctx->slot_ctx->bank ) << 32UL | fd_bank_parent_slot_get( ctx->slot_ctx->bank ), ctx->tower_out_chunk, sizeof(fd_hash_t) * 2, 0UL, fd_frag_meta_ts_comp( fd_tickcount() ), fd_frag_meta_ts_comp( fd_tickcount() ) );
}

Expand Down
5 changes: 5 additions & 0 deletions src/flamenco/runtime/Local.mk
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ $(call add-objs,fd_txn_account,fd_flamenco)
$(call add-hdrs,fd_bank_hash_cmp.h fd_rwseq_lock.h)
$(call add-objs,fd_bank_hash_cmp,fd_flamenco)

$(call add-hdrs,fd_blockhashes.h)
$(call add-objs,fd_blockhashes,fd_flamenco)
$(call make-unit-test,test_blockhashes,test_blockhashes,fd_flamenco fd_ballet fd_util)
$(call run-unit-test,test_blockhashes)

$(call add-hdrs,fd_blockstore.h fd_rwseq_lock.h)
$(call add-objs,fd_blockstore,fd_flamenco)
$(call make-unit-test,test_blockstore,test_blockstore, fd_flamenco fd_util fd_ballet,$(SECP256K1_LIBS))
Expand Down
45 changes: 12 additions & 33 deletions src/flamenco/runtime/context/fd_exec_slot_ctx.c
Original file line number Diff line number Diff line change
Expand Up @@ -187,38 +187,15 @@ fd_exec_slot_ctx_recover( fd_exec_slot_ctx_t * slot_ctx,
/* Index vote accounts */

/* Block Hash Queue */

fd_block_hash_queue_global_t * bhq = (fd_block_hash_queue_global_t *)&slot_ctx->bank->block_hash_queue[0];
uchar * last_hash_mem = (uchar *)fd_ulong_align_up( (ulong)bhq + sizeof(fd_block_hash_queue_global_t), alignof(fd_hash_t) );
uchar * ages_pool_mem = (uchar *)fd_ulong_align_up( (ulong)last_hash_mem + sizeof(fd_hash_t), fd_hash_hash_age_pair_t_map_align() );

fd_hash_hash_age_pair_t_mapnode_t * ages_pool = fd_hash_hash_age_pair_t_map_join( fd_hash_hash_age_pair_t_map_new( ages_pool_mem, 301 ) );
fd_hash_hash_age_pair_t_mapnode_t * ages_root = NULL;

bhq->last_hash_index = old_bank->blockhash_queue.last_hash_index;

fd_hash_t const * last_hash = fd_block_hash_vec_last_hash_join( &old_bank->blockhash_queue );

if( last_hash ) {
fd_memcpy( last_hash_mem, last_hash, sizeof(fd_hash_t) );
} else {
fd_memset( last_hash_mem, 0, sizeof(fd_hash_t) );
{
fd_blockhashes_t * bhq = fd_bank_block_hash_queue_modify( slot_ctx->bank );
ulong seed; FD_TEST( fd_rng_secure( &seed, sizeof(ulong) ) );
FD_TEST( fd_blockhashes_recover(
bhq,
fd_block_hash_vec_ages_join( &old_bank->blockhash_queue ),
old_bank->blockhash_queue.ages_len,
seed ) );
}
bhq->last_hash_offset = (ulong)last_hash_mem - (ulong)bhq;

fd_hash_hash_age_pair_t const * ages = fd_block_hash_vec_ages_join( &old_bank->blockhash_queue );

for( ulong i=0UL; i<old_bank->blockhash_queue.ages_len; i++ ) {
fd_hash_hash_age_pair_t const * elem = &ages[i];
fd_hash_hash_age_pair_t_mapnode_t * node = fd_hash_hash_age_pair_t_map_acquire( ages_pool );
node->elem = *elem;
fd_hash_hash_age_pair_t_map_insert( ages_pool, &ages_root, node );
}

fd_block_hash_queue_ages_pool_update( bhq, ages_pool );
fd_block_hash_queue_ages_root_update( bhq, ages_root );

bhq->max_age = old_bank->blockhash_queue.max_age;

/* Bank Hash */

Expand Down Expand Up @@ -309,8 +286,10 @@ fd_exec_slot_ctx_recover( fd_exec_slot_ctx_t * slot_ctx,

/* PoH */

if( last_hash ) {
fd_bank_poh_set( slot_ctx->bank, *last_hash );
{
fd_blockhashes_t const * bhq = fd_bank_block_hash_queue_query( slot_ctx->bank );
fd_hash_t const * last_hash = fd_blockhashes_peek_last( bhq );
if( last_hash ) fd_bank_poh_set( slot_ctx->bank, *last_hash );
}

/* Prev Bank Hash */
Expand Down
3 changes: 2 additions & 1 deletion src/flamenco/runtime/fd_bank.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "../leaders/fd_leaders.h"
#include "../features/fd_features.h"
#include "../fd_rwlock.h"
#include "fd_blockhashes.h"

FD_PROTOTYPES_BEGIN

Expand Down Expand Up @@ -132,7 +133,7 @@ FD_PROTOTYPES_BEGIN
X(fd_clock_timestamp_votes_global_t, clock_timestamp_votes, 5000000UL, 128UL, 1, 1 ) /* TODO: This needs to get sized out */ \
X(fd_account_keys_global_t, stake_account_keys, 100000000UL, 128UL, 1, 1 ) /* Supports roughly 3M stake accounts */ \
X(fd_account_keys_global_t, vote_account_keys, 3200000UL, 128UL, 1, 1 ) /* Supports roughly 100k vote accounts */ \
X(fd_block_hash_queue_global_t, block_hash_queue, 50000UL, 128UL, 0, 0 ) /* Block hash queue */ \
X(fd_blockhashes_t, block_hash_queue, sizeof(fd_blockhashes_t), alignof(fd_blockhashes_t), 0, 0 ) /* Block hash queue */ \
X(fd_fee_rate_governor_t, fee_rate_governor, sizeof(fd_fee_rate_governor_t), alignof(fd_fee_rate_governor_t), 0, 0 ) /* Fee rate governor */ \
X(ulong, capitalization, sizeof(ulong), alignof(ulong), 0, 0 ) /* Capitalization */ \
X(ulong, lamports_per_signature, sizeof(ulong), alignof(ulong), 0, 0 ) /* Lamports per signature */ \
Expand Down
139 changes: 139 additions & 0 deletions src/flamenco/runtime/fd_blockhashes.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#include "fd_blockhashes.h"

fd_blockhashes_t *
fd_blockhashes_init( fd_blockhashes_t * mem,
ulong seed ) {
if( FD_UNLIKELY( !mem ) ) {
FD_LOG_WARNING(( "NULL mem" ));
return NULL;
}
FD_TEST( fd_blockhash_deq_join( fd_blockhash_deq_new( &mem->d ) ) );
memset( mem->d.deque, 0x5a, sizeof(fd_blockhash_info_t) * FD_BLOCKHASHES_MAX );
FD_TEST( fd_blockhash_map_join( fd_blockhash_map_new( mem, FD_BLOCKHASH_MAP_CHAIN_MAX, seed ) ) );
return mem;
}

fd_blockhashes_t *
fd_blockhashes_recover( fd_blockhashes_t * blockhashes,
fd_hash_hash_age_pair_t const * ages,
ulong age_cnt,
ulong seed ) {
FD_TEST( fd_blockhashes_init( blockhashes, seed ) );
if( FD_UNLIKELY( !age_cnt || age_cnt>FD_BLOCKHASHES_MAX ) ) {
FD_LOG_WARNING(( "Corrupt snapshot: blockhash queue age count %lu is out of range [1,%d)", age_cnt, FD_BLOCKHASHES_MAX ));
}

/* For depressing reasons, the ages array is not sorted when ingested
from a snapshot. The hash_index field is also not validated.
Firedancer assumes that the sequence of hash_index numbers is
gapless and does not wrap around. */

ulong seq_min = ULONG_MAX-1;
for( ulong i=0UL; i<age_cnt; i++ ) {
seq_min = fd_ulong_min( seq_min, ages[i].val.hash_index );
}
ulong seq_max;
if( FD_UNLIKELY( __builtin_uaddl_overflow( seq_min, age_cnt, &seq_max ) ) ) {
FD_LOG_WARNING(( "Corrupt snapshot: blockhash queue sequence number wraparound (seq_min=%lu age_cnt=%lu)", seq_min, age_cnt ));
return NULL;
}

/* Reset */

for( ulong i=0UL; i<age_cnt; i++ ) {
fd_blockhash_info_t * ele = fd_blockhash_deq_push_tail_nocopy( blockhashes->d.deque );
memset( ele, 0, sizeof(fd_blockhash_info_t) );
}

/* Load hashes */

for( ulong i=0UL; i<age_cnt; i++ ) {
fd_hash_hash_age_pair_t const * elem = &ages[i];
ulong idx;
if( FD_UNLIKELY( __builtin_usubl_overflow( elem->val.hash_index, seq_min, &idx ) ) ) {
FD_LOG_WARNING(( "Corrupt snapshot: gap in blockhash queue (seq=[%lu,%lu) idx=%lu)",
seq_min, seq_max, elem->val.hash_index ));
return NULL;
}
fd_blockhash_info_t * info = &blockhashes->d.deque[ idx ];
if( FD_UNLIKELY( info->exists ) ) {
FD_LOG_HEXDUMP_NOTICE(( "info", info, sizeof(fd_blockhash_info_t) ));
FD_LOG_WARNING(( "Corrupt snapshot: duplicate blockhash queue index %lu", idx ));
return NULL;
}
info->exists = 1;
info->hash = elem->key;
info->fee_calculator = elem->val.fee_calculator;
fd_blockhash_map_idx_insert( blockhashes->map, idx, blockhashes->d.deque );
}

return blockhashes;
}

static void
fd_blockhashes_pop_old( fd_blockhashes_t * blockhashes ) {
if( FD_UNLIKELY( fd_blockhash_deq_empty( blockhashes->d.deque ) ) ) return;
fd_blockhash_info_t * info = fd_blockhash_deq_pop_head_nocopy( blockhashes->d.deque );
info->exists = 0;
fd_blockhash_map_ele_remove( blockhashes->map, &info->hash, NULL, blockhashes->d.deque );
}

void
fd_blockhashes_pop_new( fd_blockhashes_t * blockhashes ) {
if( FD_UNLIKELY( fd_blockhash_deq_empty( blockhashes->d.deque ) ) ) return;
fd_blockhash_info_t * info = fd_blockhash_deq_pop_tail_nocopy( blockhashes->d.deque );
info->exists = 0;
fd_blockhash_map_ele_remove( blockhashes->map, &info->hash, NULL, blockhashes->d.deque );
}

fd_blockhash_info_t *
fd_blockhashes_push_new( fd_blockhashes_t * blockhashes,
fd_hash_t const * hash ) {
if( FD_UNLIKELY( fd_blockhash_deq_full( blockhashes->d.deque ) ) ) {
fd_blockhashes_pop_old( blockhashes );
}
if( FD_UNLIKELY( fd_blockhash_map_idx_query( blockhashes->map, hash, ULONG_MAX, blockhashes->d.deque )!=ULONG_MAX ) ) {
char bh_cstr[ FD_BASE58_ENCODED_32_SZ ]; fd_base58_encode_32( hash->uc, NULL, bh_cstr );
FD_LOG_CRIT(( "Attempted to register duplicate blockhash %s", bh_cstr ));
}

fd_blockhash_info_t * info = fd_blockhash_deq_push_tail_nocopy( blockhashes->d.deque );
*info = (fd_blockhash_info_t) { .hash = *hash, .exists = 1 };

fd_blockhash_map_ele_insert( blockhashes->map, info, blockhashes->d.deque );

return info;
}

fd_blockhash_info_t *
fd_blockhashes_push_old( fd_blockhashes_t * blockhashes,
fd_hash_t const * hash ) {
if( FD_UNLIKELY( fd_blockhash_deq_full( blockhashes->d.deque ) ) ) {
return NULL;
}
if( FD_UNLIKELY( fd_blockhash_map_idx_query( blockhashes->map, hash, ULONG_MAX, blockhashes->d.deque )!=ULONG_MAX ) ) {
char bh_cstr[ FD_BASE58_ENCODED_32_SZ ]; fd_base58_encode_32( hash->uc, NULL, bh_cstr );
FD_LOG_CRIT(( "Attempted to register duplicate blockhash %s", bh_cstr ));
}

fd_blockhash_info_t * info = fd_blockhash_deq_push_head_nocopy( blockhashes->d.deque );
*info = (fd_blockhash_info_t) { .hash = *hash, .exists = 1 };

fd_blockhash_map_ele_insert( blockhashes->map, info, blockhashes->d.deque );

return info;
}


FD_FN_PURE int
fd_blockhashes_check_age( fd_blockhashes_t const * blockhashes,
fd_hash_t const * blockhash,
ulong max_age ) {
ulong const idx = fd_blockhash_map_idx_query_const( blockhashes->map, blockhash, ULONG_MAX, blockhashes->d.deque );
if( FD_UNLIKELY( idx==ULONG_MAX ) ) return 0;
/* Derive distance from tail (end) */
ulong const max = fd_blockhash_deq_max( blockhashes->d.deque );
ulong const end = (blockhashes->d.end - 1) & (max-1);
ulong const age = end + fd_ulong_if( idx<=end, 0UL, max ) - idx;
return age<=max_age;
}
122 changes: 122 additions & 0 deletions src/flamenco/runtime/fd_blockhashes.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
#ifndef HEADER_fd_src_flamenco_runtime_fd_blockhashes_h
#define HEADER_fd_src_flamenco_runtime_fd_blockhashes_h

#include "../types/fd_types.h"
#include "../../funk/fd_funk_base.h" /* fd_funk_rec_key_hash1 */

/* fd_blockhashes.h provides a "blockhash queue" API. The blockhash
queue is a consensus-relevant data structure that is part of the slot
bank.

See solana_accounts_db::blockhash_queue::BlockhashQueue. */

#define FD_BLOCKHASHES_MAX 301

/* See solana_accounts_db::blockhash_queue::HashInfo. */

struct fd_blockhash_info {
fd_hash_t hash;
fd_fee_calculator_t fee_calculator;
ushort next;
ushort exists : 1;
};

typedef struct fd_blockhash_info fd_blockhash_info_t;

/* Declare a static size deque for the blockhash queue. */

#define DEQUE_NAME fd_blockhash_deq
#define DEQUE_T fd_blockhash_info_t
#define DEQUE_MAX 512 /* must be a power of 2 */
#include "../../util/tmpl/fd_deque.c"

/* Declare a separately chained hash map over the blockhash queue. */

#define FD_BLOCKHASH_MAP_CHAIN_MAX (512UL)
#define FD_BLOCKHASH_MAP_FOOTPRINT (1048UL)

#define MAP_NAME fd_blockhash_map
#define MAP_ELE_T fd_blockhash_info_t
#define MAP_KEY_T fd_hash_t
#define MAP_KEY hash
#define MAP_IDX_T ushort
#define MAP_NEXT next
#define MAP_KEY_EQ(k0,k1) fd_hash_eq( (k0), (k1) )
#define MAP_KEY_HASH(k,s) fd_funk_rec_key_hash1( (k->uc), 0, (s) )
#include "../../util/tmpl/fd_map_chain.c"

/* fd_blockhashes_t is the class representing a blockhash queue.

It is a static size container housing sub-structures as plain old
struct members. Safe to declare as a local variable assuming the
stack is sufficiently sized. Entirely self-contained and position-
independent (safe to clone via fd_memcpy and safe to map into another
address space).

Under the hood it is an array-backed double-ended queue, and a
separately-chained hash index on top. New entries are inserted to
the **tail** of the queue. */

struct fd_blockhashes {

union {
fd_blockhash_map_t map[1];
uchar map_mem[ FD_BLOCKHASH_MAP_FOOTPRINT ];
};

fd_blockhash_deq_private_t d;

};

typedef struct fd_blockhashes fd_blockhashes_t;

FD_PROTOTYPES_BEGIN

fd_blockhashes_t *
fd_blockhashes_init( fd_blockhashes_t * mem,
ulong seed );

fd_blockhashes_t *
fd_blockhashes_recover( fd_blockhashes_t * blockhashes,
fd_hash_hash_age_pair_t const * ages,
ulong age_cnt,
ulong seed );

/* fd_blockhashes_push_new adds a new slot to the blockhash queue.
The caller fills the returned pointer with blockhash queue info
(currently only lamports_per_signature). Called as part of regular
runtime processing. Evicts the oldest entry if the queue is full
(practically always the case except for the first few blocks after
genesis). Always returns a valid pointer. */

fd_blockhash_info_t *
fd_blockhashes_push_new( fd_blockhashes_t * blockhashes,
fd_hash_t const * hash );

/* fd_blockhashes_push_old behaves like the above, but adding a new
oldest entry instead. Returns NULL if there is no more space.
Useful for testing. */

fd_blockhash_info_t *
fd_blockhashes_push_old( fd_blockhashes_t * blockhashes,
fd_hash_t const * hash );

/* fd_blockhashes_pop_new removes the newest blockhash queue entry. */

void
fd_blockhashes_pop_new( fd_blockhashes_t * blockhashes );

FD_FN_PURE int
fd_blockhashes_check_age( fd_blockhashes_t const * blockhashes,
fd_hash_t const * blockhash,
ulong max_age );

FD_FN_PURE static inline fd_hash_t const *
fd_blockhashes_peek_last( fd_blockhashes_t const * blockhashes ) {
if( FD_UNLIKELY( fd_blockhash_deq_empty( blockhashes->d.deque ) ) ) return 0;
return &fd_blockhash_deq_peek_tail_const( blockhashes->d.deque )->hash;
}

FD_PROTOTYPES_END

#endif /* HEADER_fd_src_flamenco_runtime_fd_blockhashes_h */
Loading
Loading