Skip to main content

ethrex_storage/
store.rs

1#[cfg(feature = "rocksdb")]
2use crate::backend::rocksdb::RocksDBBackend;
3use crate::{
4    STORE_METADATA_FILENAME, STORE_SCHEMA_VERSION,
5    api::{
6        StorageBackend, StorageReadView, StorageWriteBatch,
7        tables::{
8            ACCOUNT_CODE_METADATA, ACCOUNT_CODES, ACCOUNT_FLATKEYVALUE, ACCOUNT_TRIE_NODES,
9            BAD_BLOCKS, BLOCK_ACCESS_LISTS, BLOCK_NUMBERS, BODIES, CANONICAL_BLOCK_HASHES,
10            CHAIN_DATA, EXECUTION_WITNESSES, FULLSYNC_HEADERS, HEADERS, INVALID_CHAINS,
11            MISC_VALUES, PENDING_BLOCKS, RECEIPTS_V2, SNAP_STATE, STATE_HISTORY,
12            STORAGE_FLATKEYVALUE, STORAGE_TRIE_NODES, TRANSACTION_LOCATIONS,
13        },
14    },
15    apply_prefix,
16    backend::in_memory::InMemoryBackend,
17    block_data_buffer::BlockDataBuffer,
18    error::StoreError,
19    journal::{FlatDiff, JournalEntry},
20    layering::{Overlay, TrieLayerCache, TrieWrapper},
21    rlp::{BlockBodyRLP, BlockHeaderRLP, BlockRLP},
22    trie::{BackendTrieDB, BackendTrieDBLocked, classify_trie_key},
23    utils::{ChainDataIndex, SnapStateIndex},
24};
25
26use ethrex_common::{
27    Address, H256, U256,
28    types::{
29        AccountInfo, AccountState, AccountUpdate, Block, BlockBody, BlockHash, BlockHeader,
30        BlockNumber, ChainConfig, Code, CodeMetadata, ForkId, Genesis, GenesisAccount, Index,
31        Receipt, Transaction,
32        block_access_list::BlockAccessList,
33        block_execution_witness::{ExecutionWitness, RpcExecutionWitness},
34    },
35    utils::keccak,
36};
37use ethrex_crypto::{NativeCrypto, keccak::keccak_hash};
38use ethrex_rlp::{
39    decode::{RLPDecode, decode_bytes},
40    encode::RLPEncode,
41};
42use ethrex_trie::{EMPTY_TRIE_HASH, Nibbles, Trie, TrieLogger, TrieNode, TrieWitness};
43use ethrex_trie::{Node, NodeRLP};
44use lru::LruCache;
45use rayon::prelude::*;
46use rustc_hash::FxBuildHasher;
47use serde::{Deserialize, Serialize};
48use std::{
49    collections::{BTreeMap, HashMap, HashSet, hash_map::Entry},
50    fmt::Debug,
51    io::Write,
52    path::{Path, PathBuf},
53    sync::{
54        Arc, Condvar, Mutex, RwLock,
55        atomic::{AtomicUsize, Ordering},
56        mpsc::{SyncSender, TryRecvError, sync_channel},
57    },
58    thread::JoinHandle,
59};
60use tracing::{debug, error, info, warn};
61
62/// Maximum number of execution witnesses to keep in the database
63pub const MAX_WITNESSES: u64 = 128;
64
65// We use one constant for in-memory and another for on-disk backends.
66// This is due to tests requiring state older than 128 blocks.
67// TODO: unify these
68pub const DB_COMMIT_THRESHOLD: usize = 128;
69const IN_MEMORY_COMMIT_THRESHOLD: usize = 10000;
70
71/// Depth-only commit threshold for batch execution (full sync / block import). Each batch
72/// layer holds ~1024 blocks of trie diffs (~1 GB), so we flush after a few layers to bound
73/// memory. The canonical `head - DB_COMMIT_THRESHOLD` safe-commit root never lands on a batch
74/// layer boundary, so batch mode commits by depth instead; this is sound because full sync and
75/// import only ever extend a single canonical chain (no competing forks to mis-commit).
76pub const BATCH_COMMIT_THRESHOLD: usize = 4;
77
78/// Sorted, sharded `multi_get` tuning, shared by the account, storage, and
79/// trie-node batch read paths. A single `multi_get` runs at queue depth 1
80/// (async_io is off), so cold batches are split into contiguous sorted shards
81/// read on separate blocking threads. `KEYS_PER_SHARD` targets shard size,
82/// `MAX_SHARDS` caps concurrency; scattered keys make 64 the sweet spot (128
83/// over-fragments; coldbench).
84const KEYS_PER_SHARD: usize = 256;
85const MAX_SHARDS: usize = 64;
86
87/// Shard count for a sorted, sharded `multi_get` over `n` keys.
88fn shard_count(n: usize) -> usize {
89    n.div_ceil(KEYS_PER_SHARD).clamp(1, MAX_SHARDS)
90}
91
92/// Default size in bytes of the RocksDB shared block cache: 12 GiB.
93///
94/// This cache holds both data blocks AND the index/bloom-filter blocks for every
95/// open SST file (because we enable `cache_index_and_filter_blocks`), so its size
96/// is the effective upper bound on RocksDB's resident memory footprint. 12 GiB
97/// keeps the filter/index working set resident plus hot EVM state; a sweep on a
98/// synced mainnet node (32 GiB cap) found 8-16 GiB all keep up with head-following,
99/// with larger giving no gain (the OS page cache backstops the uncompressed state
100/// CFs) and ~8 GiB the floor where the filter set starts to thrash.
101pub const DEFAULT_ROCKSDB_BLOCK_CACHE_SIZE_BYTES: usize = 12 * 1024 * 1024 * 1024;
102
103/// Tunable configuration for [`Store::new_with_config`] and related constructors.
104///
105/// Use [`StoreConfig::default()`] for production-tuned defaults; callers that
106/// don't need to override anything should keep calling [`Store::new`] directly.
107#[derive(Debug, Clone, Copy)]
108pub struct StoreConfig {
109    /// Size in bytes of the RocksDB shared block cache. With
110    /// `cache_index_and_filter_blocks` enabled (the ethrex default), this is
111    /// the effective ceiling on RocksDB's resident memory. Ignored for
112    /// in-memory backends.
113    pub rocksdb_block_cache_size: usize,
114    /// Bound on the persist worker's channel: number of staged (acked) live
115    /// messages whose flush may still be in flight. Once full, the next send
116    /// blocks — that is the backpressure that throttles `newPayload`.
117    /// Clamped to `max(1)` at construction (0 would make a rendezvous channel).
118    pub persist_channel_capacity: usize,
119}
120
121impl Default for StoreConfig {
122    fn default() -> Self {
123        Self {
124            rocksdb_block_cache_size: DEFAULT_ROCKSDB_BLOCK_CACHE_SIZE_BYTES,
125            persist_channel_capacity: DEFAULT_PERSIST_CHANNEL_CAPACITY,
126        }
127    }
128}
129
130/// Control messages for the FlatKeyValue generator
131#[derive(Debug, PartialEq)]
132enum FKVGeneratorControlMessage {
133    Stop,
134    Continue,
135}
136
137// 64mb
138const CODE_CACHE_MAX_SIZE: u64 = 64 * 1024 * 1024;
139
140/// Key used to persist the `flushed_upto` block number in `MISC_VALUES`.
141const FLUSHED_UPTO_KEY: &[u8] = b"bodies_flushed_upto";
142
143/// Single key under which the bounded list of bad blocks is stored in `BAD_BLOCKS`.
144const BAD_BLOCKS_KEY: &[u8] = b"bad_blocks";
145
146/// Maximum number of bad blocks retained for `debug_getBadBlocks`.
147const MAX_BAD_BLOCKS: usize = 16;
148
149#[derive(Debug)]
150struct CodeCache {
151    inner_cache: LruCache<H256, Code, FxBuildHasher>,
152    cache_size: u64,
153}
154
155impl Default for CodeCache {
156    fn default() -> Self {
157        Self {
158            inner_cache: LruCache::unbounded_with_hasher(FxBuildHasher),
159            cache_size: 0,
160        }
161    }
162}
163
164impl CodeCache {
165    fn get(&mut self, code_hash: &H256) -> Result<Option<Code>, StoreError> {
166        Ok(self.inner_cache.get(code_hash).cloned())
167    }
168
169    fn insert(&mut self, code: &Code) -> Result<(), StoreError> {
170        let code_size = code.size();
171        let cache_len = self.inner_cache.len() + 1;
172        self.cache_size += code_size as u64;
173        let current_size = self.cache_size;
174        debug!(
175            "[ACCOUNT CODE CACHE] cache elements (): {cache_len}, total size: {current_size} bytes"
176        );
177
178        while self.cache_size > CODE_CACHE_MAX_SIZE {
179            if let Some((_, code)) = self.inner_cache.pop_lru() {
180                self.cache_size -= code.size() as u64;
181            } else {
182                break;
183            }
184        }
185
186        self.inner_cache.get_or_insert(code.hash, || code.clone());
187        Ok(())
188    }
189}
190
191/// Main storage interface for the ethrex client.
192///
193/// `Store` is `Clone` and thread-safe; all clones share the same backend and
194/// caches via `Arc`. Reads consult an in-memory block-data buffer before disk
195/// so not-yet-flushed blocks are always visible.
196#[derive(Debug, Clone)]
197pub struct Store {
198    /// Path to the database directory.
199    db_path: PathBuf,
200    /// Storage backend (InMemory or RocksDB).
201    backend: Arc<dyn StorageBackend>,
202    /// Chain configuration (fork schedule, chain ID, etc.).
203    chain_config: ChainConfig,
204    /// Cache for trie nodes from recent blocks.
205    trie_cache: Arc<RwLock<Arc<TrieLayerCache>>>,
206    /// Channel for controlling the FlatKeyValue generator background task.
207    flatkeyvalue_control_tx: std::sync::mpsc::SyncSender<FKVGeneratorControlMessage>,
208    /// In-memory overlay of block data not yet flushed to disk.
209    block_data_buffer: Arc<RwLock<Arc<BlockDataBuffer>>>,
210    /// Channel to the single persist worker (`apply_updates` → `PersistMessage::Block`,
211    /// `wait_for_persistence_idle` → `PersistMessage::Ping`). The worker is the
212    /// sole mutator of `block_data_buffer` in production.
213    persist_tx: std::sync::mpsc::SyncSender<PersistMessage>,
214    /// Roots whose trie diff-layer is being built but not yet installed in
215    /// `trie_cache`. Trie opens block on these so a just-added block's state is
216    /// never read as stale before its layer lands.
217    pending_trie_roots: Arc<PendingTrieRoots>,
218    /// Cached latest canonical block header. May be slightly stale, which is
219    /// acceptable for RPC "latest" queries and sync operations.
220    latest_block_header: LatestBlockHeaderCache,
221    /// Last computed FlatKeyValue for incremental updates.
222    last_computed_flatkeyvalue: Arc<RwLock<Vec<u8>>>,
223
224    /// Cache for account bytecodes, keyed by the bytecode hash.
225    /// Note that we don't remove entries on account code changes, since
226    /// those changes already affect the code hash stored in the account, and only
227    /// may result in this cache having useless data.
228    account_code_cache: Arc<Mutex<CodeCache>>,
229
230    /// Cache for code metadata (code length), keyed by the bytecode hash.
231    /// Uses FxHashMap for efficient lookups, much smaller than code cache.
232    code_metadata_cache: Arc<Mutex<rustc_hash::FxHashMap<H256, CodeMetadata>>>,
233
234    /// Serializes concurrent `forkchoice_update` callers so that the cache
235    /// update and the DB write transaction remain mutually ordered.
236    fcu_lock: Arc<tokio::sync::Mutex<()>>,
237
238    /// Canonical safe-commit state root, computed after each forkchoice update.
239    ///
240    /// Shared with the [`TrieLayerCache`] so that the Store can update the cell without
241    /// replacing the cache Arc. `H256::zero()` means "no safe commit point yet".
242    /// Cloning `Store` shares this cell across all clones, which is required and correct.
243    safe_commit_root: Arc<RwLock<H256>>,
244
245    /// While set, `forkchoice_update_inner` skips STATE_HISTORY pruning (the
246    /// finalized-number update still lands). Set by `Blockchain::enter_reorg` for
247    /// the duration of a deep-reorg apply pass: `Overlay::from_journal` reads
248    /// journal entries one by one with no snapshot isolation, and syncer-driven
249    /// forkchoice updates are not gated by the reorg mutex, so a concurrent
250    /// finality advance could otherwise prune entries out from under overlay
251    /// construction (spurious `MissingEntry`) or between a case-1 attempt and its
252    /// retry. Pruning catches up on the first finality advance after the pass
253    /// ends (`delete_range` is cumulative).
254    journal_pruning_paused: Arc<std::sync::atomic::AtomicBool>,
255
256    background_threads: Arc<ThreadList>,
257}
258
259#[derive(Debug, Default)]
260struct ThreadList {
261    list: Vec<JoinHandle<()>>,
262}
263
264impl Drop for ThreadList {
265    fn drop(&mut self) {
266        for handle in self.list.drain(..) {
267            let _ = handle.join();
268        }
269    }
270}
271
272/// Storage trie nodes grouped by account address hash.
273///
274/// Each entry contains the hashed account address and the trie nodes
275/// for that account's storage trie.
276pub type StorageTrieNodes = Vec<(H256, Vec<(Nibbles, Vec<u8>)>)>;
277type StorageTries = HashMap<Address, (TrieWitness, Trie)>;
278
279/// Storage backend type selection.
280///
281/// Used when creating a new [`Store`] to specify which backend to use.
282#[derive(Debug, Clone, Copy, PartialEq, Eq)]
283pub enum EngineType {
284    /// In-memory storage, non-persistent. Suitable for testing.
285    InMemory,
286    /// RocksDB storage, persistent. Suitable for production.
287    #[cfg(feature = "rocksdb")]
288    RocksDB,
289}
290
291/// Batch of updates to apply to the store atomically.
292///
293/// Used during block execution to collect all state changes before
294/// committing them to the database in a single transaction.
295pub struct UpdateBatch {
296    /// New nodes to add to the state trie.
297    pub account_updates: Vec<TrieNode>,
298    /// Storage trie updates per account (keyed by hashed address).
299    pub storage_updates: Vec<(H256, Vec<TrieNode>)>,
300    /// Blocks to store.
301    pub blocks: Vec<Block>,
302    /// Receipts to store, grouped by block hash.
303    pub receipts: Vec<(H256, Vec<Receipt>)>,
304    /// Contract code updates (code hash -> bytecode).
305    pub code_updates: Vec<(H256, Code)>,
306    /// Commit gate for this batch's trie layers (independent of `wait_for_flush`).
307    ///
308    /// - `None`: live path (`newPayload`). The persist worker commits by the canonical
309    ///   `head - DB_COMMIT_THRESHOLD` safe-commit root.
310    /// - `Some(depth)`: single-canonical-chain execution (batch import, full sync, startup
311    ///   state regeneration). The persist worker commits every layer deeper than `depth`
312    ///   (see [`Trie::get_commitable_by_depth`]), which bounds resident trie layers to ~`depth`.
313    pub commit_depth: Option<usize>,
314    /// When the persist worker acks (independent of `commit_depth`).
315    ///
316    /// - `false`: ack after staging, so the caller's next-block execution overlaps this
317    ///   block's disk flush. Used by the live path and the per-block re-execution paths
318    ///   (regen / full-sync fallback / import tail) — their memory is already bounded by
319    ///   `commit_depth`'s depth gate and the persist channel capacity, so waiting per block
320    ///   would only serialize CPU and I/O for no benefit.
321    /// - `true`: ack after flush, bounding in-flight work to ~1 message. Used by the bespoke
322    ///   batch path, where a single message carries ~1024 blocks (~1 GB of trie diff) and two
323    ///   in flight would be a real memory cost.
324    pub wait_for_flush: bool,
325}
326
327/// A block's historical chain data to backfill below the snap-sync pivot: the
328/// (already-canonical) header, its body, and its receipts.
329///
330/// `index_transactions` controls whether the transaction-lookup index is written
331/// for this block; the backfill task drives it from the `--history.transactions`
332/// horizon so old blocks can be excluded from the index while still storing their
333/// bodies and receipts.
334pub struct BackfilledBlock {
335    pub header: BlockHeader,
336    pub body: BlockBody,
337    pub receipts: Vec<Receipt>,
338    pub index_transactions: bool,
339}
340
341/// Storage trie updates grouped by account address hash.
342pub type StorageUpdates = Vec<(H256, Vec<(Nibbles, Vec<u8>)>)>;
343
344/// Collection of account state changes from block execution.
345///
346/// Contains all the data needed to update the state trie after
347/// executing a block: account updates, storage updates, and code deployments.
348pub struct AccountUpdatesList {
349    /// Root hash of the state trie after applying these updates.
350    pub state_trie_hash: H256,
351    /// State trie node updates (path -> RLP-encoded node).
352    pub state_updates: Vec<(Nibbles, Vec<u8>)>,
353    /// Storage trie updates per account.
354    pub storage_updates: StorageUpdates,
355    /// New contract bytecode deployments.
356    pub code_updates: Vec<(H256, Code)>,
357}
358
359/// Encodes a tx-location entry as the operand passed to `merge_cf`.
360///
361/// The operand uses the **same encoding as the stored value** — a
362/// `Vec<(BlockNumber, BlockHash, Index)>` with a single element. This is
363/// required for an *associative* merge operator: RocksDB folds operands
364/// together with PartialMerge (during compaction, without a base value), and
365/// the result becomes an operand for a later merge. If the operand format
366/// differed from the merge output (e.g. operand = bare tuple, output = Vec),
367/// the re-fed result would fail to decode and entries would be silently
368/// dropped. Keeping both as `Vec` makes the merge truly associative.
369pub(crate) fn encode_tx_location_operand(
370    block_number: BlockNumber,
371    block_hash: BlockHash,
372    index: Index,
373) -> Vec<u8> {
374    vec![(block_number, block_hash, index)].encode_to_vec()
375}
376
377/// Merge function for the `TRANSACTION_LOCATIONS` column family.
378///
379/// The CF stores `Vec<(BlockNumber, BlockHash, Index)>` keyed by tx hash.
380/// Both stored values and operands use this same `Vec` encoding — this
381/// associativity requirement is mandatory: RocksDB folds operands together
382/// during compaction without a base value (PartialMerge), then feeds that
383/// result back into a later merge. A differing format would silently drop
384/// entries. See `encode_tx_location_operand`.
385///
386/// Within the fold, a later entry with the same `block_hash` replaces an
387/// earlier one (reorg dedupe). On decode failure the merge returns `None`
388/// so RocksDB surfaces a corruption error rather than silently dropping
389/// locations.
390///
391/// Merge instead of read-modify-write avoids the ~5–20 ms/block per-tx point
392/// lookup on the write path; consolidation is deferred to compaction or the
393/// next read.
394pub fn tx_locations_merge(
395    existing: Option<&[u8]>,
396    operands: impl IntoIterator<Item = impl AsRef<[u8]>>,
397) -> Option<Vec<u8>> {
398    // Fold one RLP-encoded `Vec` chunk into `list`, deduping by block_hash
399    // (later entry wins). Returns false on decode failure so the caller can
400    // abort the whole merge.
401    fn fold_chunk(
402        list: &mut Vec<(BlockNumber, BlockHash, Index)>,
403        bytes: &[u8],
404        what: &str,
405    ) -> bool {
406        match <Vec<(BlockNumber, BlockHash, Index)>>::decode(bytes) {
407            Ok(entries) => {
408                for (bn, bh, idx) in entries {
409                    list.retain(|(_, existing_bh, _)| *existing_bh != bh);
410                    list.push((bn, bh, idx));
411                }
412                true
413            }
414            Err(e) => {
415                error!(
416                    "tx_locations_merge: failed to decode {what} ({} bytes): {e}; \
417                     aborting merge to avoid silent data loss",
418                    bytes.len()
419                );
420                false
421            }
422        }
423    }
424
425    let mut list: Vec<(BlockNumber, BlockHash, Index)> = Vec::new();
426
427    // Order matters: RocksDB delivers operands oldest-first.
428    if let Some(bytes) = existing
429        && !fold_chunk(&mut list, bytes, "existing value")
430    {
431        return None;
432    }
433    for op in operands {
434        if !fold_chunk(&mut list, op.as_ref(), "operand") {
435            return None;
436        }
437    }
438    Some(list.encode_to_vec())
439}
440
441impl Store {
442    /// Block until the persist worker has fully processed all previously-sent
443    /// `Block` messages (staged, trie-layer built, flushed, evicted).
444    ///
445    /// Uses an ack-based `Ping` rather than a bare send because the channel is
446    /// buffered — a bare send proves nothing about prior message completion. The
447    /// worker is FIFO, so it handles the `Ping` only after every earlier `Block`
448    /// is done.
449    ///
450    /// Concurrent-producer caveat: if another thread sends a `Block` after the
451    /// `Ping` is enqueued, that block may not be flushed by the time this returns.
452    pub async fn wait_for_persistence_idle(&self) -> Result<(), StoreError> {
453        let tx = self.persist_tx.clone();
454        tokio::task::spawn_blocking(move || {
455            let (ack_tx, ack_rx) = sync_channel::<Result<(), StoreError>>(1);
456            tx.send(PersistMessage::Ping(ack_tx))
457                .map_err(|e| StoreError::Custom(format!("wait_for_persistence_idle send: {e}")))?;
458            ack_rx
459                .recv()
460                .map_err(|e| StoreError::Custom(format!("wait_for_persistence_idle ack: {e}")))?
461        })
462        .await
463        .map_err(|e| StoreError::Custom(format!("wait_for_persistence_idle join: {e}")))?
464    }
465
466    /// Flushes all in-memory state to disk for a clean shutdown.
467    ///
468    /// Sends a `Shutdown` handshake to the persist worker, which (being FIFO)
469    /// first drains every queued `Block`, then force-flushes the block-data
470    /// buffer to disk. Once the worker acks, this syncs the backend (memtables +
471    /// WAL) so the next process start needs no WAL recovery.
472    ///
473    /// The in-memory trie diff-layers are intentionally *not* force-committed.
474    /// The on-disk trie is a single-version, path-based store, so folding the
475    /// non-finalized tail into it would leave a post-restart reorg unable to
476    /// reconstruct the overwritten ancestor state (the node would wedge). The
477    /// recent (< `DB_COMMIT_THRESHOLD`) layers are dropped and re-executed on the
478    /// next start from the deep, reorg-safe on-disk base — exactly as after any
479    /// restart today.
480    ///
481    /// After this returns the persist worker has exited; the store must not be
482    /// used for further writes. Idempotent only in the sense that a second call
483    /// errors on the closed channel — call it exactly once, on shutdown.
484    pub async fn shutdown(&self) -> Result<(), StoreError> {
485        let tx = self.persist_tx.clone();
486        let backend = self.backend.clone();
487        tokio::task::spawn_blocking(move || {
488            let (ack_tx, ack_rx) = sync_channel::<Result<(), StoreError>>(1);
489            tx.send(PersistMessage::Shutdown { ack: ack_tx })
490                .map_err(|e| StoreError::Custom(format!("shutdown send: {e}")))?;
491            ack_rx
492                .recv()
493                .map_err(|e| StoreError::Custom(format!("shutdown ack: {e}")))??;
494            // Worker has flushed block data to the WAL/memtables; make it durable
495            // and recovery-free.
496            backend.flush()
497        })
498        .await
499        .map_err(|e| StoreError::Custom(format!("shutdown join: {e}")))?
500    }
501
502    /// Add a block in a single transaction.
503    /// This will store -> BlockHeader, BlockBody, BlockTransactions, BlockNumber.
504    pub async fn add_block(&self, block: Block) -> Result<(), StoreError> {
505        self.add_blocks(vec![block]).await
506    }
507
508    /// Add a batch of blocks in a single transaction.
509    /// This will store -> BlockHeader, BlockBody, BlockTransactions, BlockNumber.
510    pub async fn add_blocks(&self, blocks: Vec<Block>) -> Result<(), StoreError> {
511        let db = self.backend.clone();
512        tokio::task::spawn_blocking(move || {
513            let mut tx = db.begin_write()?;
514
515            for block in blocks {
516                write_block_data(
517                    tx.as_mut(),
518                    block.header.number,
519                    block.hash(),
520                    &block.header,
521                    &block.body,
522                )?;
523            }
524
525            tx.commit()
526        })
527        .await
528        .map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
529    }
530
531    /// Add block header
532    pub async fn add_block_header(
533        &self,
534        block_hash: BlockHash,
535        block_header: BlockHeader,
536    ) -> Result<(), StoreError> {
537        let hash_key = block_hash.encode_to_vec();
538        let header_value = BlockHeaderRLP::from(block_header).into_vec();
539        self.write_async(HEADERS, hash_key, header_value).await
540    }
541
542    /// Add a batch of block headers
543    pub async fn add_block_headers(
544        &self,
545        block_headers: Vec<BlockHeader>,
546    ) -> Result<(), StoreError> {
547        let mut txn = self.backend.begin_write()?;
548
549        for header in block_headers {
550            let block_hash = header.hash();
551            let block_number = header.number;
552            let hash_key = block_hash.encode_to_vec();
553            let header_value = BlockHeaderRLP::from(header).into_vec();
554
555            txn.put(HEADERS, &hash_key, &header_value)?;
556
557            let number_key = block_number.to_le_bytes().to_vec();
558            txn.put(BLOCK_NUMBERS, &hash_key, &number_key)?;
559        }
560        txn.commit()?;
561        Ok(())
562    }
563
564    /// Obtain canonical block header
565    pub fn get_block_header(
566        &self,
567        block_number: BlockNumber,
568    ) -> Result<Option<BlockHeader>, StoreError> {
569        let latest = self.latest_block_header.get();
570        if block_number == latest.number {
571            return Ok(Some((*latest).clone()));
572        }
573        // Resolve the canonical hash, then read through the buffer-aware by-hash
574        // path so a canonical-but-still-buffered block is visible (mirrors
575        // `get_block_body`). `load_block_header` is disk-only and would return
576        // `None` for a block whose header has not been flushed yet.
577        let Some(block_hash) = self.get_canonical_block_hash_sync(block_number)? else {
578            return Ok(None);
579        };
580        self.get_block_header_by_hash(block_hash)
581    }
582
583    /// Add block body
584    pub async fn add_block_body(
585        &self,
586        block_hash: BlockHash,
587        block_body: BlockBody,
588    ) -> Result<(), StoreError> {
589        let hash_key = block_hash.encode_to_vec();
590        let body_value = BlockBodyRLP::from(block_body).into_vec();
591        self.write_async(BODIES, hash_key, body_value).await
592    }
593
594    /// Backfill historical chain data for a contiguous range of already-canonical
595    /// blocks (bodies, receipts, and optionally the transaction-lookup index),
596    /// then lower `earliest_block_number` to `new_earliest` — all in one write
597    /// transaction.
598    ///
599    /// This is the storage half of `--history.chain` backfill. It does NOT touch
600    /// state, the trie layer cache, or fork choice: the headers and canonical
601    /// mapping already exist from snap-sync header backfill, so only the
602    /// body / receipt / tx-index tables are filled. Writing the data and advancing
603    /// the frontier atomically preserves the invariant that every block in
604    /// `[earliest_block_number, head]` has a stored body — a crash mid-batch
605    /// leaves both the data and the frontier unchanged, so resume never skips a
606    /// hole.
607    ///
608    /// `new_earliest` must be the lowest block number covered by `blocks` (the
609    /// bottom of the contiguous filled range); the caller fills downward without
610    /// gaps.
611    pub async fn add_backfilled_blocks(
612        &self,
613        blocks: Vec<BackfilledBlock>,
614        new_earliest: BlockNumber,
615    ) -> Result<(), StoreError> {
616        let backend = self.backend.clone();
617        tokio::task::spawn_blocking(move || {
618            let mut txn = backend.begin_write()?;
619            for block in &blocks {
620                let hash = block.header.hash();
621                let hash_key = hash.encode_to_vec();
622                txn.put(
623                    BODIES,
624                    &hash_key,
625                    &BlockBodyRLP::from(block.body.clone()).into_vec(),
626                )?;
627                for (index, receipt) in block.receipts.iter().enumerate() {
628                    txn.put(
629                        RECEIPTS_V2,
630                        &receipt_key(&hash, index as u64),
631                        &receipt.encode_storage(),
632                    )?;
633                }
634                if block.index_transactions {
635                    for (index, transaction) in block.body.transactions.iter().enumerate() {
636                        txn.merge(
637                            TRANSACTION_LOCATIONS,
638                            transaction.hash(&NativeCrypto).as_bytes(),
639                            &encode_tx_location_operand(block.header.number, hash, index as u64),
640                        )?;
641                    }
642                }
643            }
644            txn.put(
645                CHAIN_DATA,
646                &chain_data_key(ChainDataIndex::EarliestBlockNumber),
647                &new_earliest.to_le_bytes(),
648            )?;
649            txn.commit()
650        })
651        .await
652        .map_err(|e| StoreError::Custom(format!("Backfill write task panicked: {e}")))?
653    }
654
655    /// RocksDB observability snapshot (per-column-family sizes/keys/files, block
656    /// cache, compaction). `None` for the in-memory backend. Read by the metrics
657    /// collector; cheap (RocksDB property reads).
658    pub fn rocksdb_stats(&self) -> Option<crate::api::RocksDbStats> {
659        self.backend.db_stats()
660    }
661
662    /// Obtain canonical block body
663    pub async fn get_block_body(
664        &self,
665        block_number: BlockNumber,
666    ) -> Result<Option<BlockBody>, StoreError> {
667        let Some(block_hash) = self.get_canonical_block_hash_sync(block_number)? else {
668            return Ok(None);
669        };
670
671        self.get_block_body_by_hash(block_hash).await
672    }
673
674    /// Remove canonical block
675    pub async fn remove_block(&self, block_number: BlockNumber) -> Result<(), StoreError> {
676        let Some(hash) = self.get_canonical_block_hash_sync(block_number)? else {
677            return Ok(());
678        };
679
680        let backend = self.backend.clone();
681        tokio::task::spawn_blocking(move || {
682            let hash_key = hash.encode_to_vec();
683
684            let mut txn = backend.begin_write()?;
685            txn.delete(
686                CANONICAL_BLOCK_HASHES,
687                block_number.to_le_bytes().as_slice(),
688            )?;
689            txn.delete(BODIES, &hash_key)?;
690            txn.delete(HEADERS, &hash_key)?;
691            txn.delete(BLOCK_NUMBERS, &hash_key)?;
692            txn.commit()
693        })
694        .await
695        .map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
696    }
697
698    /// Obtain canonical block bodies in from..=to
699    pub async fn get_block_bodies(
700        &self,
701        from: BlockNumber,
702        to: BlockNumber,
703    ) -> Result<Vec<Option<BlockBody>>, StoreError> {
704        // TODO: Implement read bulk
705        let buffer = self.buffer()?;
706        let backend = self.backend.clone();
707        tokio::task::spawn_blocking(move || {
708            let numbers: Vec<BlockNumber> = (from..=to).collect();
709            let mut block_bodies = Vec::new();
710
711            let txn = backend.begin_read()?;
712            for number in numbers {
713                let Some(hash) = txn
714                    .get(CANONICAL_BLOCK_HASHES, number.to_le_bytes().as_slice())?
715                    .map(|bytes| H256::decode(bytes.as_slice()))
716                    .transpose()?
717                else {
718                    block_bodies.push(None);
719                    continue;
720                };
721                // Consult the in-memory buffer first so a not-yet-flushed body
722                // is not reported as missing (mirrors get_block_bodies_by_hash).
723                if let Some(body) = buffer.get_body(&hash) {
724                    block_bodies.push(Some(body));
725                    continue;
726                }
727                let hash_key = hash.encode_to_vec();
728                let block_body_opt = txn
729                    .get(BODIES, &hash_key)?
730                    .map(|bytes| BlockBodyRLP::from_bytes(bytes).to())
731                    .transpose()
732                    .map_err(StoreError::from)?;
733
734                block_bodies.push(block_body_opt);
735            }
736
737            Ok(block_bodies)
738        })
739        .await
740        .map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
741    }
742
743    /// Obtain block bodies from a list of hashes
744    pub async fn get_block_bodies_by_hash(
745        &self,
746        hashes: Vec<BlockHash>,
747    ) -> Result<Vec<BlockBody>, StoreError> {
748        let buffer = self.buffer()?;
749        let backend = self.backend.clone();
750        // TODO: Implement read bulk
751        tokio::task::spawn_blocking(move || {
752            let txn = backend.begin_read()?;
753            let mut block_bodies = Vec::new();
754            for hash in hashes {
755                // Consult the in-memory buffer first, like the single-hash reader,
756                // so a not-yet-flushed body is not reported as missing.
757                if let Some(body) = buffer.get_body(&hash) {
758                    block_bodies.push(body);
759                    continue;
760                }
761                let hash_key = hash.encode_to_vec();
762
763                let Some(block_body) = txn
764                    .get(BODIES, &hash_key)?
765                    .map(|bytes| BlockBodyRLP::from_bytes(bytes).to())
766                    .transpose()
767                    .map_err(StoreError::from)?
768                else {
769                    return Err(StoreError::Custom(format!(
770                        "Block body not found for hash: {hash}"
771                    )));
772                };
773                block_bodies.push(block_body);
774            }
775            Ok(block_bodies)
776        })
777        .await
778        .map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
779    }
780
781    /// Obtain any block body using the hash
782    pub async fn get_block_body_by_hash(
783        &self,
784        block_hash: BlockHash,
785    ) -> Result<Option<BlockBody>, StoreError> {
786        if let Some(b) = self.buffer()?.get_body(&block_hash) {
787            return Ok(Some(b));
788        }
789        self.read_async(BODIES, block_hash.encode_to_vec())
790            .await?
791            .map(|bytes| BlockBodyRLP::from_bytes(bytes).to())
792            .transpose()
793            .map_err(StoreError::from)
794    }
795
796    pub fn get_block_header_by_hash(
797        &self,
798        block_hash: BlockHash,
799    ) -> Result<Option<BlockHeader>, StoreError> {
800        let latest = self.latest_block_header.get();
801        if block_hash == latest.hash() {
802            return Ok(Some((*latest).clone()));
803        }
804        if let Some(h) = self.buffer()?.get_header(&block_hash) {
805            return Ok(Some(h));
806        }
807        self.load_block_header_by_hash(block_hash)
808    }
809
810    pub fn add_pending_block(&self, block: Block) -> Result<(), StoreError> {
811        let block_hash = block.hash();
812        let block_value = BlockRLP::from(block).into_vec();
813        self.write(PENDING_BLOCKS, block_hash.as_bytes().to_vec(), block_value)
814    }
815
816    pub async fn get_pending_block(
817        &self,
818        block_hash: BlockHash,
819    ) -> Result<Option<Block>, StoreError> {
820        self.read_async(PENDING_BLOCKS, block_hash.as_bytes().to_vec())
821            .await?
822            .map(|bytes| BlockRLP::from_bytes(bytes).to())
823            .transpose()
824            .map_err(StoreError::from)
825    }
826
827    /// Add block number for a given hash
828    pub async fn add_block_number(
829        &self,
830        block_hash: BlockHash,
831        block_number: BlockNumber,
832    ) -> Result<(), StoreError> {
833        let number_value = block_number.to_le_bytes().to_vec();
834        self.write_async(BLOCK_NUMBERS, block_hash.encode_to_vec(), number_value)
835            .await
836    }
837
838    /// Obtain block number for a given hash
839    pub async fn get_block_number(
840        &self,
841        block_hash: BlockHash,
842    ) -> Result<Option<BlockNumber>, StoreError> {
843        if let Some(n) = self.buffer()?.get_number(&block_hash) {
844            return Ok(Some(n));
845        }
846        self.read_async(BLOCK_NUMBERS, block_hash.encode_to_vec())
847            .await?
848            .map(decode_block_number)
849            .transpose()
850    }
851
852    /// Store transaction location (block number and index of the transaction within the block)
853    pub async fn add_transaction_location(
854        &self,
855        transaction_hash: H256,
856        block_number: BlockNumber,
857        block_hash: BlockHash,
858        index: Index,
859    ) -> Result<(), StoreError> {
860        self.add_transaction_locations(vec![(transaction_hash, block_number, block_hash, index)])
861            .await
862    }
863
864    /// Store transaction locations in batch (one db transaction for all)
865    pub async fn add_transaction_locations(
866        &self,
867        locations: Vec<(H256, BlockNumber, BlockHash, Index)>,
868    ) -> Result<(), StoreError> {
869        let db = self.backend.clone();
870        tokio::task::spawn_blocking(move || {
871            let mut tx = db.begin_write()?;
872            for (tx_hash, block_number, block_hash, index) in locations {
873                tx.merge(
874                    TRANSACTION_LOCATIONS,
875                    tx_hash.as_bytes(),
876                    &encode_tx_location_operand(block_number, block_hash, index),
877                )?;
878            }
879            tx.commit()
880        })
881        .await
882        .map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
883    }
884
885    /// Obtain transaction location (block hash and index)
886    pub async fn get_transaction_location(
887        &self,
888        transaction_hash: H256,
889    ) -> Result<Option<(BlockNumber, BlockHash, Index)>, StoreError> {
890        let buffered = self.buffer()?.get_tx_locations(&transaction_hash);
891        let db = self.backend.clone();
892        tokio::task::spawn_blocking(move || {
893            let tx = db.begin_read()?;
894            let mut locations = buffered;
895            if let Some(bytes) = tx.get(TRANSACTION_LOCATIONS, transaction_hash.as_bytes())? {
896                locations.extend(<Vec<(BlockNumber, BlockHash, Index)>>::decode(&bytes)?);
897            }
898            for (block_number, block_hash, index) in locations {
899                let canonical_hash = tx
900                    .get(
901                        CANONICAL_BLOCK_HASHES,
902                        block_number.to_le_bytes().as_slice(),
903                    )?
904                    .map(|bytes| H256::decode(bytes.as_slice()))
905                    .transpose()?;
906                if canonical_hash == Some(block_hash) {
907                    return Ok(Some((block_number, block_hash, index)));
908                }
909            }
910            Ok(None)
911        })
912        .await
913        .map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
914    }
915
916    /// Add receipt
917    pub async fn add_receipt(
918        &self,
919        block_hash: BlockHash,
920        index: Index,
921        receipt: Receipt,
922    ) -> Result<(), StoreError> {
923        let key = receipt_key(&block_hash, index);
924        // Storage codec (NOT wire/consensus): preserves frame-receipt
925        // `succeeded` + aggregated logs; identical to encode_to_vec for
926        // non-frame receipts.
927        let value = receipt.encode_storage();
928        self.write_async(RECEIPTS_V2, key, value).await
929    }
930
931    /// Add receipts
932    pub async fn add_receipts(
933        &self,
934        block_hash: BlockHash,
935        receipts: Vec<Receipt>,
936    ) -> Result<(), StoreError> {
937        let batch_items: Vec<_> = receipts
938            .into_iter()
939            .enumerate()
940            .map(|(index, receipt)| {
941                let key = receipt_key(&block_hash, index as u64);
942                let value = receipt.encode_storage();
943                (key, value)
944            })
945            .collect();
946        self.write_batch_async(RECEIPTS_V2, batch_items).await
947    }
948
949    /// Obtain receipt for a canonical block represented by the block number.
950    pub async fn get_receipt(
951        &self,
952        block_number: BlockNumber,
953        index: Index,
954    ) -> Result<Option<Receipt>, StoreError> {
955        // FIXME (#4353)
956        let Some(block_hash) = self.get_canonical_block_hash(block_number).await? else {
957            return Ok(None);
958        };
959        self.get_receipt_by_block_hash(block_hash, index).await
960    }
961
962    /// Obtain receipt by block hash and index
963    async fn get_receipt_by_block_hash(
964        &self,
965        block_hash: BlockHash,
966        index: Index,
967    ) -> Result<Option<Receipt>, StoreError> {
968        if let Some(r) = self.buffer()?.get_receipt(&block_hash, index) {
969            return Ok(Some(r));
970        }
971        let key = receipt_key(&block_hash, index);
972        self.read_async(RECEIPTS_V2, key)
973            .await?
974            .map(|bytes| Receipt::decode_storage(bytes.as_slice()))
975            .transpose()
976            .map_err(StoreError::from)
977    }
978
979    /// Get account code by its hash.
980    ///
981    /// Checks the in-memory block-data buffer first, then the LRU cache
982    /// (`account_code_cache`), and finally the database.  Code that has been
983    /// inserted via `engine_newPayload` but not yet flushed to disk is therefore
984    /// visible to callers without an explicit flush.
985    pub fn get_account_code(&self, code_hash: H256) -> Result<Option<Code>, StoreError> {
986        if let Some(code) = self.buffer()?.get_code(&code_hash) {
987            return Ok(Some(code));
988        }
989        // check cache first
990        if let Some(code) = self
991            .account_code_cache
992            .lock()
993            .map_err(|_| StoreError::LockError)?
994            .get(&code_hash)?
995        {
996            return Ok(Some(code));
997        }
998
999        let Some(bytes) = self
1000            .backend
1001            .begin_read()?
1002            .get(ACCOUNT_CODES, code_hash.as_bytes())?
1003        else {
1004            return Ok(None);
1005        };
1006        let (bytecode_slice, targets) = decode_bytes(&bytes)?;
1007        let code = Code::from_parts_unchecked(
1008            code_hash,
1009            bytecode_slice,
1010            <Vec<u32>>::decode(targets)?.into(),
1011        );
1012
1013        // insert into cache and evict if needed
1014        self.account_code_cache
1015            .lock()
1016            .map_err(|_| StoreError::LockError)?
1017            .insert(&code)?;
1018
1019        Ok(Some(code))
1020    }
1021
1022    /// Check if account code exists by its hash, without constructing the full `Code` struct.
1023    /// More efficient than `get_account_code` for existence checks since it skips
1024    /// RLP decoding and `Code` struct construction (no `jump_targets` deserialization).
1025    /// Note: The underlying `get()` still reads the value from RocksDB (including blob files).
1026    pub fn code_exists(&self, code_hash: H256) -> Result<bool, StoreError> {
1027        // Code introduced by a not-yet-flushed block lives only in the buffer; check
1028        // it first so a contract created in the current block is visible (matches
1029        // get_account_code / get_code_metadata).
1030        if self.buffer()?.get_code(&code_hash).is_some() {
1031            return Ok(true);
1032        }
1033        // Check cache first
1034        if self
1035            .account_code_cache
1036            .lock()
1037            .map_err(|_| StoreError::LockError)?
1038            .get(&code_hash)?
1039            .is_some()
1040        {
1041            return Ok(true);
1042        }
1043        // Check DB without reading the full value
1044        Ok(self
1045            .backend
1046            .begin_read()?
1047            .get(ACCOUNT_CODES, code_hash.as_bytes())?
1048            .is_some())
1049    }
1050
1051    /// Get code metadata (length) by its hash.
1052    ///
1053    /// Checks cache first, falls back to database. If metadata is missing,
1054    /// falls back to loading full code and extracts length (auto-migration).
1055    pub fn get_code_metadata(&self, code_hash: H256) -> Result<Option<CodeMetadata>, StoreError> {
1056        use ethrex_common::constants::EMPTY_KECCAK_HASH;
1057
1058        // Empty code special case
1059        if code_hash == *EMPTY_KECCAK_HASH {
1060            return Ok(Some(CodeMetadata { length: 0 }));
1061        }
1062
1063        // Check cache first
1064        if let Some(metadata) = self
1065            .code_metadata_cache
1066            .lock()
1067            .map_err(|_| StoreError::LockError)?
1068            .get(&code_hash)
1069            .copied()
1070        {
1071            return Ok(Some(metadata));
1072        }
1073
1074        // Try reading from metadata table
1075        let metadata = if let Some(bytes) = self
1076            .backend
1077            .begin_read()?
1078            .get(ACCOUNT_CODE_METADATA, code_hash.as_bytes())?
1079        {
1080            let length =
1081                u64::from_be_bytes(bytes.try_into().map_err(|_| {
1082                    StoreError::Custom("Invalid metadata length encoding".to_string())
1083                })?);
1084            CodeMetadata { length }
1085        } else {
1086            // Fallback: load full code and extract length (auto-migration)
1087            let Some(code) = self.get_account_code(code_hash)? else {
1088                return Ok(None);
1089            };
1090            let metadata = CodeMetadata {
1091                length: code.len() as u64,
1092            };
1093
1094            // Write metadata for future use (async, fire and forget)
1095            let metadata_buf = metadata.length.to_be_bytes().to_vec();
1096            let hash_key = code_hash.0.to_vec();
1097            let backend = self.backend.clone();
1098            tokio::task::spawn(async move {
1099                if let Err(e) = async {
1100                    let mut tx = backend.begin_write()?;
1101                    tx.put(ACCOUNT_CODE_METADATA, &hash_key, &metadata_buf)?;
1102                    tx.commit()
1103                }
1104                .await
1105                {
1106                    tracing::warn!("Failed to write code metadata during auto-migration: {}", e);
1107                }
1108            });
1109
1110            metadata
1111        };
1112
1113        // Update cache
1114        self.code_metadata_cache
1115            .lock()
1116            .map_err(|_| StoreError::LockError)?
1117            .insert(code_hash, metadata);
1118
1119        Ok(Some(metadata))
1120    }
1121
1122    /// Add account code
1123    pub async fn add_account_code(&self, code: Code) -> Result<(), StoreError> {
1124        let hash_key = code.hash.0.to_vec();
1125        let buf = encode_code(&code);
1126        let metadata_buf = (code.len() as u64).to_be_bytes();
1127
1128        // Write both code and metadata atomically
1129        let backend = self.backend.clone();
1130        tokio::task::spawn_blocking(move || {
1131            let mut tx = backend.begin_write()?;
1132            tx.put(ACCOUNT_CODES, &hash_key, &buf)?;
1133            tx.put(ACCOUNT_CODE_METADATA, &hash_key, &metadata_buf)?;
1134            tx.commit()
1135        })
1136        .await
1137        .map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
1138    }
1139
1140    /// Clears all checkpoint data created during the last snap sync
1141    pub async fn clear_snap_state(&self) -> Result<(), StoreError> {
1142        let db = self.backend.clone();
1143        tokio::task::spawn_blocking(move || db.clear_table(SNAP_STATE))
1144            .await
1145            .map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
1146    }
1147
1148    pub async fn get_transaction_by_hash(
1149        &self,
1150        transaction_hash: H256,
1151    ) -> Result<Option<Transaction>, StoreError> {
1152        let (_block_number, block_hash, index) =
1153            match self.get_transaction_location(transaction_hash).await? {
1154                Some(location) => location,
1155                None => return Ok(None),
1156            };
1157        self.get_transaction_by_location(block_hash, index).await
1158    }
1159
1160    pub async fn get_transaction_by_location(
1161        &self,
1162        block_hash: H256,
1163        index: u64,
1164    ) -> Result<Option<Transaction>, StoreError> {
1165        let block_body = match self.get_block_body_by_hash(block_hash).await? {
1166            Some(body) => body,
1167            None => return Ok(None),
1168        };
1169        let index: usize = index.try_into()?;
1170        Ok(block_body.transactions.get(index).cloned())
1171    }
1172
1173    pub async fn get_block_by_hash(
1174        &self,
1175        block_hash: BlockHash,
1176    ) -> Result<Option<Block>, StoreError> {
1177        let header = match self.get_block_header_by_hash(block_hash)? {
1178            Some(header) => header,
1179            None => return Ok(None),
1180        };
1181        let body = match self.get_block_body_by_hash(block_hash).await? {
1182            Some(body) => body,
1183            None => return Ok(None),
1184        };
1185        Ok(Some(Block::new(header, body)))
1186    }
1187
1188    pub async fn get_block_by_number(
1189        &self,
1190        block_number: BlockNumber,
1191    ) -> Result<Option<Block>, StoreError> {
1192        let Some(block_hash) = self.get_canonical_block_hash(block_number).await? else {
1193            return Ok(None);
1194        };
1195        self.get_block_by_hash(block_hash).await
1196    }
1197
1198    // Get the canonical block hash for a given block number.
1199    pub async fn get_canonical_block_hash(
1200        &self,
1201        block_number: BlockNumber,
1202    ) -> Result<Option<BlockHash>, StoreError> {
1203        let last = self.latest_block_header.get();
1204        if last.number == block_number {
1205            return Ok(Some(last.hash()));
1206        }
1207        let backend = self.backend.clone();
1208        tokio::task::spawn_blocking(move || {
1209            backend
1210                .begin_read()?
1211                .get(
1212                    CANONICAL_BLOCK_HASHES,
1213                    block_number.to_le_bytes().as_slice(),
1214                )?
1215                .map(|bytes| H256::decode(bytes.as_slice()))
1216                .transpose()
1217                .map_err(StoreError::from)
1218        })
1219        .await
1220        .map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
1221    }
1222
1223    /// Stores the chain configuration values, should only be called once after reading the genesis file
1224    /// Ignores previously stored values if present
1225    pub async fn set_chain_config(&mut self, chain_config: &ChainConfig) -> Result<(), StoreError> {
1226        self.chain_config = *chain_config;
1227        let key = chain_data_key(ChainDataIndex::ChainConfig);
1228        let value = serde_json::to_string(chain_config)
1229            .map_err(|_| StoreError::Custom("Failed to serialize chain config".to_string()))?
1230            .into_bytes();
1231        self.write_async(CHAIN_DATA, key, value).await
1232    }
1233
1234    /// Update earliest block number
1235    pub async fn update_earliest_block_number(
1236        &self,
1237        block_number: BlockNumber,
1238    ) -> Result<(), StoreError> {
1239        let key = chain_data_key(ChainDataIndex::EarliestBlockNumber);
1240        let value = block_number.to_le_bytes().to_vec();
1241        self.write_async(CHAIN_DATA, key, value).await
1242    }
1243
1244    /// Reads a block number stored under the given chain-data index.
1245    async fn read_chain_data_block_number(
1246        &self,
1247        index: ChainDataIndex,
1248    ) -> Result<Option<BlockNumber>, StoreError> {
1249        self.read_async(CHAIN_DATA, chain_data_key(index))
1250            .await?
1251            .map(decode_block_number)
1252            .transpose()
1253    }
1254
1255    /// Obtain earliest block number
1256    pub async fn get_earliest_block_number(&self) -> Result<BlockNumber, StoreError> {
1257        self.read_chain_data_block_number(ChainDataIndex::EarliestBlockNumber)
1258            .await?
1259            .ok_or(StoreError::MissingEarliestBlockNumber)
1260    }
1261
1262    /// Obtain finalized block number
1263    pub async fn get_finalized_block_number(&self) -> Result<Option<BlockNumber>, StoreError> {
1264        self.read_chain_data_block_number(ChainDataIndex::FinalizedBlockNumber)
1265            .await
1266    }
1267
1268    /// Obtain safe block number
1269    pub async fn get_safe_block_number(&self) -> Result<Option<BlockNumber>, StoreError> {
1270        self.read_chain_data_block_number(ChainDataIndex::SafeBlockNumber)
1271            .await
1272    }
1273
1274    /// Obtain latest block number
1275    pub fn get_latest_block_number(&self) -> Result<BlockNumber, StoreError> {
1276        Ok(self.latest_block_header.get().number)
1277    }
1278
1279    /// Update pending block number
1280    pub async fn update_pending_block_number(
1281        &self,
1282        block_number: BlockNumber,
1283    ) -> Result<(), StoreError> {
1284        let key = chain_data_key(ChainDataIndex::PendingBlockNumber);
1285        let value = block_number.to_le_bytes().to_vec();
1286        self.write_async(CHAIN_DATA, key, value).await
1287    }
1288
1289    /// Obtain pending block number
1290    pub async fn get_pending_block_number(&self) -> Result<Option<BlockNumber>, StoreError> {
1291        self.read_chain_data_block_number(ChainDataIndex::PendingBlockNumber)
1292            .await
1293    }
1294
1295    /// DB mutation step of `forkchoice_update`.
1296    ///
1297    /// Callers MUST hold `fcu_lock` (only `forkchoice_update` should invoke this).
1298    /// The read of `LatestBlockNumber` below happens outside the write
1299    /// transaction and would be a TOCTOU window without that serialization.
1300    /// Applies a fork-choice update atomically: writes canonical hashes, latest/safe/
1301    /// finalized numbers, and prunes the state-history journal up to the new finalized.
1302    ///
1303    /// **Concurrency:** This function must be called with exclusive access. In
1304    /// production it is gated by `fcu_lock` from [`forkchoice_update`]. The
1305    /// pre-finalized read used to decide journal pruning happens outside the write
1306    /// transaction; without serialization, two concurrent callers could race on
1307    /// `prev_finalized` and double-prune. Tests that call this directly must not
1308    /// run concurrently with another FCU.
1309    async fn forkchoice_update_inner(
1310        &self,
1311        new_canonical_blocks: Vec<(BlockNumber, BlockHash)>,
1312        head_number: BlockNumber,
1313        head_hash: BlockHash,
1314        safe: Option<BlockNumber>,
1315        finalized: Option<BlockNumber>,
1316    ) -> Result<(), StoreError> {
1317        let latest = self.load_latest_block_number().await?.unwrap_or(0);
1318        let db = self.backend.clone();
1319        let journal_pruning_paused = self.journal_pruning_paused.clone();
1320        tokio::task::spawn_blocking(move || {
1321            let mut txn = db.begin_write()?;
1322
1323            for (block_number, block_hash) in new_canonical_blocks {
1324                let head_key = block_number.to_le_bytes();
1325                let head_value = block_hash.encode_to_vec();
1326                txn.put(CANONICAL_BLOCK_HASHES, &head_key, &head_value)?;
1327            }
1328
1329            // Delete canonical entries above the new head by enumerating each key.
1330            // `delete_range` is not safe here: keys are `u64::to_le_bytes()`, and
1331            // RocksDB's lexicographic comparator does not match LE numeric order
1332            // (e.g. block 256 = [0x00, 0x01, ..] sorts before block 11 = [0x0B, ..]),
1333            // so a range-delete would silently miss blocks whose LE first byte is
1334            // smaller than `head+1`'s first byte.
1335            for number in (head_number + 1)..=(latest) {
1336                txn.delete(CANONICAL_BLOCK_HASHES, number.to_le_bytes().as_slice())?;
1337            }
1338
1339            // Make head canonical
1340            let head_key = head_number.to_le_bytes();
1341            let head_value = head_hash.encode_to_vec();
1342            txn.put(CANONICAL_BLOCK_HASHES, &head_key, &head_value)?;
1343
1344            // Update chain data
1345            let latest_key = chain_data_key(ChainDataIndex::LatestBlockNumber);
1346            txn.put(CHAIN_DATA, &latest_key, &head_number.to_le_bytes())?;
1347
1348            if let Some(safe) = safe {
1349                let safe_key = chain_data_key(ChainDataIndex::SafeBlockNumber);
1350                txn.put(CHAIN_DATA, &safe_key, &safe.to_le_bytes())?;
1351            }
1352
1353            if let Some(finalized) = finalized {
1354                let finalized_key = chain_data_key(ChainDataIndex::FinalizedBlockNumber);
1355
1356                // Read the previous finalized number from the same backend before we
1357                // overwrite it. The journal can only be pruned when finality actually
1358                // advances; a no-op or backwards FCU must not touch STATE_HISTORY.
1359                // Pre-merge or fresh chains have no entry; treat as 0.
1360                //
1361                // A length mismatch is treated as a hard error rather than a silent
1362                // fallback to 0: if a future schema change stores this field with a
1363                // different width, the silent default would make `finalized > 0` true
1364                // for every FCU and prune the entire journal. Bail out instead.
1365                let prev_finalized = match db.begin_read()?.get(CHAIN_DATA, &finalized_key)? {
1366                    Some(bytes) => {
1367                        let arr: [u8; 8] = bytes.as_slice().try_into().map_err(|_| {
1368                            StoreError::Custom(format!(
1369                                "FinalizedBlockNumber has unexpected length {} (want 8)",
1370                                bytes.len()
1371                            ))
1372                        })?;
1373                        BlockNumber::from_le_bytes(arr)
1374                    }
1375                    None => 0,
1376                };
1377
1378                txn.put(CHAIN_DATA, &finalized_key, &finalized.to_le_bytes())?;
1379
1380                // Prune every STATE_HISTORY entry at or below the new finalized number
1381                // in the same atomic txn. `delete_range` is half-open `[start, end)`,
1382                // so `end = finalized + 1`. STATE_HISTORY uses big-endian keys, so
1383                // lexicographic byte order matches numeric order.
1384                //
1385                // Skipped while a deep-reorg apply pass is in flight
1386                // (`journal_pruning_paused`): `Overlay::from_journal` reads entries
1387                // with no snapshot isolation, so pruning mid-construction fails it
1388                // with a spurious `MissingEntry`. The finalized-number update above
1389                // still lands; pruning catches up on the next advance after the
1390                // pass ends because `delete_range` is cumulative from zero.
1391                if finalized > prev_finalized
1392                    && !journal_pruning_paused.load(std::sync::atomic::Ordering::Acquire)
1393                {
1394                    let start = 0u64.to_be_bytes();
1395                    let end = finalized.saturating_add(1).to_be_bytes();
1396                    txn.delete_range(STATE_HISTORY, &start, &end)?;
1397                }
1398            }
1399
1400            txn.commit()
1401        })
1402        .await
1403        .map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
1404    }
1405
1406    pub async fn get_receipts_for_block(
1407        &self,
1408        block_hash: &BlockHash,
1409    ) -> Result<Vec<Receipt>, StoreError> {
1410        self.get_receipts_for_block_from_index(block_hash, 0, None)
1411            .await
1412    }
1413
1414    /// Retrieves receipts for a block starting from the given index,
1415    /// optionally limited to `max_count` receipts.
1416    ///
1417    /// Uses cursor-based prefix iteration over the 32-byte block hash prefix
1418    /// for efficient batch retrieval. Used by:
1419    /// - eth/70 partial receipt requests (EIP-7975) via p2p
1420    /// - `eth_getTransactionReceipt` RPC with a count limit to avoid
1421    ///   fetching the entire block's receipts
1422    pub async fn get_receipts_for_block_from_index(
1423        &self,
1424        block_hash: &BlockHash,
1425        start_index: u64,
1426        max_count: Option<usize>,
1427    ) -> Result<Vec<Receipt>, StoreError> {
1428        if let Some(all) = self.buffer()?.get_receipts(block_hash) {
1429            let start = start_index as usize;
1430            let slice = all.into_iter().skip(start);
1431            return Ok(match max_count {
1432                Some(max) => slice.take(max).collect(),
1433                None => slice.collect(),
1434            });
1435        }
1436        let backend = self.backend.clone();
1437        let block_hash = *block_hash;
1438
1439        tokio::task::spawn_blocking(move || {
1440            let txn = backend.begin_read()?;
1441            let prefix = block_hash.as_bytes().to_vec();
1442            // Seek directly to block_hash || start_index to avoid O(start_index) scan.
1443            // Keys are big-endian u64, so lexicographic order matches numeric order.
1444            let mut seek_key = prefix.clone();
1445            seek_key.extend_from_slice(&start_index.to_be_bytes());
1446            let iter = txn.prefix_iterator(RECEIPTS_V2, &seek_key)?;
1447            let mut receipts = Vec::new();
1448            for result in iter {
1449                let (k, v) = result?;
1450                if !k.starts_with(&prefix) {
1451                    break;
1452                }
1453                if k.len() != 40 {
1454                    continue;
1455                }
1456                receipts.push(Receipt::decode_storage(v.as_ref())?);
1457                if let Some(max) = max_count
1458                    && receipts.len() >= max
1459                {
1460                    break;
1461                }
1462            }
1463            Ok(receipts)
1464        })
1465        .await
1466        .map_err(|e| StoreError::Custom(format!("Task panicked: {e}")))?
1467    }
1468
1469    // Snap State methods
1470
1471    /// Sets the hash of the last header downloaded during a snap sync
1472    pub async fn set_header_download_checkpoint(
1473        &self,
1474        block_hash: BlockHash,
1475    ) -> Result<(), StoreError> {
1476        let key = snap_state_key(SnapStateIndex::HeaderDownloadCheckpoint);
1477        let value = block_hash.encode_to_vec();
1478        self.write_async(SNAP_STATE, key, value).await
1479    }
1480
1481    /// Gets the hash of the last header downloaded during a snap sync
1482    pub async fn get_header_download_checkpoint(&self) -> Result<Option<BlockHash>, StoreError> {
1483        let key = snap_state_key(SnapStateIndex::HeaderDownloadCheckpoint);
1484        self.backend
1485            .begin_read()?
1486            .get(SNAP_STATE, &key)?
1487            .map(|bytes| H256::decode(bytes.as_slice()))
1488            .transpose()
1489            .map_err(StoreError::from)
1490    }
1491
1492    /// The `forkchoice_update` and `new_payload` methods require the `latest_valid_hash`
1493    /// when processing an invalid payload. To provide this, we must track invalid chains.
1494    ///
1495    /// We only store the last known valid head upon encountering a bad block,
1496    /// rather than tracking every subsequent invalid block.
1497    pub async fn set_latest_valid_ancestor(
1498        &self,
1499        bad_block: BlockHash,
1500        latest_valid: BlockHash,
1501    ) -> Result<(), StoreError> {
1502        let value = latest_valid.encode_to_vec();
1503        self.write_async(INVALID_CHAINS, bad_block.as_bytes().to_vec(), value)
1504            .await
1505    }
1506
1507    /// Returns the latest valid ancestor hash for a given invalid block hash.
1508    /// Used to provide `latest_valid_hash` in the Engine API when processing invalid payloads.
1509    pub async fn get_latest_valid_ancestor(
1510        &self,
1511        block: BlockHash,
1512    ) -> Result<Option<BlockHash>, StoreError> {
1513        self.read_async(INVALID_CHAINS, block.as_bytes().to_vec())
1514            .await?
1515            .map(|bytes| H256::decode(bytes.as_slice()))
1516            .transpose()
1517            .map_err(StoreError::from)
1518    }
1519
1520    /// Records a block that failed validation so it can be served by
1521    /// `debug_getBadBlocks`. The list is bounded to [`MAX_BAD_BLOCKS`] entries,
1522    /// kept sorted by descending block number, with the oldest dropped once the
1523    /// bound is exceeded. Duplicate `(number, hash)` entries are ignored.
1524    pub async fn add_bad_block(&self, block: Block) -> Result<(), StoreError> {
1525        let mut bad_blocks = self.get_bad_blocks().await?;
1526        let block_number = block.header.number;
1527        let block_hash = block.hash();
1528        if bad_blocks
1529            .iter()
1530            .any(|b| b.header.number == block_number && b.hash() == block_hash)
1531        {
1532            return Ok(());
1533        }
1534        bad_blocks.push(block);
1535        bad_blocks.sort_by(|a, b| b.header.number.cmp(&a.header.number));
1536        bad_blocks.truncate(MAX_BAD_BLOCKS);
1537        self.write_async(
1538            BAD_BLOCKS,
1539            BAD_BLOCKS_KEY.to_vec(),
1540            bad_blocks.encode_to_vec(),
1541        )
1542        .await
1543    }
1544
1545    /// Returns the recent bad blocks seen by the client, sorted by descending
1546    /// block number. Used by `debug_getBadBlocks`.
1547    pub async fn get_bad_blocks(&self) -> Result<Vec<Block>, StoreError> {
1548        match self.read_async(BAD_BLOCKS, BAD_BLOCKS_KEY.to_vec()).await? {
1549            Some(bytes) => Vec::<Block>::decode(&bytes).map_err(StoreError::from),
1550            None => Ok(Vec::new()),
1551        }
1552    }
1553
1554    /// Obtain block number for a given hash
1555    pub fn get_block_number_sync(
1556        &self,
1557        block_hash: BlockHash,
1558    ) -> Result<Option<BlockNumber>, StoreError> {
1559        if let Some(n) = self.buffer()?.get_number(&block_hash) {
1560            return Ok(Some(n));
1561        }
1562        let txn = self.backend.begin_read()?;
1563        txn.get(BLOCK_NUMBERS, &block_hash.encode_to_vec())?
1564            .map(decode_block_number)
1565            .transpose()
1566    }
1567
1568    /// Get the canonical block hash for a given block number.
1569    pub fn get_canonical_block_hash_sync(
1570        &self,
1571        block_number: BlockNumber,
1572    ) -> Result<Option<BlockHash>, StoreError> {
1573        let last = self.latest_block_header.get();
1574        if last.number == block_number {
1575            return Ok(Some(last.hash()));
1576        }
1577        let txn = self.backend.begin_read()?;
1578        txn.get(
1579            CANONICAL_BLOCK_HASHES,
1580            block_number.to_le_bytes().as_slice(),
1581        )?
1582        .map(|bytes| H256::decode(bytes.as_slice()))
1583        .transpose()
1584        .map_err(StoreError::from)
1585    }
1586
1587    /// CAUTION: This method writes directly to the underlying database, bypassing any caching layer.
1588    /// For updating the state after block execution, use [`Self::store_block_updates`].
1589    pub async fn write_storage_trie_nodes_batch(
1590        &self,
1591        storage_trie_nodes: StorageUpdates,
1592    ) -> Result<(), StoreError> {
1593        let mut txn = self.backend.begin_write()?;
1594        tokio::task::spawn_blocking(move || {
1595            for (address_hash, nodes) in storage_trie_nodes {
1596                for (node_path, node_data) in nodes {
1597                    let key = apply_prefix(Some(address_hash), node_path);
1598                    if node_data.is_empty() {
1599                        txn.delete(STORAGE_TRIE_NODES, key.as_ref())?;
1600                    } else {
1601                        txn.put(STORAGE_TRIE_NODES, key.as_ref(), &node_data)?;
1602                    }
1603                }
1604            }
1605            txn.commit()
1606        })
1607        .await
1608        .map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
1609    }
1610
1611    /// CAUTION: This method writes directly to the underlying database, bypassing any caching layer.
1612    /// For updating the state after block execution, use [`Self::store_block_updates`].
1613    pub async fn write_account_code_batch(
1614        &self,
1615        account_codes: Vec<(H256, Code)>,
1616    ) -> Result<(), StoreError> {
1617        let mut code_batch_items = Vec::new();
1618        let mut metadata_batch_items = Vec::new();
1619
1620        for (code_hash, code) in account_codes {
1621            let buf = encode_code(&code);
1622            let metadata_buf = (code.len() as u64).to_be_bytes().to_vec();
1623            code_batch_items.push((code_hash.as_bytes().to_vec(), buf));
1624            metadata_batch_items.push((code_hash.as_bytes().to_vec(), metadata_buf));
1625        }
1626
1627        // Write both batches
1628        self.write_batch_async(ACCOUNT_CODES, code_batch_items)
1629            .await?;
1630        self.write_batch_async(ACCOUNT_CODE_METADATA, metadata_batch_items)
1631            .await
1632    }
1633
1634    /// Returns a snapshot of the current block-data buffer.
1635    fn buffer(&self) -> Result<Arc<BlockDataBuffer>, StoreError> {
1636        Ok(self
1637            .block_data_buffer
1638            .read()
1639            .map_err(|_| StoreError::LockError)?
1640            .clone())
1641    }
1642
1643    // Helper methods for async operations with spawn_blocking
1644    // These methods ensure RocksDB I/O doesn't block the tokio runtime
1645
1646    /// Helper method for async writes
1647    /// Spawns blocking task to avoid blocking tokio runtime
1648    pub fn write(
1649        &self,
1650        table: &'static str,
1651        key: Vec<u8>,
1652        value: Vec<u8>,
1653    ) -> Result<(), StoreError> {
1654        let backend = self.backend.clone();
1655        let mut txn = backend.begin_write()?;
1656        txn.put(table, &key, &value)?;
1657        txn.commit()
1658    }
1659
1660    /// Helper method for async writes
1661    /// Spawns blocking task to avoid blocking tokio runtime
1662    async fn write_async(
1663        &self,
1664        table: &'static str,
1665        key: Vec<u8>,
1666        value: Vec<u8>,
1667    ) -> Result<(), StoreError> {
1668        let backend = self.backend.clone();
1669
1670        tokio::task::spawn_blocking(move || {
1671            let mut txn = backend.begin_write()?;
1672            txn.put(table, &key, &value)?;
1673            txn.commit()
1674        })
1675        .await
1676        .map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
1677    }
1678
1679    /// Helper method for async reads
1680    /// Spawns blocking task to avoid blocking tokio runtime
1681    pub async fn read_async(
1682        &self,
1683        table: &'static str,
1684        key: Vec<u8>,
1685    ) -> Result<Option<Vec<u8>>, StoreError> {
1686        let backend = self.backend.clone();
1687
1688        tokio::task::spawn_blocking(move || {
1689            let txn = backend.begin_read()?;
1690            txn.get(table, &key)
1691        })
1692        .await
1693        .map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
1694    }
1695
1696    /// Helper method for sync reads
1697    /// Spawns blocking task to avoid blocking tokio runtime
1698    pub fn read(&self, table: &'static str, key: Vec<u8>) -> Result<Option<Vec<u8>>, StoreError> {
1699        let backend = self.backend.clone();
1700        let txn = backend.begin_read()?;
1701        txn.get(table, &key)
1702    }
1703
1704    /// Helper method for batch writes
1705    /// Spawns blocking task to avoid blocking tokio runtime
1706    /// This is the most important optimization for healing performance
1707    pub async fn write_batch_async(
1708        &self,
1709        table: &'static str,
1710        batch_ops: Vec<(Vec<u8>, Vec<u8>)>,
1711    ) -> Result<(), StoreError> {
1712        let backend = self.backend.clone();
1713
1714        tokio::task::spawn_blocking(move || {
1715            let mut txn = backend.begin_write()?;
1716            txn.put_batch(table, batch_ops)?;
1717            txn.commit()
1718        })
1719        .await
1720        .map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
1721    }
1722
1723    /// Helper method for batch writes
1724    pub fn write_batch(
1725        &self,
1726        table: &'static str,
1727        batch_ops: Vec<(Vec<u8>, Vec<u8>)>,
1728    ) -> Result<(), StoreError> {
1729        let backend = self.backend.clone();
1730        let mut txn = backend.begin_write()?;
1731        txn.put_batch(table, batch_ops)?;
1732        txn.commit()
1733    }
1734
1735    pub async fn add_fullsync_batch(&self, headers: Vec<BlockHeader>) -> Result<(), StoreError> {
1736        self.write_batch_async(
1737            FULLSYNC_HEADERS,
1738            headers
1739                .into_iter()
1740                .map(|header| (header.number.to_le_bytes().to_vec(), header.encode_to_vec()))
1741                .collect(),
1742        )
1743        .await
1744    }
1745
1746    pub async fn read_fullsync_batch(
1747        &self,
1748        start: BlockNumber,
1749        limit: u64,
1750    ) -> Result<Vec<Option<BlockHeader>>, StoreError> {
1751        let mut res = vec![];
1752        let read_tx = self.backend.begin_read()?;
1753        // TODO: use read_bulk here
1754        for key in start..start + limit {
1755            let header_opt = read_tx
1756                .get(FULLSYNC_HEADERS, &key.to_le_bytes())?
1757                .map(|header| BlockHeader::decode(&header))
1758                .transpose()?;
1759            res.push(header_opt);
1760        }
1761        Ok(res)
1762    }
1763
1764    pub async fn clear_fullsync_headers(&self) -> Result<(), StoreError> {
1765        self.backend.clear_table(FULLSYNC_HEADERS)
1766    }
1767
1768    /// Delete a key from a table
1769    pub fn delete(&self, table: &'static str, key: Vec<u8>) -> Result<(), StoreError> {
1770        let mut txn = self.backend.begin_write()?;
1771        txn.delete(table, &key)?;
1772        txn.commit()
1773    }
1774
1775    pub fn store_block_updates(&self, update_batch: UpdateBatch) -> Result<(), StoreError> {
1776        self.apply_updates(update_batch)
1777    }
1778
1779    /// Compute `(parent_state_root, last_state_root, last_block_number,
1780    /// last_block_hash)` for a batch's trie update: the state root of the first
1781    /// block's parent, the last block's own state root, and the last block's
1782    /// number and hash (the identity the journal records for the committed
1783    /// layer). Used by `apply_updates` for both the live and full-sync paths
1784    /// (which share the single persist worker).
1785    fn batch_state_roots(
1786        &self,
1787        update_batch: &UpdateBatch,
1788    ) -> Result<(H256, H256, BlockNumber, H256), StoreError> {
1789        let parent_state_root = self
1790            .get_block_header_by_hash(
1791                update_batch
1792                    .blocks
1793                    .first()
1794                    .ok_or(StoreError::UpdateBatchNoBlocks)?
1795                    .header
1796                    .parent_hash,
1797            )?
1798            .map(|header| header.state_root)
1799            .unwrap_or_default();
1800        let last_block = update_batch
1801            .blocks
1802            .last()
1803            .ok_or(StoreError::UpdateBatchNoBlocks)?;
1804        let last_state_root = last_block.header.state_root;
1805        let last_block_number = last_block.header.number;
1806        let last_block_hash = last_block.hash();
1807        Ok((
1808            parent_state_root,
1809            last_state_root,
1810            last_block_number,
1811            last_block_hash,
1812        ))
1813    }
1814
1815    /// Single path for all updates: hand the whole unit (block data + one aggregate trie
1816    /// diff) to the SINGLE persist worker and wait for its ack. `commit_depth` selects the
1817    /// commit gate; `wait_for_flush` selects when the worker acks (see [`UpdateBatch`]).
1818    fn apply_updates(&self, update_batch: UpdateBatch) -> Result<(), StoreError> {
1819        let (parent_state_root, last_state_root, last_block_number, last_block_hash) =
1820            self.batch_state_roots(&update_batch)?;
1821
1822        let UpdateBatch {
1823            account_updates,
1824            storage_updates,
1825            blocks,
1826            receipts,
1827            code_updates,
1828            commit_depth,
1829            wait_for_flush,
1830        } = update_batch;
1831
1832        // Register before handing off to the worker and before this returns, so
1833        // any reader opening this root blocks in `gated_snapshot` until the
1834        // layer is installed rather than snapshotting a stale cache.
1835        self.pending_trie_roots.register(last_state_root)?;
1836
1837        // Pair blocks with receipts. Single-block fast path avoids a HashMap
1838        // allocation; full-sync batch joins by hash.
1839        let blocks_with_receipts: Vec<(Block, Vec<Receipt>)> = if blocks.len() == 1 {
1840            let block = blocks.into_iter().next().expect("len == 1");
1841            let hash = block.hash();
1842            let r = receipts
1843                .into_iter()
1844                .find(|(h, _)| *h == hash)
1845                .map(|(_, r)| r)
1846                .unwrap_or_default();
1847            vec![(block, r)]
1848        } else {
1849            let mut receipts_by_hash: std::collections::HashMap<BlockHash, Vec<Receipt>> =
1850                receipts.into_iter().collect();
1851            blocks
1852                .into_iter()
1853                .map(|b| {
1854                    let r = receipts_by_hash.remove(&b.hash()).unwrap_or_default();
1855                    (b, r)
1856                })
1857                .collect()
1858        };
1859
1860        // Send to the persist worker and wait for its ack.
1861        // wait_for_flush=false: worker acks after staging; the ack carries the PRIOR flush
1862        //   result so a disk error surfaces on the next call.
1863        // wait_for_flush=true: worker acks after flush, bounding in-flight work to ~1.
1864        let (ack_tx, ack_rx) = sync_channel(1);
1865        self.persist_tx
1866            .send(PersistMessage::Block(Box::new(BlockPersist {
1867                blocks: blocks_with_receipts,
1868                codes: code_updates,
1869                parent_state_root,
1870                child_state_root: last_state_root,
1871                account_updates,
1872                storage_updates,
1873                commit_depth,
1874                wait_for_flush,
1875                block_number: last_block_number,
1876                block_hash: last_block_hash,
1877                ack: ack_tx,
1878            })))
1879            .map_err(|e| StoreError::Custom(format!("failed to send block persist: {e}")))?;
1880        ack_rx
1881            .recv()
1882            .map_err(|e| StoreError::Custom(format!("block persist ack failed: {e}")))??;
1883
1884        Ok(())
1885    }
1886
1887    /// Opens (or creates) a store at `path` with the default [`StoreConfig`].
1888    ///
1889    /// Production callers that need to override storage tunables (e.g. the RocksDB
1890    /// block cache size from a CLI option) should use [`Store::new_with_config`].
1891    pub fn new(path: impl AsRef<Path>, engine_type: EngineType) -> Result<Self, StoreError> {
1892        Self::new_with_config(path, engine_type, StoreConfig::default())
1893    }
1894
1895    /// Opens (or creates) a store at `path`, applying the supplied [`StoreConfig`].
1896    pub fn new_with_config(
1897        path: impl AsRef<Path>,
1898        engine_type: EngineType,
1899        // `config` only feeds the RocksDB backend; without that feature it is unused.
1900        #[cfg_attr(not(feature = "rocksdb"), allow(unused_variables))] config: StoreConfig,
1901    ) -> Result<Self, StoreError> {
1902        let db_path = path.as_ref().to_path_buf();
1903
1904        if engine_type != EngineType::InMemory {
1905            let version = read_store_schema_version(&db_path)?;
1906
1907            match version {
1908                None if db_path.exists() && dir_contains_legacy_db(&db_path)? => {
1909                    // Pre-metadata DB — cannot migrate safely
1910                    return Err(StoreError::NotFoundDBVersion);
1911                }
1912                None => {
1913                    // No metadata and no recognizable database files. The directory
1914                    // may still hold unrelated files (e.g. a JWT secret placed in the
1915                    // datadir by tooling such as EthDocker, see issue #5680), so treat
1916                    // this as a fresh datadir and write the initial metadata instead
1917                    // of erroring out.
1918                    init_metadata_file(&db_path)?;
1919                }
1920                Some(v) if v < 1 => {
1921                    return Err(StoreError::MigrationFailed {
1922                        from: v,
1923                        to: STORE_SCHEMA_VERSION,
1924                        reason: format!("DB version v{v} is invalid (predates migrations)"),
1925                    });
1926                }
1927                Some(v) if v > STORE_SCHEMA_VERSION => {
1928                    return Err(StoreError::MigrationFailed {
1929                        from: v,
1930                        to: STORE_SCHEMA_VERSION,
1931                        reason: format!(
1932                            "DB version v{v} is more recent than the client expects (v{STORE_SCHEMA_VERSION}). Rolling back is not supported"
1933                        ),
1934                    });
1935                }
1936                #[cfg(feature = "rocksdb")]
1937                Some(v) if v < STORE_SCHEMA_VERSION => {
1938                    // Open backend, run migrations, then drop obsolete CFs.
1939                    // Cleanup must happen AFTER migrations so legacy CFs (e.g.
1940                    // `receipts`) are still readable during the migration.
1941                    let rocksdb = Arc::new(RocksDBBackend::open(
1942                        &path,
1943                        config.rocksdb_block_cache_size,
1944                    )?);
1945                    crate::migrations::run_pending_migrations(rocksdb.as_ref(), &db_path, v)?;
1946                    rocksdb.drop_obsolete_cfs(&path);
1947                    let backend: Arc<dyn crate::api::StorageBackend> = rocksdb;
1948                    return Self::from_backend(
1949                        backend,
1950                        db_path,
1951                        DB_COMMIT_THRESHOLD,
1952                        config.persist_channel_capacity,
1953                    );
1954                }
1955                Some(_) => {
1956                    // version == STORE_SCHEMA_VERSION, proceed normally.
1957                    // Without the `rocksdb` feature this also covers v < target,
1958                    // but that path is unreachable since InMemory is the only
1959                    // engine type and the outer guard excludes it.
1960                }
1961            }
1962        }
1963
1964        match engine_type {
1965            #[cfg(feature = "rocksdb")]
1966            EngineType::RocksDB => {
1967                let rocksdb = RocksDBBackend::open(&path, config.rocksdb_block_cache_size)?;
1968                rocksdb.drop_obsolete_cfs(&path);
1969                let backend: Arc<dyn StorageBackend> = Arc::new(rocksdb);
1970                Self::from_backend(
1971                    backend,
1972                    db_path,
1973                    DB_COMMIT_THRESHOLD,
1974                    config.persist_channel_capacity,
1975                )
1976            }
1977            EngineType::InMemory => {
1978                let backend = Arc::new(InMemoryBackend::open()?);
1979                Self::from_backend(
1980                    backend,
1981                    db_path,
1982                    IN_MEMORY_COMMIT_THRESHOLD,
1983                    config.persist_channel_capacity,
1984                )
1985            }
1986        }
1987    }
1988
1989    fn from_backend(
1990        backend: Arc<dyn StorageBackend>,
1991        db_path: PathBuf,
1992        commit_threshold: usize,
1993        persist_channel_capacity: usize,
1994    ) -> Result<Self, StoreError> {
1995        debug!("Initializing Store with {commit_threshold} in-memory diff-layers");
1996        let (fkv_tx, fkv_rx) = std::sync::mpsc::sync_channel(0);
1997        let persist_cap = persist_channel_capacity.max(1); // clamp: 0 would be a rendezvous channel
1998        let (persist_tx, persist_rx) = std::sync::mpsc::sync_channel(persist_cap);
1999
2000        let (last_written, initial_flushed_upto) = {
2001            let tx = backend.begin_read()?;
2002            let last_written = tx
2003                .get(MISC_VALUES, "last_written".as_bytes())?
2004                .unwrap_or_else(|| vec![0u8; 64]);
2005            let last_written = if last_written == [0xff] {
2006                vec![0xff; 64]
2007            } else {
2008                last_written
2009            };
2010            let initial_flushed_upto = match tx.get(MISC_VALUES, FLUSHED_UPTO_KEY)? {
2011                Some(bytes) => decode_flushed_upto(&bytes)?,
2012                None => 0,
2013            };
2014            (last_written, initial_flushed_upto)
2015        };
2016        let mut initial_buffer = BlockDataBuffer::new();
2017        initial_buffer.set_flushed_upto(initial_flushed_upto);
2018
2019        let mut background_threads = Vec::new();
2020        let safe_commit_root = Arc::new(RwLock::new(H256::zero()));
2021        let mut store = Self {
2022            db_path,
2023            backend,
2024            chain_config: Default::default(),
2025            latest_block_header: Default::default(),
2026            trie_cache: Arc::new(RwLock::new(Arc::new(TrieLayerCache::new_with_safe_commit(
2027                commit_threshold,
2028                safe_commit_root.clone(),
2029            )))),
2030            flatkeyvalue_control_tx: fkv_tx,
2031            block_data_buffer: Arc::new(RwLock::new(Arc::new(initial_buffer))),
2032            persist_tx,
2033            pending_trie_roots: Arc::new(PendingTrieRoots::default()),
2034            last_computed_flatkeyvalue: Arc::new(RwLock::new(last_written)),
2035            account_code_cache: Arc::new(Mutex::new(CodeCache::default())),
2036            code_metadata_cache: Arc::new(Mutex::new(rustc_hash::FxHashMap::default())),
2037            fcu_lock: Arc::new(tokio::sync::Mutex::new(())),
2038            safe_commit_root,
2039            journal_pruning_paused: Arc::new(std::sync::atomic::AtomicBool::new(false)),
2040            background_threads: Default::default(),
2041        };
2042        let backend_clone = store.backend.clone();
2043        let last_computed_fkv = store.last_computed_flatkeyvalue.clone();
2044        background_threads.push(std::thread::spawn(move || {
2045            let rx = fkv_rx;
2046            // Wait for the first Continue to start generation
2047            loop {
2048                match rx.recv() {
2049                    Ok(FKVGeneratorControlMessage::Continue) => break,
2050                    Ok(FKVGeneratorControlMessage::Stop) => {}
2051                    Err(std::sync::mpsc::RecvError) => {
2052                        debug!("Closing FlatKeyValue generator.");
2053                        return;
2054                    }
2055                }
2056            }
2057
2058            let _ = flatkeyvalue_generator(&backend_clone, &last_computed_fkv, &rx)
2059                .inspect_err(|err| error!("Error while generating FlatKeyValue: {err}"));
2060        }));
2061        // The single persist worker: sole swapper of `block_data_buffer`, sole
2062        // builder of trie diff-layers. One DB transaction per `Block` message.
2063        let persist_backend = store.backend.clone();
2064        let persist_buffer = store.block_data_buffer.clone();
2065        let persist_trie_cache = store.trie_cache.clone();
2066        let persist_pending_roots = store.pending_trie_roots.clone();
2067        let persist_fkv_ctl = store.flatkeyvalue_control_tx.clone();
2068        background_threads.push(std::thread::spawn(move || {
2069            let rx = persist_rx;
2070            // Carries the prior flush result: the live path acks after staging,
2071            // so a disk failure surfaces on the next message's ack.
2072            let mut last_flush_result: Result<(), StoreError> = Ok(());
2073            loop {
2074                match rx.recv() {
2075                    Ok(PersistMessage::Block(bp)) => {
2076                        let bp = *bp;
2077                        // Stage block data (sole swapper of the buffer; codes
2078                        // are batch-level and attributed to the first block).
2079                        let staged = mutate_block_buffer(&persist_buffer, move |b| {
2080                            let mut codes = Some(bp.codes);
2081                            for (block, receipts) in bp.blocks {
2082                                b.insert(block, receipts, codes.take().unwrap_or_default());
2083                            }
2084                        });
2085                        if let Err(e) = staged {
2086                            // Stage failure is terminal for this message.
2087                            // Clear the pending root so gated readers are not
2088                            // blocked forever (apply_trie_phase1, which normally
2089                            // does this, is skipped when we continue here).
2090                            persist_pending_roots.clear(bp.child_state_root);
2091                            let _ = bp.ack.send(Err(e));
2092                            continue;
2093                        }
2094                        // ACK-AFTER-STAGING: ack now, carrying the prior flush result.
2095                        // NOTE: this acks block validity BEFORE apply_trie_phase1
2096                        // installs the trie layer below. A phase-1 failure (only
2097                        // reachable via lock poisoning, which is already fatal) is
2098                        // therefore deferred to the next block's ack via
2099                        // last_flush_result rather than attributed to this block;
2100                        // the pending root is still cleared unconditionally, so
2101                        // gated readers error rather than hang.
2102                        if !bp.wait_for_flush {
2103                            let _ = bp
2104                                .ack
2105                                .send(std::mem::replace(&mut last_flush_result, Ok(())));
2106                        }
2107                        // Build + install the trie layer; clear the read gate.
2108                        if let Err(err) = apply_trie_phase1(
2109                            &persist_trie_cache,
2110                            &persist_pending_roots,
2111                            bp.parent_state_root,
2112                            bp.child_state_root,
2113                            bp.block_number,
2114                            bp.block_hash,
2115                            bp.account_updates,
2116                            bp.storage_updates,
2117                        ) {
2118                            error!("persist worker trie phase-1 failed: {err}");
2119                            if bp.wait_for_flush {
2120                                let _ = bp.ack.send(Err(err));
2121                            } else {
2122                                last_flush_result = Err(err);
2123                            }
2124                            continue;
2125                        }
2126                        // Flush block data + commit bottom trie layer when due.
2127                        let flushed = flush_block_data(persist_backend.as_ref(), &persist_buffer)
2128                            .inspect_err(|err| error!("flush_block_data failed: {err}"))
2129                            .and_then(|_| {
2130                                commit_trie_if_due(
2131                                    persist_backend.as_ref(),
2132                                    &persist_trie_cache,
2133                                    &persist_fkv_ctl,
2134                                    bp.parent_state_root,
2135                                    bp.commit_depth,
2136                                    bp.wait_for_flush,
2137                                )
2138                            });
2139                        // ACK-AFTER-FLUSH: ack now (bounds in-flight work to ~1), folding in
2140                        // any prior deferred error. ACK-AFTER-STAGING: stash for the next ack.
2141                        if bp.wait_for_flush {
2142                            let prior = std::mem::replace(&mut last_flush_result, Ok(()));
2143                            let _ = bp.ack.send(prior.and(flushed));
2144                        } else {
2145                            last_flush_result = flushed;
2146                        }
2147                    }
2148                    Ok(PersistMessage::Commit(root)) => match persist_trie_cache.read() {
2149                        Ok(guard) => {
2150                            let trie = guard.clone();
2151                            drop(guard);
2152                            // Forkchoice-driven flush is the live (non-batch) path, so
2153                            // journaling is enabled: pass `is_batch = false`.
2154                            let _ = commit_to_disk(
2155                                persist_backend.as_ref(),
2156                                &persist_fkv_ctl,
2157                                &persist_trie_cache,
2158                                &trie,
2159                                root,
2160                                false,
2161                            )
2162                            .inspect_err(|err| error!("commit_to_disk failed: {err}"));
2163                        }
2164                        Err(_) => error!("trie cache lock poisoned during commit"),
2165                    },
2166                    Ok(PersistMessage::Ping(ack)) => {
2167                        // Idle handshake: reached only after all earlier Block
2168                        // messages are fully processed. Carry the pending flush
2169                        // result so a live-path failure is not silently dropped.
2170                        let _ = ack.send(std::mem::replace(&mut last_flush_result, Ok(())));
2171                    }
2172                    Ok(PersistMessage::Shutdown { ack }) => {
2173                        // Graceful shutdown: drain (already guaranteed by FIFO) and
2174                        // force-flush the not-yet-flushed block-data tail. The trie
2175                        // diff-layers stay in memory and are dropped on exit: the
2176                        // on-disk trie is a single-version path store, so committing
2177                        // the non-finalized tail would make a post-restart reorg
2178                        // unrecoverable. Those layers re-execute on the next start.
2179                        let result = flush_block_data(persist_backend.as_ref(), &persist_buffer);
2180                        let prior = std::mem::replace(&mut last_flush_result, Ok(()));
2181                        let _ = ack.send(prior.and(result));
2182                        // No more work will follow a shutdown request.
2183                        return;
2184                    }
2185                    Err(_) => return,
2186                }
2187            }
2188        }));
2189        store.background_threads = Arc::new(ThreadList {
2190            list: background_threads,
2191        });
2192        Ok(store)
2193    }
2194
2195    /// Opens (or creates) a store at `store_path` and seeds it from the
2196    /// given genesis file, using the default [`StoreConfig`].
2197    pub async fn new_from_genesis(
2198        store_path: &Path,
2199        engine_type: EngineType,
2200        genesis_path: &str,
2201    ) -> Result<Self, StoreError> {
2202        Self::new_from_genesis_with_config(
2203            store_path,
2204            engine_type,
2205            genesis_path,
2206            StoreConfig::default(),
2207        )
2208        .await
2209    }
2210
2211    /// Opens (or creates) a store at `store_path` from genesis, applying the
2212    /// supplied [`StoreConfig`].
2213    pub async fn new_from_genesis_with_config(
2214        store_path: &Path,
2215        engine_type: EngineType,
2216        genesis_path: &str,
2217        config: StoreConfig,
2218    ) -> Result<Self, StoreError> {
2219        let file = std::fs::File::open(genesis_path)
2220            .map_err(|error| StoreError::Custom(format!("Failed to open genesis file: {error}")))?;
2221        let reader = std::io::BufReader::new(file);
2222        let genesis: Genesis = serde_json::from_reader(reader)
2223            .map_err(|e| StoreError::Custom(format!("Failed to deserialize genesis file: {e}")))?;
2224        let mut store = Self::new_with_config(store_path, engine_type, config)?;
2225        store.add_initial_state(genesis).await?;
2226        Ok(store)
2227    }
2228
2229    pub async fn get_account_info(
2230        &self,
2231        block_number: BlockNumber,
2232        address: Address,
2233    ) -> Result<Option<AccountInfo>, StoreError> {
2234        match self.get_canonical_block_hash(block_number).await? {
2235            Some(block_hash) => self.get_account_info_by_hash(block_hash, address),
2236            None => Ok(None),
2237        }
2238    }
2239
2240    pub fn get_account_info_by_hash(
2241        &self,
2242        block_hash: BlockHash,
2243        address: Address,
2244    ) -> Result<Option<AccountInfo>, StoreError> {
2245        let Some(state_trie) = self.state_trie(block_hash)? else {
2246            return Ok(None);
2247        };
2248        let hashed_address = hash_address_fixed(&address);
2249
2250        let Some(encoded_state) = state_trie.get(hashed_address.as_bytes())? else {
2251            return Ok(None);
2252        };
2253
2254        let account_state = AccountState::decode(&encoded_state)?;
2255        Ok(Some(AccountInfo {
2256            code_hash: account_state.code_hash,
2257            balance: account_state.balance,
2258            nonce: account_state.nonce,
2259        }))
2260    }
2261
2262    pub fn get_account_state_by_acc_hash(
2263        &self,
2264        block_hash: BlockHash,
2265        account_hash: H256,
2266    ) -> Result<Option<AccountState>, StoreError> {
2267        let Some(state_trie) = self.state_trie(block_hash)? else {
2268            return Ok(None);
2269        };
2270        let Some(encoded_state) = state_trie.get(account_hash.as_bytes())? else {
2271            return Ok(None);
2272        };
2273        let account_state = AccountState::decode(&encoded_state)?;
2274        Ok(Some(account_state))
2275    }
2276
2277    pub async fn get_fork_id(&self) -> Result<ForkId, StoreError> {
2278        let chain_config = self.get_chain_config();
2279        let genesis_header = self
2280            .load_block_header(0)?
2281            .ok_or(StoreError::MissingEarliestBlockNumber)?;
2282        let block_header = self.latest_block_header.get();
2283
2284        Ok(ForkId::new(
2285            chain_config,
2286            genesis_header,
2287            block_header.timestamp,
2288            block_header.number,
2289        ))
2290    }
2291
2292    pub async fn get_code_by_account_address(
2293        &self,
2294        block_number: BlockNumber,
2295        address: Address,
2296    ) -> Result<Option<Code>, StoreError> {
2297        let Some(block_hash) = self.get_canonical_block_hash(block_number).await? else {
2298            return Ok(None);
2299        };
2300        let Some(state_trie) = self.state_trie(block_hash)? else {
2301            return Ok(None);
2302        };
2303        let hashed_address = hash_address_fixed(&address);
2304        let Some(encoded_state) = state_trie.get(hashed_address.as_bytes())? else {
2305            return Ok(None);
2306        };
2307        let account_state = AccountState::decode(&encoded_state)?;
2308        self.get_account_code(account_state.code_hash)
2309    }
2310
2311    pub async fn get_nonce_by_account_address(
2312        &self,
2313        block_number: BlockNumber,
2314        address: Address,
2315    ) -> Result<Option<u64>, StoreError> {
2316        let Some(block_hash) = self.get_canonical_block_hash(block_number).await? else {
2317            return Ok(None);
2318        };
2319        let Some(state_trie) = self.state_trie(block_hash)? else {
2320            return Ok(None);
2321        };
2322        let hashed_address = hash_address_fixed(&address);
2323        let Some(encoded_state) = state_trie.get(hashed_address.as_bytes())? else {
2324            return Ok(None);
2325        };
2326        let account_state = AccountState::decode(&encoded_state)?;
2327        Ok(Some(account_state.nonce))
2328    }
2329
2330    /// Applies account updates based on the block's latest storage state
2331    /// and returns the new state root after the updates have been applied.
2332    pub fn apply_account_updates_batch(
2333        &self,
2334        block_hash: BlockHash,
2335        account_updates: &[AccountUpdate],
2336    ) -> Result<Option<AccountUpdatesList>, StoreError> {
2337        let Some(mut state_trie) = self.state_trie(block_hash)? else {
2338            return Ok(None);
2339        };
2340
2341        Ok(Some(self.apply_account_updates_from_trie_batch(
2342            &mut state_trie,
2343            account_updates,
2344        )?))
2345    }
2346
2347    pub fn apply_account_updates_from_trie_batch<'a>(
2348        &self,
2349        state_trie: &mut Trie,
2350        account_updates: impl IntoIterator<Item = &'a AccountUpdate>,
2351    ) -> Result<AccountUpdatesList, StoreError> {
2352        let mut ret_storage_updates = Vec::new();
2353        let mut code_updates = Vec::new();
2354        let state_root = state_trie.hash_no_commit(&NativeCrypto);
2355        for update in account_updates {
2356            let hashed_address = hash_address_fixed(&update.address);
2357            if update.removed {
2358                // Remove account from trie
2359                state_trie.remove(hashed_address.as_bytes())?;
2360                continue;
2361            }
2362            // Add or update AccountState in the trie
2363            // Fetch current state or create a new state to be inserted
2364            let mut account_state = match state_trie.get(hashed_address.as_bytes())? {
2365                Some(encoded_state) => AccountState::decode(&encoded_state)?,
2366                None => AccountState::default(),
2367            };
2368            if update.removed_storage {
2369                account_state.storage_root = EMPTY_TRIE_HASH;
2370            }
2371            if let Some(info) = &update.info {
2372                account_state.nonce = info.nonce;
2373                account_state.balance = info.balance;
2374                account_state.code_hash = info.code_hash;
2375                // Store updated code in DB
2376                if let Some(code) = &update.code {
2377                    code_updates.push((info.code_hash, code.clone()));
2378                }
2379            }
2380            // Store the added storage in the account's storage trie and compute its new root
2381            if !update.added_storage.is_empty() {
2382                let mut storage_trie =
2383                    self.open_storage_trie(hashed_address, state_root, account_state.storage_root)?;
2384                for (storage_key, storage_value) in &update.added_storage {
2385                    let hashed_key = hash_key(storage_key);
2386                    if storage_value.is_zero() {
2387                        storage_trie.remove(&hashed_key)?;
2388                    } else {
2389                        storage_trie.insert(hashed_key, storage_value.encode_to_vec())?;
2390                    }
2391                }
2392                let (storage_hash, storage_updates) =
2393                    storage_trie.collect_changes_since_last_hash(&NativeCrypto);
2394                account_state.storage_root = storage_hash;
2395                ret_storage_updates.push((hashed_address, storage_updates));
2396            }
2397            state_trie.insert(
2398                hashed_address.as_bytes().to_vec(),
2399                account_state.encode_to_vec(),
2400            )?;
2401        }
2402        let (state_trie_hash, state_updates) =
2403            state_trie.collect_changes_since_last_hash(&NativeCrypto);
2404
2405        Ok(AccountUpdatesList {
2406            state_trie_hash,
2407            state_updates,
2408            storage_updates: ret_storage_updates,
2409            code_updates,
2410        })
2411    }
2412
2413    /// Performs the same actions as apply_account_updates_from_trie
2414    ///  but also returns the used storage tries with witness recorded
2415    pub fn apply_account_updates_from_trie_with_witness(
2416        &self,
2417        mut state_trie: Trie,
2418        account_updates: &[AccountUpdate],
2419        mut storage_tries: StorageTries,
2420    ) -> Result<(StorageTries, AccountUpdatesList), StoreError> {
2421        let mut ret_storage_updates = Vec::new();
2422
2423        let mut code_updates = Vec::new();
2424
2425        let state_root = state_trie.hash_no_commit(&NativeCrypto);
2426
2427        for update in account_updates.iter() {
2428            let hashed_address = hash_address(&update.address);
2429
2430            if update.removed {
2431                // Remove account from trie
2432                state_trie.remove(&hashed_address)?;
2433
2434                continue;
2435            }
2436
2437            // Add or update AccountState in the trie
2438            // Fetch current state or create a new state to be inserted
2439            let mut account_state = match state_trie.get(&hashed_address)? {
2440                Some(encoded_state) => AccountState::decode(&encoded_state)?,
2441                None => AccountState::default(),
2442            };
2443
2444            if update.removed_storage {
2445                account_state.storage_root = EMPTY_TRIE_HASH;
2446            }
2447
2448            if let Some(info) = &update.info {
2449                account_state.nonce = info.nonce;
2450
2451                account_state.balance = info.balance;
2452
2453                account_state.code_hash = info.code_hash;
2454
2455                // Store updated code in DB
2456                if let Some(code) = &update.code {
2457                    code_updates.push((info.code_hash, code.clone()));
2458                }
2459            }
2460
2461            // Store the added storage in the account's storage trie and compute its new root
2462            if !update.added_storage.is_empty() {
2463                let (_witness, storage_trie) = match storage_tries.entry(update.address) {
2464                    Entry::Occupied(value) => value.into_mut(),
2465                    Entry::Vacant(vacant) => {
2466                        let trie = self.open_storage_trie(
2467                            H256::from_slice(&hashed_address),
2468                            state_root,
2469                            account_state.storage_root,
2470                        )?;
2471                        vacant.insert(TrieLogger::open_trie(trie))
2472                    }
2473                };
2474
2475                for (storage_key, storage_value) in &update.added_storage {
2476                    let hashed_key = hash_key(storage_key);
2477
2478                    if storage_value.is_zero() {
2479                        storage_trie.remove(&hashed_key)?;
2480                    } else {
2481                        storage_trie.insert(hashed_key, storage_value.encode_to_vec())?;
2482                    }
2483                }
2484
2485                let (storage_hash, storage_updates) =
2486                    storage_trie.collect_changes_since_last_hash(&NativeCrypto);
2487
2488                account_state.storage_root = storage_hash;
2489
2490                ret_storage_updates.push((H256::from_slice(&hashed_address), storage_updates));
2491            }
2492
2493            state_trie.insert(hashed_address, account_state.encode_to_vec())?;
2494        }
2495
2496        let (state_trie_hash, state_updates) =
2497            state_trie.collect_changes_since_last_hash(&NativeCrypto);
2498
2499        let account_updates_list = AccountUpdatesList {
2500            state_trie_hash,
2501            state_updates,
2502            storage_updates: ret_storage_updates,
2503            code_updates,
2504        };
2505
2506        Ok((storage_tries, account_updates_list))
2507    }
2508
2509    /// Adds all genesis accounts and returns the genesis block's state_root
2510    pub async fn setup_genesis_state_trie(
2511        &self,
2512        genesis_accounts: BTreeMap<Address, GenesisAccount>,
2513    ) -> Result<H256, StoreError> {
2514        let mut storage_trie_nodes = vec![];
2515        let mut genesis_state_trie = self.open_direct_state_trie(EMPTY_TRIE_HASH)?;
2516        for (address, account) in genesis_accounts {
2517            let hashed_address = hash_address(&address);
2518            let h256_hashed_address = H256::from_slice(&hashed_address);
2519
2520            // Store account code (as this won't be stored in the trie)
2521            let code = Code::from_bytecode(account.code, &NativeCrypto);
2522            let code_hash = code.hash;
2523            self.add_account_code(code).await?;
2524
2525            // Store the account's storage in a clean storage trie and compute its root
2526            let mut storage_trie =
2527                self.open_direct_storage_trie(h256_hashed_address, EMPTY_TRIE_HASH)?;
2528            for (storage_key, storage_value) in account.storage {
2529                if !storage_value.is_zero() {
2530                    let hashed_key = hash_key(&H256(storage_key.to_big_endian()));
2531                    storage_trie.insert(hashed_key, storage_value.encode_to_vec())?;
2532                }
2533            }
2534
2535            let (storage_root, storage_nodes) =
2536                storage_trie.collect_changes_since_last_hash(&NativeCrypto);
2537
2538            storage_trie_nodes.extend(
2539                storage_nodes
2540                    .into_iter()
2541                    .map(|(path, n)| (apply_prefix(Some(h256_hashed_address), path).into_vec(), n)),
2542            );
2543
2544            // Add account to trie
2545            let account_state = AccountState {
2546                nonce: account.nonce,
2547                balance: account.balance,
2548                storage_root,
2549                code_hash,
2550            };
2551            genesis_state_trie.insert(hashed_address, account_state.encode_to_vec())?;
2552        }
2553
2554        let (state_root, account_trie_nodes) =
2555            genesis_state_trie.collect_changes_since_last_hash(&NativeCrypto);
2556        let account_trie_nodes = account_trie_nodes
2557            .into_iter()
2558            .map(|(path, n)| (apply_prefix(None, path).into_vec(), n))
2559            .collect::<Vec<_>>();
2560
2561        let mut tx = self.backend.begin_write()?;
2562        tx.put_batch(ACCOUNT_TRIE_NODES, account_trie_nodes)?;
2563        tx.put_batch(STORAGE_TRIE_NODES, storage_trie_nodes)?;
2564        tx.commit()?;
2565
2566        Ok(state_root)
2567    }
2568
2569    // Key format: block_number (8 bytes, big-endian) + block_hash (32 bytes)
2570    fn make_witness_key(block_number: u64, block_hash: &BlockHash) -> Vec<u8> {
2571        let mut composite_key = Vec::with_capacity(8 + 32);
2572        composite_key.extend_from_slice(&block_number.to_be_bytes());
2573        composite_key.extend_from_slice(block_hash.as_bytes());
2574        composite_key
2575    }
2576
2577    /// Stores a pre-serialized execution witness for a block.
2578    ///
2579    /// The witness is converted to RPC format (RpcExecutionWitness) before storage
2580    /// to avoid expensive `encode_subtrie` traversal on every read. This pre-computes
2581    /// the serialization at write time instead of read time.
2582    pub fn store_witness(
2583        &self,
2584        block_hash: BlockHash,
2585        block_number: u64,
2586        witness: ExecutionWitness,
2587    ) -> Result<(), StoreError> {
2588        // Convert to RPC format once at storage time
2589        let rpc_witness = RpcExecutionWitness::try_from(witness)?;
2590        let key = Self::make_witness_key(block_number, &block_hash);
2591        let value = serde_json::to_vec(&rpc_witness)?;
2592        self.write(EXECUTION_WITNESSES, key, value)?;
2593        // Clean up old witnesses (keep only last 128)
2594        self.cleanup_old_witnesses(block_number)
2595    }
2596
2597    fn cleanup_old_witnesses(&self, latest_block_number: u64) -> Result<(), StoreError> {
2598        // If we have less than 128 blocks, no cleanup needed
2599        if latest_block_number <= MAX_WITNESSES {
2600            return Ok(());
2601        }
2602
2603        let threshold = latest_block_number - MAX_WITNESSES;
2604
2605        if let Some(oldest_block_number) = self.get_oldest_witness_number()? {
2606            let prefix = oldest_block_number.to_be_bytes();
2607            let mut to_delete = Vec::new();
2608
2609            {
2610                let read_txn = self.backend.begin_read()?;
2611                let iter = read_txn.prefix_iterator(EXECUTION_WITNESSES, &prefix)?;
2612
2613                // We may have multiple witnesses for the same block number (forks)
2614                for item in iter {
2615                    let (key, _value) = item?;
2616                    let mut block_number_bytes = [0u8; 8];
2617                    block_number_bytes.copy_from_slice(&key[0..8]);
2618                    let block_number = u64::from_be_bytes(block_number_bytes);
2619                    if block_number > threshold {
2620                        break;
2621                    }
2622                    to_delete.push(key.to_vec());
2623                }
2624            }
2625
2626            for key in to_delete {
2627                self.delete(EXECUTION_WITNESSES, key)?;
2628            }
2629        };
2630
2631        self.update_oldest_witness_number(threshold + 1)?;
2632
2633        Ok(())
2634    }
2635
2636    fn update_oldest_witness_number(&self, oldest_block_number: u64) -> Result<(), StoreError> {
2637        self.write(
2638            MISC_VALUES,
2639            b"oldest_witness_block_number".to_vec(),
2640            oldest_block_number.to_le_bytes().to_vec(),
2641        )?;
2642        Ok(())
2643    }
2644
2645    fn get_oldest_witness_number(&self) -> Result<Option<u64>, StoreError> {
2646        let Some(value) = self.read(MISC_VALUES, b"oldest_witness_block_number".to_vec())? else {
2647            return Ok(None);
2648        };
2649
2650        let array: [u8; 8] = value.as_slice().try_into().map_err(|_| {
2651            StoreError::Custom("Invalid oldest witness block number bytes".to_string())
2652        })?;
2653        Ok(Some(u64::from_le_bytes(array)))
2654    }
2655
2656    /// Returns the raw JSON bytes of a cached witness for a block.
2657    ///
2658    /// This is the most efficient method for the RPC handler since it avoids
2659    /// deserialization and re-serialization. The bytes can be parsed directly
2660    /// as a JSON Value for the RPC response.
2661    pub fn get_witness_json_bytes(
2662        &self,
2663        block_number: u64,
2664        block_hash: BlockHash,
2665    ) -> Result<Option<Vec<u8>>, StoreError> {
2666        let key = Self::make_witness_key(block_number, &block_hash);
2667        self.read(EXECUTION_WITNESSES, key)
2668    }
2669
2670    /// Returns the deserialized RpcExecutionWitness for a block.
2671    ///
2672    /// Prefer `get_witness_json_bytes` when you need to return the witness
2673    /// as JSON (e.g., for RPC responses) to avoid re-serialization.
2674    pub fn get_witness_by_number_and_hash(
2675        &self,
2676        block_number: u64,
2677        block_hash: BlockHash,
2678    ) -> Result<Option<RpcExecutionWitness>, StoreError> {
2679        let key = Self::make_witness_key(block_number, &block_hash);
2680        match self.read(EXECUTION_WITNESSES, key)? {
2681            Some(value) => {
2682                let witness: RpcExecutionWitness = serde_json::from_slice(&value)?;
2683                Ok(Some(witness))
2684            }
2685            None => Ok(None),
2686        }
2687    }
2688
2689    /// Stores a block access list for a given block hash.
2690    pub fn store_block_access_list(
2691        &self,
2692        block_hash: BlockHash,
2693        bal: &BlockAccessList,
2694    ) -> Result<(), StoreError> {
2695        let key = block_hash.as_bytes().to_vec();
2696        let mut value = vec![];
2697        bal.encode(&mut value);
2698        self.write(BLOCK_ACCESS_LISTS, key, value)
2699    }
2700
2701    /// Returns the block access list for a given block hash, if stored.
2702    pub fn get_block_access_list(
2703        &self,
2704        block_hash: BlockHash,
2705    ) -> Result<Option<BlockAccessList>, StoreError> {
2706        let key = block_hash.as_bytes().to_vec();
2707        match self.read(BLOCK_ACCESS_LISTS, key)? {
2708            Some(value) => {
2709                let bal = BlockAccessList::decode(&value)
2710                    .map_err(|e| StoreError::Custom(format!("Failed to decode BAL: {e}")))?;
2711                Ok(Some(bal))
2712            }
2713            None => Ok(None),
2714        }
2715    }
2716
2717    pub async fn add_initial_state(&mut self, genesis: Genesis) -> Result<(), StoreError> {
2718        self.add_initial_state_inner(genesis, false).await
2719    }
2720
2721    /// Like [`Store::add_initial_state`], but trusts a pre-existing datadir's
2722    /// state instead of validating it against the provided genesis. If a genesis
2723    /// header is already stored, it is kept as-is rather than recomputing the
2724    /// genesis state root from `genesis.alloc` and rejecting on mismatch. The
2725    /// chain config from the genesis file is still applied either way.
2726    ///
2727    /// Intended for booting a datadir produced out-of-band (e.g. by a state
2728    /// generator that writes the state trie directly and emits a genesis file
2729    /// with an empty `alloc`), where the operator vouches for the stored state
2730    /// root. Has no effect on a fresh datadir: the genesis is built normally.
2731    pub async fn add_initial_state_skip_validation(
2732        &mut self,
2733        genesis: Genesis,
2734    ) -> Result<(), StoreError> {
2735        self.add_initial_state_inner(genesis, true).await
2736    }
2737
2738    async fn add_initial_state_inner(
2739        &mut self,
2740        genesis: Genesis,
2741        skip_genesis_validation: bool,
2742    ) -> Result<(), StoreError> {
2743        debug!("Storing initial state from genesis");
2744
2745        // Obtain genesis block
2746        let genesis_block = genesis.get_block();
2747        let genesis_block_number = genesis_block.header.number;
2748
2749        let genesis_hash = genesis_block.hash();
2750
2751        let stored_genesis_header = self.load_block_header(genesis_block_number)?;
2752
2753        // Always set the chain config from the genesis file. The in-memory
2754        // `chain_config` starts at `Default::default()` on every boot and is
2755        // not reloaded from the datadir, so skipping this would leave the store
2756        // with the wrong chainId and an empty fork schedule. Skip-validation
2757        // only waives the genesis state-root/header check; the `config` section
2758        // of the genesis file is still authoritative and must be applied.
2759        self.set_chain_config(&genesis.config).await?;
2760
2761        // The cache can't be empty. Clamp the head to the durable block: after a
2762        // crash, `LatestBlockNumber` can be ahead of `flushed_upto` (FCU writes the
2763        // head synchronously while block bodies are buffered), so loading the raw
2764        // latest header would brick boot when its body was never flushed.
2765        if let Some(latest) = self.load_latest_block_number().await? {
2766            self.anchor_to_durable_head(latest).await?;
2767        }
2768
2769        match stored_genesis_header {
2770            Some(header) if skip_genesis_validation => {
2771                info!(
2772                    stored_genesis = %header.hash(),
2773                    "Skipping genesis state validation; trusting the genesis header and state already stored in the datadir"
2774                );
2775                return Ok(());
2776            }
2777            Some(header) if header.hash() == genesis_hash => {
2778                info!("Received genesis file matching a previously stored one, nothing to do");
2779                return Ok(());
2780            }
2781            Some(_) => {
2782                error!(
2783                    "The chain configuration stored in the database is incompatible with the provided configuration. If you intended to switch networks, choose another datadir or clear the database (e.g., run `ethrex removedb`) and try again."
2784                );
2785                return Err(StoreError::IncompatibleChainConfig);
2786            }
2787            None => {
2788                self.add_block_header(genesis_hash, genesis_block.header.clone())
2789                    .await?
2790            }
2791        }
2792        // Store genesis accounts
2793        // TODO: Should we use this root instead of computing it before the block hash check?
2794        let genesis_state_root = self.setup_genesis_state_trie(genesis.alloc).await?;
2795        debug_assert_eq!(genesis_state_root, genesis_block.header.state_root);
2796
2797        // Store genesis block
2798        info!(hash = %genesis_hash, "Storing genesis block");
2799
2800        self.add_block(genesis_block).await?;
2801        self.update_earliest_block_number(genesis_block_number)
2802            .await?;
2803        self.forkchoice_update(vec![], genesis_block_number, genesis_hash, None, None)
2804            .await?;
2805        Ok(())
2806    }
2807
2808    pub async fn load_initial_state(&self) -> Result<(), StoreError> {
2809        info!("Loading initial state from DB");
2810        let Some(latest) = self.load_latest_block_number().await? else {
2811            return Err(StoreError::MissingLatestBlockNumber);
2812        };
2813        // Use the same durable-head clamp as the node boot path so export and the
2814        // running node agree on the head. The persisted head is only rewritten when
2815        // it actually moved, so a plain export run does not mutate `CHAIN_DATA`.
2816        self.anchor_to_durable_head(latest).await?;
2817        Ok(())
2818    }
2819
2820    pub fn get_storage_at(
2821        &self,
2822        block_number: BlockNumber,
2823        address: Address,
2824        storage_key: H256,
2825    ) -> Result<Option<U256>, StoreError> {
2826        match self.get_block_header(block_number)? {
2827            Some(header) => self.get_storage_at_root(header.state_root, address, storage_key),
2828            None => Ok(None),
2829        }
2830    }
2831
2832    pub fn get_storage_at_root(
2833        &self,
2834        state_root: H256,
2835        address: Address,
2836        storage_key: H256,
2837    ) -> Result<Option<U256>, StoreError> {
2838        let account_hash = hash_address_fixed(&address);
2839
2840        // Pre-acquire shared resources once for both trie opens
2841        let read_view = self.backend.begin_read()?;
2842        let cache = self.gated_snapshot(state_root)?;
2843        let last_written = self.last_written()?;
2844        // While a deep-reorg overlay serves this root, flat-KV reads must
2845        // go through the trie: journal entries written while the FKV generator was
2846        // running lack pre-images for keys past the generator frontier, so disk
2847        // flat-KV may hold the generator's stale values. `TrieWrapper` already
2848        // forces the trie-node read path in this window; mirror the gate here so
2849        // the EMPTY_TRIE_HASH shortcut doesn't bypass it.
2850        let use_fkv = Self::flatkeyvalue_computed_with_last_written(account_hash, &last_written)
2851            && !cache.overlay_serves(state_root);
2852
2853        let storage_root = if use_fkv {
2854            // We will use FKVs, we don't need the root
2855            EMPTY_TRIE_HASH
2856        } else {
2857            let state_trie = self.open_state_trie_shared(
2858                state_root,
2859                read_view.clone(),
2860                cache.clone(),
2861                last_written.clone(),
2862            )?;
2863            let Some(encoded_account) = state_trie.get(account_hash.as_bytes())? else {
2864                return Ok(None);
2865            };
2866            let account = AccountState::decode(&encoded_account)?;
2867            account.storage_root
2868        };
2869        let storage_trie = self.open_storage_trie_shared(
2870            account_hash,
2871            state_root,
2872            storage_root,
2873            read_view,
2874            cache,
2875            last_written,
2876        )?;
2877
2878        let hashed_key = hash_key_fixed(&storage_key);
2879        storage_trie
2880            .get(&hashed_key)?
2881            .map(|rlp| U256::decode(&rlp).map_err(StoreError::RLPDecode))
2882            .transpose()
2883    }
2884
2885    /// Gets storage value when the account hash and storage root are already known.
2886    ///
2887    /// This skips the state-trie account lookup and account RLP decode done by
2888    /// [`Self::get_storage_at_root`], and directly opens the account storage trie.
2889    pub fn get_storage_at_root_with_known_storage_root(
2890        &self,
2891        state_root: H256,
2892        account_hash: H256,
2893        storage_root: H256,
2894        storage_key: H256,
2895    ) -> Result<Option<U256>, StoreError> {
2896        let read_view = self.backend.begin_read()?;
2897        let cache = self.gated_snapshot(state_root)?;
2898        let last_written = self.last_written()?;
2899        // When FKV is active the real storage root is in the flatkeyvalue store,
2900        // not in the account's RLP-encoded storage_root field. Use EMPTY_TRIE_HASH
2901        // so open_storage_trie_shared falls through to the FKV path. While a
2902        // deep-reorg overlay serves this root, keep the real root instead:
2903        // disk flat-KV may hold stale generator values, so reads must go through
2904        // the trie (see `TrieWrapper::flatkeyvalue_computed`).
2905        let storage_root =
2906            if Self::flatkeyvalue_computed_with_last_written(account_hash, &last_written)
2907                && !cache.overlay_serves(state_root)
2908            {
2909                EMPTY_TRIE_HASH
2910            } else {
2911                storage_root
2912            };
2913        let storage_trie = self.open_storage_trie_shared(
2914            account_hash,
2915            state_root,
2916            storage_root,
2917            read_view,
2918            cache,
2919            last_written,
2920        )?;
2921
2922        let hashed_key = hash_key_fixed(&storage_key);
2923        storage_trie
2924            .get(&hashed_key)?
2925            .map(|rlp| U256::decode(&rlp).map_err(StoreError::RLPDecode))
2926            .transpose()
2927    }
2928
2929    pub fn get_chain_config(&self) -> ChainConfig {
2930        self.chain_config
2931    }
2932
2933    pub async fn get_latest_canonical_block_hash(&self) -> Result<Option<BlockHash>, StoreError> {
2934        Ok(Some(self.latest_block_header.get().hash()))
2935    }
2936
2937    /// Updates the canonical chain.
2938    /// Inserts new canonical blocks, removes blocks beyond the new head,
2939    /// and updates the head, safe, and finalized block pointers.
2940    /// All operations are performed in a single database transaction.
2941    pub async fn forkchoice_update(
2942        &self,
2943        new_canonical_blocks: Vec<(BlockNumber, BlockHash)>,
2944        head_number: BlockNumber,
2945        head_hash: BlockHash,
2946        safe: Option<BlockNumber>,
2947        finalized: Option<BlockNumber>,
2948    ) -> Result<(), StoreError> {
2949        // Serialize concurrent forkchoice updates. Without this, two callers
2950        // could interleave their `latest_block_header` cache updates with each
2951        // other's DB writes, leaving the cache inconsistent with the DB or
2952        // letting a later caller's write reorder relative to the cache update
2953        // order (see the TOCTOU discussion around canonical/latest drift).
2954        let _guard = self.fcu_lock.lock().await;
2955
2956        // Updates first the latest_block_header to avoid nonce inconsistencies #3927.
2957        // Snapshot the previous header so we can roll the cache back if the DB
2958        // write fails — otherwise the cache would point at a block the DB does
2959        // not consider canonical.
2960        let previous_head = self.latest_block_header.get();
2961        let new_head = self
2962            .get_block_header_by_hash(head_hash)?
2963            .ok_or_else(|| StoreError::MissingLatestBlockNumber)?;
2964        self.latest_block_header.update(new_head);
2965        if let Err(err) = self
2966            .forkchoice_update_inner(
2967                new_canonical_blocks,
2968                head_number,
2969                head_hash,
2970                safe,
2971                finalized,
2972            )
2973            .await
2974        {
2975            self.latest_block_header.update((*previous_head).clone());
2976            return Err(err);
2977        }
2978
2979        // Refresh the canonical safe-commit root now that the canonical tables reflect the new
2980        // head. `None` (chain shorter than the threshold, e.g. genesis init at head 0) leaves the
2981        // cell unchanged so genesis-on-disk is never gated away.
2982        // No `latest_block_header` rollback on error here (unlike the `inner` failure above): the
2983        // only error is a poisoned safe-commit `RwLock`, which is an unrecoverable process state.
2984        if let Some(root) = self.compute_safe_commit_root(head_number)? {
2985            // Advancing the cell alone does not flush: the commit step (Phase 2) only runs
2986            // while blocks execute, so an execute-all-then-one-forkchoice flow (e.g. block
2987            // import) would accumulate every layer and never persist. When the safe-commit
2988            // root advances, poke the worker to flush the now-committable backlog up to it.
2989            if self.set_safe_commit_root(root)? {
2990                let tx = self.persist_tx.clone();
2991                let _ = tokio::task::spawn_blocking(move || tx.send(PersistMessage::Commit(root)))
2992                    .await;
2993            }
2994        }
2995
2996        Ok(())
2997    }
2998
2999    /// Updates the dedicated safe-commit-root cell with the given state root,
3000    /// returning `true` if the cell changed (so callers can skip a redundant flush).
3001    ///
3002    /// This is a plain synchronous function; it touches only the dedicated cell
3003    /// and is disjoint from the trie-cache Arc (no cache clone or replacement).
3004    /// Crate-private: only `forkchoice_update` (post-canonicalization) may set it,
3005    /// preserving the invariant that the cell only ever holds a canonical state root.
3006    pub(crate) fn set_safe_commit_root(&self, root: H256) -> Result<bool, StoreError> {
3007        let mut guard = self
3008            .safe_commit_root
3009            .write()
3010            .map_err(|_| StoreError::LockError)?;
3011        let changed = *guard != root;
3012        *guard = root;
3013        Ok(changed)
3014    }
3015
3016    /// Computes the canonical safe-commit state root: the state root of the canonical block
3017    /// `commit_threshold` layers below `head`.
3018    ///
3019    /// Returns `Ok(None)` when the chain is shorter than the threshold (underflow), or when the
3020    /// target block is not yet canonical / its header is absent. Synchronous getters only; no
3021    /// await and no lock guard held across one. The threshold is read from the trie cache to
3022    /// avoid duplicating the IN_MEMORY/DB selection that `from_backend` already made.
3023    /// Crate-private: only `forkchoice_update` consumes it.
3024    pub(crate) fn compute_safe_commit_root(
3025        &self,
3026        head: BlockNumber,
3027    ) -> Result<Option<H256>, StoreError> {
3028        let commit_threshold = self
3029            .trie_cache
3030            .read()
3031            .map_err(|_| StoreError::LockError)?
3032            .commit_threshold;
3033        let Some(target) = head.checked_sub(commit_threshold as u64) else {
3034            return Ok(None);
3035        };
3036        let Some(hash) = self.get_canonical_block_hash_sync(target)? else {
3037            return Ok(None);
3038        };
3039        let Some(header) = self.get_block_header_by_hash(hash)? else {
3040            return Ok(None);
3041        };
3042        Ok(Some(header.state_root))
3043    }
3044
3045    /// Obtain the storage trie for the given block
3046    pub fn state_trie(&self, block_hash: BlockHash) -> Result<Option<Trie>, StoreError> {
3047        let Some(header) = self.get_block_header_by_hash(block_hash)? else {
3048            return Ok(None);
3049        };
3050        Ok(Some(self.open_state_trie(header.state_root)?))
3051    }
3052
3053    /// Obtain the storage trie for the given account on the given block
3054    pub fn storage_trie(
3055        &self,
3056        block_hash: BlockHash,
3057        address: Address,
3058    ) -> Result<Option<Trie>, StoreError> {
3059        let Some(header) = self.get_block_header_by_hash(block_hash)? else {
3060            return Ok(None);
3061        };
3062        // Fetch Account from state_trie
3063        let Some(state_trie) = self.state_trie(block_hash)? else {
3064            return Ok(None);
3065        };
3066        let hashed_address = hash_address_fixed(&address);
3067        let Some(encoded_account) = state_trie.get(hashed_address.as_bytes())? else {
3068            return Ok(None);
3069        };
3070        let account = AccountState::decode(&encoded_account)?;
3071        // Open storage_trie
3072        let storage_root = account.storage_root;
3073        Ok(Some(self.open_storage_trie(
3074            hashed_address,
3075            header.state_root,
3076            storage_root,
3077        )?))
3078    }
3079
3080    pub async fn get_account_state(
3081        &self,
3082        block_number: BlockNumber,
3083        address: Address,
3084    ) -> Result<Option<AccountState>, StoreError> {
3085        let Some(block_hash) = self.get_canonical_block_hash(block_number).await? else {
3086            return Ok(None);
3087        };
3088        let Some(state_trie) = self.state_trie(block_hash)? else {
3089            return Ok(None);
3090        };
3091        self.get_account_state_from_trie(&state_trie, address)
3092    }
3093
3094    pub fn get_account_state_by_root(
3095        &self,
3096        state_root: H256,
3097        address: Address,
3098    ) -> Result<Option<AccountState>, StoreError> {
3099        let state_trie = self.open_state_trie(state_root)?;
3100        self.get_account_state_from_trie(&state_trie, address)
3101    }
3102
3103    pub fn get_account_state_from_trie(
3104        &self,
3105        state_trie: &Trie,
3106        address: Address,
3107    ) -> Result<Option<AccountState>, StoreError> {
3108        let hashed_address = hash_address_fixed(&address);
3109        let Some(encoded_state) = state_trie.get(hashed_address.as_bytes())? else {
3110            return Ok(None);
3111        };
3112        Ok(Some(AccountState::decode(&encoded_state)?))
3113    }
3114
3115    /// Batch lookup of account states by address against a given state root.
3116    ///
3117    /// Fast path: for addresses whose hashed path falls within the FKV cursor
3118    /// (and which are not present in the in-memory diff-layer cache), values
3119    /// are fetched in a single `multi_get` on `ACCOUNT_FLATKEYVALUE`. Other
3120    /// addresses fall back to per-address trie walks.
3121    ///
3122    /// Results are returned in the same order as the input addresses.
3123    pub fn get_account_states_batch_by_root(
3124        &self,
3125        state_root: H256,
3126        addresses: &[Address],
3127    ) -> Result<Vec<Option<AccountState>>, StoreError> {
3128        if addresses.is_empty() {
3129            return Ok(Vec::new());
3130        }
3131
3132        let last_written = self.last_written()?;
3133        let trie_cache = self
3134            .trie_cache
3135            .read()
3136            .map_err(|_| StoreError::LockError)?
3137            .clone();
3138
3139        let mut results: Vec<Option<AccountState>> = vec![None; addresses.len()];
3140        // Per-address leaf paths (nibbles + leaf flag). Length 65.
3141        let leaf_paths: Vec<Vec<u8>> = addresses
3142            .iter()
3143            .map(|addr| {
3144                let hashed = hash_address_fixed(addr);
3145                Nibbles::from_bytes(hashed.as_bytes()).into_vec()
3146            })
3147            .collect();
3148
3149        let mut fkv_indices: Vec<usize> = Vec::new();
3150        let mut trie_indices: Vec<usize> = Vec::new();
3151
3152        // Match `BackendTrieDB::flatkeyvalue_computed` semantics: a path is
3153        // covered by FKV iff `last_written >= path` as raw nibble bytes. This
3154        // is the same check `Trie::get` uses; the related helper
3155        // `Store::flatkeyvalue_computed_with_last_written` slices `[0..64]`
3156        // and is intentionally more conservative — using that here would
3157        // unnecessarily fall back to the trie when the cursor sits inside an
3158        // account's storage sweep (the account leaf is already in FKV at that
3159        // point; see `flatkeyvalue_generator`).
3160        //
3161        // While a deep-reorg overlay serves this root, skip the FKV fast
3162        // path entirely: this function never consults the overlay, and disk
3163        // flat-KV may hold values the generator computed against the chain being
3164        // reorged away (journal entries written during generation lack
3165        // past-frontier flat-KV pre-images). The per-address trie fallback goes
3166        // through `TrieWrapper`, which routes reads through the overlay.
3167        let overlay_active = trie_cache.overlay_serves(state_root);
3168        let fkv_cursor: &[u8] = last_written.as_slice();
3169        for (i, path) in leaf_paths.iter().enumerate() {
3170            if let Some(value) = trie_cache.get(state_root, path.as_slice()) {
3171                if !value.is_empty() {
3172                    results[i] = Some(AccountState::decode(&value)?);
3173                }
3174                continue;
3175            }
3176            if !overlay_active && fkv_cursor >= path.as_slice() {
3177                fkv_indices.push(i);
3178            } else {
3179                trie_indices.push(i);
3180            }
3181        }
3182
3183        if !fkv_indices.is_empty() {
3184            // Sort by the prefixed (hashed-order) key so adjacent accounts land
3185            // in the same RocksDB data blocks, then read the sorted keys in
3186            // contiguous shards concurrently. A single `multi_get` runs the whole
3187            // batch at queue depth 1 (async_io is off), which on a cold account-
3188            // heavy block serializes thousands of random reads (coldbench: ~13x
3189            // slower than this sharded path). Mirrors the storage FKV batch.
3190            fkv_indices.sort_unstable_by(|&a, &b| leaf_paths[a].cmp(&leaf_paths[b]));
3191            let read_view = self.backend.begin_read()?;
3192            let keys: Vec<&[u8]> = fkv_indices
3193                .iter()
3194                .map(|&i| leaf_paths[i].as_slice())
3195                .collect();
3196            let shards = shard_count(keys.len());
3197            let raw: Vec<Result<Option<Vec<u8>>, StoreError>> = if shards <= 1 {
3198                read_view.multi_get(ACCOUNT_FLATKEYVALUE, &keys)
3199            } else {
3200                let chunk = keys.len().div_ceil(shards);
3201                let rv = read_view.as_ref();
3202                std::thread::scope(|scope| {
3203                    let handles: Vec<_> = keys
3204                        .chunks(chunk)
3205                        .map(|ck| scope.spawn(move || rv.multi_get(ACCOUNT_FLATKEYVALUE, ck)))
3206                        .collect();
3207                    handles
3208                        .into_iter()
3209                        // A panicked shard yields one Err element rather than
3210                        // re-panicking the whole node; the consumer's `?` below
3211                        // surfaces it to the caller. Every caller of this batch
3212                        // treats a failed warm as best-effort, so the keys are
3213                        // simply left uncached and read normally later.
3214                        .flat_map(|h| {
3215                            h.join().unwrap_or_else(|_| {
3216                                vec![Err(StoreError::Custom(
3217                                    "account prefetch shard panicked".into(),
3218                                ))]
3219                            })
3220                        })
3221                        .collect()
3222                })
3223            };
3224            for (slot, res) in fkv_indices.iter().zip(raw.into_iter()) {
3225                let Some(encoded) = res? else { continue };
3226                if encoded.is_empty() {
3227                    continue;
3228                }
3229                results[*slot] = Some(AccountState::decode(&encoded)?);
3230            }
3231        }
3232
3233        if !trie_indices.is_empty() {
3234            // Fall back to the regular trie path for any addresses whose path
3235            // hasn't been swept by the FKV generator yet. Parallelized to
3236            // recover the per-address fan-out the pre-batch `par_iter` path
3237            // had, which matters during initial sync when most addresses
3238            // miss FKV.
3239            let state_trie = self.open_state_trie(state_root)?;
3240            let fetched: Result<Vec<(usize, Option<AccountState>)>, StoreError> = trie_indices
3241                .par_iter()
3242                .map(|&i| {
3243                    self.get_account_state_from_trie(&state_trie, addresses[i])
3244                        .map(|s| (i, s))
3245                })
3246                .collect();
3247            for (i, s) in fetched? {
3248                results[i] = s;
3249            }
3250        }
3251
3252        Ok(results)
3253    }
3254
3255    /// Batch lookup of storage slot values against a given state root.
3256    ///
3257    /// Each input tuple is `(account_hash, storage_root, slot)` where
3258    /// `account_hash` is the keccak hash of the account address and `slot` is
3259    /// the raw (un-hashed) storage key, matching the inputs available to
3260    /// [`Self::get_storage_at_root_with_known_storage_root`].
3261    ///
3262    /// Fast path: for slots whose prefixed FKV leaf path falls within the FKV
3263    /// cursor (and which are not present in the in-memory diff-layer cache),
3264    /// values are fetched in a single `multi_get` on `STORAGE_FLATKEYVALUE`.
3265    /// The batch is sorted by the prefixed FKV key (hashed order) before the
3266    /// `multi_get` so that adjacent slots share RocksDB data blocks, removing
3267    /// the random-order read amplification of per-slot lookups. Slots not yet
3268    /// swept by the FKV generator fall back to per-slot trie walks via
3269    /// [`Self::get_storage_at_root_with_known_storage_root`], which preserves the
3270    /// exact FKV / trie / diff-cache ordering of the single-slot path.
3271    ///
3272    /// Results are returned in the same order as the input slots. A missing slot
3273    /// (deleted or never written) yields `None`, identical to the single-get path.
3274    pub fn get_storage_values_batch_by_root(
3275        &self,
3276        state_root: H256,
3277        slots: &[(H256, H256, H256)],
3278    ) -> Result<Vec<Option<U256>>, StoreError> {
3279        if slots.is_empty() {
3280            return Ok(Vec::new());
3281        }
3282
3283        let last_written = self.last_written()?;
3284        let trie_cache = self
3285            .trie_cache
3286            .read()
3287            .map_err(|_| StoreError::LockError)?
3288            .clone();
3289
3290        let mut results: Vec<Option<U256>> = vec![None; slots.len()];
3291        // Per-slot prefixed FKV leaf paths, length 131: 64 account-hash nibbles
3292        // + 1 (account leaf flag) + 1 (separator) + 64 hashed-slot nibbles + 1
3293        // (storage leaf flag). This is the exact key
3294        // `BackendTrieDB::flatkeyvalue_computed` / `Trie::get` use for a storage
3295        // leaf (see `apply_prefix`).
3296        let leaf_paths: Vec<Vec<u8>> = slots
3297            .iter()
3298            .map(|(account_hash, _storage_root, slot)| {
3299                let hashed_slot = hash_key_fixed(slot);
3300                apply_prefix(Some(*account_hash), Nibbles::from_bytes(&hashed_slot)).into_vec()
3301            })
3302            .collect();
3303
3304        let mut fkv_indices: Vec<usize> = Vec::new();
3305        let mut trie_indices: Vec<usize> = Vec::new();
3306
3307        // Same watermark check as the account batch and `BackendTrieDB`: a path
3308        // is covered by FKV iff `last_written >= prefixed_path` compared as raw
3309        // nibble bytes. `Nibbles` orders by its inner `data` vec, so the raw
3310        // slice comparison is byte-identical to the `Nibbles >= Nibbles` check
3311        // in `BackendTrieDB::flatkeyvalue_computed`.
3312        //
3313        // While a deep-reorg overlay serves this root, skip the FKV fast path
3314        // entirely: this function never consults the overlay, and disk flat-KV may
3315        // hold values the generator computed against the chain being reorged away
3316        // (journal entries written during generation lack past-frontier flat-KV
3317        // pre-images). The per-slot trie fallback reads through `TrieWrapper`, which
3318        // routes through the overlay. Mirrors the gate in
3319        // `get_storage_at_root` and `get_account_states_batch_by_root`.
3320        let overlay_active = trie_cache.overlay_serves(state_root);
3321        let fkv_cursor: &[u8] = last_written.as_slice();
3322        for (i, path) in leaf_paths.iter().enumerate() {
3323            // Diff-layer cache holds the authoritative latest value for this
3324            // key at this state root, overriding both FKV and trie. Mirror the
3325            // cache-first semantics of `TrieWrapper::get` / the account batch:
3326            // an empty cached value means the slot was deleted -> None.
3327            if let Some(value) = trie_cache.get(state_root, path.as_slice()) {
3328                if !value.is_empty() {
3329                    results[i] = Some(U256::decode(&value).map_err(StoreError::RLPDecode)?);
3330                }
3331                continue;
3332            }
3333            if !overlay_active && fkv_cursor >= path.as_slice() {
3334                fkv_indices.push(i);
3335            } else {
3336                trie_indices.push(i);
3337            }
3338        }
3339
3340        if !fkv_indices.is_empty() {
3341            // Sort the FKV batch by the prefixed (hashed-order) key so adjacent
3342            // slots land in the same 4KB RocksDB data blocks. This is the whole
3343            // point of the batch: it removes the random-order amplification of
3344            // issuing one point lookup per slot in execution order.
3345            fkv_indices.sort_unstable_by(|&a, &b| leaf_paths[a].cmp(&leaf_paths[b]));
3346            let read_view = self.backend.begin_read()?;
3347            let keys: Vec<&[u8]> = fkv_indices
3348                .iter()
3349                .map(|&i| leaf_paths[i].as_slice())
3350                .collect();
3351            // Sharded parallel multi_get. A single `multi_get` runs the whole
3352            // batch serially inside RocksDB (async_io is OFF in our build, so
3353            // no io_uring => queue depth ~= 1), which on a cold NVMe is purely
3354            // read-latency-bound: it touches few blocks (sorted) but serializes
3355            // them. Splitting the SORTED keys into contiguous shards read by
3356            // separate blocking threads keeps the per-shard block sharing while
3357            // issuing the shards concurrently, so effective queue depth ~= shard
3358            // count. On a bloated-account cold read this is ~4.7x a single
3359            // multi_get and ~4.6x the per-slot `par_iter` baseline (coldbench).
3360            // Shard count scales with batch size and is capped; small batches
3361            // run a single in-line multi_get (no thread spawn). All shards share
3362            // one read view (one snapshot) so the reads stay consistent.
3363            let shards = shard_count(keys.len());
3364            let raw: Vec<Result<Option<Vec<u8>>, StoreError>> = if shards <= 1 {
3365                read_view.multi_get(STORAGE_FLATKEYVALUE, &keys)
3366            } else {
3367                let chunk = keys.len().div_ceil(shards);
3368                let rv = read_view.as_ref();
3369                std::thread::scope(|scope| {
3370                    let handles: Vec<_> = keys
3371                        .chunks(chunk)
3372                        .map(|ck| scope.spawn(move || rv.multi_get(STORAGE_FLATKEYVALUE, ck)))
3373                        .collect();
3374                    handles
3375                        .into_iter()
3376                        // A panicked shard yields one Err element rather than
3377                        // re-panicking the whole node; the consumer's `?` below
3378                        // surfaces it to the caller. Every caller of this batch
3379                        // treats a failed warm as best-effort, so the keys are
3380                        // simply left uncached and read normally later.
3381                        .flat_map(|h| {
3382                            h.join().unwrap_or_else(|_| {
3383                                vec![Err(StoreError::Custom(
3384                                    "storage prefetch shard panicked".into(),
3385                                ))]
3386                            })
3387                        })
3388                        .collect()
3389                })
3390            };
3391            for (slot_idx, res) in fkv_indices.iter().zip(raw.into_iter()) {
3392                let Some(encoded) = res? else { continue };
3393                if encoded.is_empty() {
3394                    continue;
3395                }
3396                results[*slot_idx] = Some(U256::decode(&encoded).map_err(StoreError::RLPDecode)?);
3397            }
3398        }
3399
3400        if !trie_indices.is_empty() {
3401            // Fall back to the regular single-slot path for any slot whose path
3402            // hasn't been swept by the FKV generator yet. Reusing
3403            // `get_storage_at_root_with_known_storage_root` guarantees the
3404            // trie-walk fallback is byte-identical to the unbatched read,
3405            // including its own per-slot FKV / EMPTY_TRIE_HASH handling.
3406            // Parallelized to recover the per-slot fan-out the pre-batch
3407            // `par_iter` prefetch path had.
3408            let fetched: Result<Vec<(usize, Option<U256>)>, StoreError> = trie_indices
3409                .par_iter()
3410                .map(|&i| {
3411                    let (account_hash, storage_root, slot) = slots[i];
3412                    self.get_storage_at_root_with_known_storage_root(
3413                        state_root,
3414                        account_hash,
3415                        storage_root,
3416                        slot,
3417                    )
3418                    .map(|v| (i, v))
3419                })
3420                .collect();
3421            for (i, v) in fetched? {
3422                results[i] = v;
3423            }
3424        }
3425
3426        Ok(results)
3427    }
3428
3429    /// Best-effort warming of the MPT *internal* nodes the merkleizer will walk,
3430    /// driven entirely by the BAL working set. The merkle phase reads trie nodes
3431    /// from `STORAGE_TRIE_NODES` / `ACCOUNT_TRIE_NODES` (see `BackendTrieDB::get`)
3432    /// by nibble path; the existing flat-KV prefetch warms only the leaf-value
3433    /// CFs that *execution* reads, so every merkle-walk node read is otherwise
3434    /// cold. This pulls those node blocks into the page/block cache up front so
3435    /// the subsequent walk runs warm.
3436    ///
3437    /// The keys are not discovered by walking (a walk is QD1 and serial): the
3438    /// internal nodes on the path to a leaf are all at *prefixes* of that leaf's
3439    /// nibble path, so for every BAL leaf we speculatively probe every prefix up
3440    /// to `MAX_PREFETCH_DEPTH`. Prefixes that are not real nodes are rejected by
3441    /// the CF bloom filter in RAM (no disk read; measured ~free), and the shallow
3442    /// prefixes collapse on dedup. This turns the cold merkle node reads into one
3443    /// sorted, sharded, high-queue-depth `multi_get` per CF — the same lever that
3444    /// fixed the flat-KV `sload` path, applied to the trie-node CFs.
3445    ///
3446    /// Results are discarded. Missing a node (e.g. one deeper than the probe
3447    /// band) is harmless: the merkleizer just cold-reads it. Run this
3448    /// synchronously up front (mirroring the flat-KV prefetch) so it never races
3449    /// the merkleizer; it touches different CFs than execution, so it does not
3450    /// contend with the executor either.
3451    /// Returns the number of probed keys that hit a real stored node (warmed).
3452    /// On the warm/no-op path this still reports coverage; a near-zero hit count
3453    /// against a non-empty working set signals a key-encoding mismatch (the
3454    /// effectiveness test asserts on this).
3455    pub fn prefetch_trie_nodes(
3456        &self,
3457        storage_slots: &[(Address, H256)],
3458        accounts: &[Address],
3459    ) -> Result<usize, StoreError> {
3460        // Probe band: nodes live at depth <= ~ceil(log16(trie_size)). 8 nibbles
3461        // covers tries up to ~16^8 entries near-fully; any deeper node is simply
3462        // cold-read by the walk, so under-covering only loses a little warming,
3463        // never correctness. Tunable.
3464        const MAX_PREFETCH_DEPTH: usize = 8;
3465
3466        // STORAGE_TRIE_NODES: prefixes of apply_prefix(hashed_addr, hashed_slot).
3467        // Each input pushes MAX_PREFETCH_DEPTH+1 prefixes (depths 0..=max_d).
3468        let mut storage_keys: Vec<Vec<u8>> =
3469            Vec::with_capacity(storage_slots.len() * (MAX_PREFETCH_DEPTH + 1));
3470        for (address, slot) in storage_slots {
3471            let hashed_address = hash_address_fixed(address);
3472            let hashed_slot = hash_key_fixed(slot);
3473            let slot_nibbles = Nibbles::from_bytes(&hashed_slot);
3474            let max_d = MAX_PREFETCH_DEPTH.min(slot_nibbles.len());
3475            for d in 0..=max_d {
3476                let prefix = apply_prefix(Some(hashed_address), slot_nibbles.slice(0, d));
3477                storage_keys.push(prefix.into_vec());
3478            }
3479        }
3480
3481        // ACCOUNT_TRIE_NODES: prefixes of the hashed address (no account prefix).
3482        let mut account_keys: Vec<Vec<u8>> =
3483            Vec::with_capacity(accounts.len() * (MAX_PREFETCH_DEPTH + 1));
3484        for address in accounts {
3485            let hashed_address = hash_address_fixed(address);
3486            let addr_nibbles = Nibbles::from_bytes(hashed_address.as_bytes());
3487            let max_d = MAX_PREFETCH_DEPTH.min(addr_nibbles.len());
3488            for d in 0..=max_d {
3489                account_keys.push(addr_nibbles.slice(0, d).into_vec());
3490            }
3491        }
3492
3493        let hits = self.warm_trie_node_keys(STORAGE_TRIE_NODES, storage_keys)?
3494            + self.warm_trie_node_keys(ACCOUNT_TRIE_NODES, account_keys)?;
3495        Ok(hits)
3496    }
3497
3498    /// Sorted, sharded, high-queue-depth `multi_get` over `keys` in `table`,
3499    /// discarding results. Sorting makes adjacent keys share RocksDB data blocks;
3500    /// sharding across blocking threads issues the reads at queue depth ~= shard
3501    /// count (async_io is off, so a single multi_get runs at QD1). Mirrors the
3502    /// storage flat-KV batch path. Dedups first so shared shallow prefixes are
3503    /// read once.
3504    fn warm_trie_node_keys(
3505        &self,
3506        table: &'static str,
3507        mut keys: Vec<Vec<u8>>,
3508    ) -> Result<usize, StoreError> {
3509        if keys.is_empty() {
3510            return Ok(0);
3511        }
3512        keys.sort_unstable();
3513        keys.dedup();
3514
3515        let shards = shard_count(keys.len());
3516        let read_view = self.backend.begin_read()?;
3517        let count_hits = |res: Vec<Result<Option<Vec<u8>>, StoreError>>| {
3518            res.into_iter().filter(|r| matches!(r, Ok(Some(_)))).count()
3519        };
3520        if shards <= 1 {
3521            let refs: Vec<&[u8]> = keys.iter().map(|k| k.as_slice()).collect();
3522            return Ok(count_hits(read_view.multi_get(table, &refs)));
3523        }
3524        let chunk = keys.len().div_ceil(shards);
3525        let rv = read_view.as_ref();
3526        let hits = std::thread::scope(|scope| {
3527            let handles: Vec<_> = keys
3528                .chunks(chunk)
3529                .map(|ck| {
3530                    scope.spawn(move || {
3531                        let refs: Vec<&[u8]> = ck.iter().map(|k| k.as_slice()).collect();
3532                        // Warm-only: keep cache population, just tally the hits.
3533                        count_hits(rv.multi_get(table, &refs))
3534                    })
3535                })
3536                .collect();
3537            handles
3538                .into_iter()
3539                // Best-effort warming: a panicked shard contributes 0 hits rather
3540                // than re-panicking. The caller treats this prefetch as best-effort
3541                // (missing nodes are just cold-read by the walk), so a shard hiccup
3542                // must not unwind block processing.
3543                .map(|h| h.join().unwrap_or(0))
3544                .sum::<usize>()
3545        });
3546        Ok(hits)
3547    }
3548
3549    /// Constructs a merkle proof for the given account address against a given state.
3550    /// If storage_keys are provided, also constructs the storage proofs for those keys.
3551    ///
3552    /// Returns `None` if the state trie is missing, otherwise returns the proof.
3553    pub async fn get_account_proof(
3554        &self,
3555        state_root: H256,
3556        address: Address,
3557        storage_keys: &[H256],
3558    ) -> Result<Option<AccountProof>, StoreError> {
3559        // TODO: check state root
3560        // let Some(state_trie) = self.open_state_trie(state_trie)? else {
3561        //     return Ok(None);
3562        // };
3563        let state_trie = self.open_state_trie(state_root)?;
3564        let address_path = hash_address_fixed(&address);
3565        let proof = state_trie.get_proof(address_path.as_bytes())?;
3566        let account_opt = state_trie
3567            .get(address_path.as_bytes())?
3568            .map(|encoded_state| AccountState::decode(&encoded_state))
3569            .transpose()?;
3570
3571        let mut storage_proof = Vec::with_capacity(storage_keys.len());
3572
3573        if let Some(account) = &account_opt {
3574            let storage_trie =
3575                self.open_storage_trie(address_path, state_root, account.storage_root)?;
3576
3577            for key in storage_keys {
3578                let hashed_key = hash_key(key);
3579                let proof = storage_trie.get_proof(&hashed_key)?;
3580                let value = storage_trie
3581                    .get(&hashed_key)?
3582                    .map(|rlp| U256::decode(&rlp).map_err(StoreError::RLPDecode))
3583                    .transpose()?
3584                    .unwrap_or_default();
3585
3586                let slot_proof = StorageSlotProof {
3587                    proof,
3588                    key: *key,
3589                    value,
3590                };
3591                storage_proof.push(slot_proof);
3592            }
3593        } else {
3594            storage_proof.extend(storage_keys.iter().map(|key| StorageSlotProof {
3595                proof: Vec::new(),
3596                key: *key,
3597                value: U256::zero(),
3598            }));
3599        }
3600        let account = account_opt.unwrap_or_default();
3601        let account_proof = AccountProof {
3602            proof,
3603            account,
3604            storage_proof,
3605        };
3606        Ok(Some(account_proof))
3607    }
3608
3609    // Returns an iterator across all accounts in the state trie given by the state_root
3610    // Does not check that the state_root is valid
3611    pub fn iter_accounts_from(
3612        &self,
3613        state_root: H256,
3614        starting_address: H256,
3615    ) -> Result<impl Iterator<Item = (H256, AccountState)>, StoreError> {
3616        let mut iter = self.open_locked_state_trie(state_root)?.into_iter();
3617        iter.advance(starting_address.0.to_vec())?;
3618        Ok(iter.content().map_while(|(path, value)| {
3619            Some((H256::from_slice(&path), AccountState::decode(&value).ok()?))
3620        }))
3621    }
3622
3623    // Returns an iterator across all accounts in the state trie given by the state_root
3624    // Does not check that the state_root is valid
3625    pub fn iter_accounts(
3626        &self,
3627        state_root: H256,
3628    ) -> Result<impl Iterator<Item = (H256, AccountState)>, StoreError> {
3629        self.iter_accounts_from(state_root, H256::zero())
3630    }
3631
3632    // Returns an iterator across all accounts in the state trie given by the state_root
3633    // Does not check that the state_root is valid
3634    pub fn iter_storage_from(
3635        &self,
3636        state_root: H256,
3637        hashed_address: H256,
3638        starting_slot: H256,
3639    ) -> Result<Option<impl Iterator<Item = (H256, U256)>>, StoreError> {
3640        let state_trie = self.open_locked_state_trie(state_root)?;
3641        let Some(account_rlp) = state_trie.get(hashed_address.as_bytes())? else {
3642            return Ok(None);
3643        };
3644        let storage_root = AccountState::decode(&account_rlp)?.storage_root;
3645        let mut iter = self
3646            .open_locked_storage_trie(hashed_address, state_root, storage_root)?
3647            .into_iter();
3648        iter.advance(starting_slot.0.to_vec())?;
3649        Ok(Some(iter.content().map_while(|(path, value)| {
3650            Some((H256::from_slice(&path), U256::decode(&value).ok()?))
3651        })))
3652    }
3653
3654    // Returns an iterator across all accounts in the state trie given by the state_root
3655    // Does not check that the state_root is valid
3656    pub fn iter_storage(
3657        &self,
3658        state_root: H256,
3659        hashed_address: H256,
3660    ) -> Result<Option<impl Iterator<Item = (H256, U256)>>, StoreError> {
3661        self.iter_storage_from(state_root, hashed_address, H256::zero())
3662    }
3663
3664    pub fn get_account_range_proof(
3665        &self,
3666        state_root: H256,
3667        starting_hash: H256,
3668        last_hash: Option<H256>,
3669    ) -> Result<Vec<Vec<u8>>, StoreError> {
3670        let state_trie = self.open_state_trie(state_root)?;
3671        let mut proof = state_trie.get_proof(starting_hash.as_bytes())?;
3672        if let Some(last_hash) = last_hash {
3673            proof.extend_from_slice(&state_trie.get_proof(last_hash.as_bytes())?);
3674        }
3675        Ok(proof)
3676    }
3677
3678    pub fn get_storage_range_proof(
3679        &self,
3680        state_root: H256,
3681        hashed_address: H256,
3682        starting_hash: H256,
3683        last_hash: Option<H256>,
3684    ) -> Result<Option<Vec<Vec<u8>>>, StoreError> {
3685        let state_trie = self.open_state_trie(state_root)?;
3686        let Some(account_rlp) = state_trie.get(hashed_address.as_bytes())? else {
3687            return Ok(None);
3688        };
3689        let storage_root = AccountState::decode(&account_rlp)?.storage_root;
3690        let storage_trie = self.open_storage_trie(hashed_address, state_root, storage_root)?;
3691        let mut proof = storage_trie.get_proof(starting_hash.as_bytes())?;
3692        if let Some(last_hash) = last_hash {
3693            proof.extend_from_slice(&storage_trie.get_proof(last_hash.as_bytes())?);
3694        }
3695        Ok(Some(proof))
3696    }
3697
3698    /// Receives the root of the state trie and a list of paths where the first path will correspond to a path in the state trie
3699    /// (aka a hashed account address) and the following paths will be paths in the account's storage trie (aka hashed storage keys)
3700    /// If only one hash (account) is received, then the state trie node containing the account will be returned.
3701    /// If more than one hash is received, then the storage trie nodes where each storage key is stored will be returned
3702    /// For more information check out snap capability message [`GetTrieNodes`](https://github.com/ethereum/devp2p/blob/master/caps/snap.md#gettrienodes-0x06)
3703    /// The paths can be either full paths (hash) or partial paths (compact-encoded nibbles), if a partial path is given for the account this method will not return storage nodes for it
3704    pub fn get_trie_nodes(
3705        &self,
3706        state_root: H256,
3707        paths: Vec<Vec<u8>>,
3708        byte_limit: u64,
3709    ) -> Result<Vec<Vec<u8>>, StoreError> {
3710        let Some(account_path) = paths.first() else {
3711            return Ok(vec![]);
3712        };
3713        let state_trie = self.open_state_trie(state_root)?;
3714        // State Trie Nodes Request
3715        if paths.len() == 1 {
3716            // Fetch state trie node
3717            let node = state_trie.get_node(account_path)?;
3718            return Ok(vec![node]);
3719        }
3720        // Storage Trie Nodes Request
3721        let Some(account_state) = state_trie
3722            .get(account_path)?
3723            .map(|ref rlp| AccountState::decode(rlp))
3724            .transpose()?
3725        else {
3726            return Ok(vec![]);
3727        };
3728        // We can't access the storage trie without the account's address hash
3729        let Ok(hashed_address) = account_path.clone().try_into().map(H256) else {
3730            return Ok(vec![]);
3731        };
3732        let storage_trie =
3733            self.open_storage_trie(hashed_address, state_root, account_state.storage_root)?;
3734        // Fetch storage trie nodes
3735        let mut nodes = vec![];
3736        let mut bytes_used = 0;
3737        // The number of sub-paths is bounded upstream by the snap server's per-request lookup
3738        // cap (`MAX_SERVE_LOOKUPS`), which truncates the pathset before this call; here the
3739        // byte budget only bounds response size.
3740        for path in paths.iter().skip(1) {
3741            if bytes_used >= byte_limit {
3742                break;
3743            }
3744            let node = storage_trie.get_node(path)?;
3745            bytes_used += node.len() as u64;
3746            nodes.push(node);
3747        }
3748        Ok(nodes)
3749    }
3750
3751    // Methods exclusive for trie management during snap-syncing
3752
3753    /// Snapshot the trie layer cache for reading at `state_root`, blocking until
3754    /// that root's diff layer has been installed if it is still in-flight (see
3755    /// [`PendingTrieRoots`]). This is the read barrier for deferred layer builds:
3756    /// taking it at trie-open time guarantees the snapshot contains the layer, so
3757    /// a just-added block's state is never read as stale. Roots that are not
3758    /// pending (already installed, historical/committed, genesis) never block.
3759    fn gated_snapshot(&self, state_root: H256) -> Result<Arc<TrieLayerCache>, StoreError> {
3760        self.pending_trie_roots.wait_until_ready(state_root)?;
3761        Ok(self
3762            .trie_cache
3763            .read()
3764            .map_err(|_| StoreError::LockError)?
3765            .clone())
3766    }
3767
3768    /// Obtain a state trie from the given state root
3769    /// Doesn't check if the state root is valid
3770    /// Used for internal store operations
3771    pub fn open_state_trie(&self, state_root: H256) -> Result<Trie, StoreError> {
3772        let trie_db = TrieWrapper::new(
3773            state_root,
3774            self.gated_snapshot(state_root)?,
3775            Box::new(BackendTrieDB::new_for_accounts(
3776                self.backend.clone(),
3777                self.last_written()?,
3778            )?),
3779            None,
3780        );
3781        Ok(Trie::open(Box::new(trie_db), state_root))
3782    }
3783
3784    /// Obtain a state trie from the given state root
3785    /// Doesn't check if the state root is valid
3786    /// Used for internal store operations
3787    pub fn open_direct_state_trie(&self, state_root: H256) -> Result<Trie, StoreError> {
3788        Ok(Trie::open(
3789            Box::new(BackendTrieDB::new_for_accounts(
3790                self.backend.clone(),
3791                self.last_written()?,
3792            )?),
3793            state_root,
3794        ))
3795    }
3796
3797    /// Obtain a state trie locked for reads from the given state root
3798    /// Doesn't check if the state root is valid
3799    /// Used for internal store operations
3800    pub fn open_locked_state_trie(&self, state_root: H256) -> Result<Trie, StoreError> {
3801        let trie_db = TrieWrapper::new(
3802            state_root,
3803            self.gated_snapshot(state_root)?,
3804            Box::new(state_trie_locked_backend(
3805                self.backend.as_ref(),
3806                self.last_written()?,
3807            )?),
3808            None,
3809        );
3810        Ok(Trie::open(Box::new(trie_db), state_root))
3811    }
3812
3813    /// Obtain a storage trie from the given address and storage_root.
3814    /// Doesn't check if the account is stored
3815    pub fn open_storage_trie(
3816        &self,
3817        account_hash: H256,
3818        state_root: H256,
3819        storage_root: H256,
3820    ) -> Result<Trie, StoreError> {
3821        let trie_db = TrieWrapper::new(
3822            state_root,
3823            self.gated_snapshot(state_root)?,
3824            Box::new(BackendTrieDB::new_for_storages(
3825                self.backend.clone(),
3826                self.last_written()?,
3827            )?),
3828            Some(account_hash),
3829        );
3830        Ok(Trie::open(Box::new(trie_db), storage_root))
3831    }
3832
3833    /// Open a state trie using pre-acquired shared resources.
3834    /// Avoids redundant RwLock acquisitions when multiple tries are opened
3835    /// in the same operation (e.g., state trie + storage trie in get_storage_at_root).
3836    fn open_state_trie_shared(
3837        &self,
3838        state_root: H256,
3839        read_view: Arc<dyn StorageReadView>,
3840        cache: Arc<TrieLayerCache>,
3841        last_written: Vec<u8>,
3842    ) -> Result<Trie, StoreError> {
3843        let trie_db = TrieWrapper::new(
3844            state_root,
3845            cache,
3846            Box::new(BackendTrieDB::new_for_accounts_with_view(
3847                self.backend.clone(),
3848                read_view,
3849                last_written,
3850            )?),
3851            None,
3852        );
3853        Ok(Trie::open(Box::new(trie_db), state_root))
3854    }
3855
3856    /// Open a storage trie using pre-acquired shared resources.
3857    fn open_storage_trie_shared(
3858        &self,
3859        account_hash: H256,
3860        state_root: H256,
3861        storage_root: H256,
3862        read_view: Arc<dyn StorageReadView>,
3863        cache: Arc<TrieLayerCache>,
3864        last_written: Vec<u8>,
3865    ) -> Result<Trie, StoreError> {
3866        let trie_db = TrieWrapper::new(
3867            state_root,
3868            cache,
3869            Box::new(BackendTrieDB::new_for_storages_with_view(
3870                self.backend.clone(),
3871                read_view,
3872                last_written,
3873            )?),
3874            Some(account_hash),
3875        );
3876        Ok(Trie::open(Box::new(trie_db), storage_root))
3877    }
3878
3879    /// Obtain a storage trie from the given address and storage_root.
3880    /// Doesn't check if the account is stored
3881    pub fn open_direct_storage_trie(
3882        &self,
3883        account_hash: H256,
3884        storage_root: H256,
3885    ) -> Result<Trie, StoreError> {
3886        Ok(Trie::open(
3887            Box::new(BackendTrieDB::new_for_account_storage(
3888                self.backend.clone(),
3889                account_hash,
3890                self.last_written()?,
3891            )?),
3892            storage_root,
3893        ))
3894    }
3895
3896    /// Obtain a read-locked storage trie from the given address and storage_root.
3897    /// Doesn't check if the account is stored
3898    pub fn open_locked_storage_trie(
3899        &self,
3900        account_hash: H256,
3901        state_root: H256,
3902        storage_root: H256,
3903    ) -> Result<Trie, StoreError> {
3904        let trie_db = TrieWrapper::new(
3905            state_root,
3906            self.gated_snapshot(state_root)?,
3907            Box::new(state_trie_locked_backend(
3908                self.backend.as_ref(),
3909                self.last_written()?,
3910            )?),
3911            Some(account_hash),
3912        );
3913        Ok(Trie::open(Box::new(trie_db), storage_root))
3914    }
3915
3916    pub fn has_state_root(&self, state_root: H256) -> Result<bool, StoreError> {
3917        // Empty state trie is always available
3918        if state_root == EMPTY_TRIE_HASH {
3919            return Ok(true);
3920        }
3921        let trie = self.open_state_trie(state_root)?;
3922        // NOTE: here we hash the root because the trie doesn't check the state root is correct
3923        let Some(root) = trie.db().get(Nibbles::default())? else {
3924            return Ok(false);
3925        };
3926        let root_hash = ethrex_trie::Node::decode(&root)?
3927            .compute_hash(&NativeCrypto)
3928            .finalize(&NativeCrypto);
3929        Ok(state_root == root_hash)
3930    }
3931
3932    // ===========================================================================
3933    // Deep-reorg primitives (storage side).
3934    // ===========================================================================
3935
3936    /// Returns `true` if the in-memory layer cache currently has a layer with
3937    /// the given `state_root`. Used by the deep-reorg orchestrator to decide
3938    /// whether the head's state can be reached through forward execution (cache
3939    /// hit) or whether a deep-reorg path with overlay construction is required.
3940    pub fn is_state_in_layer_cache(&self, state_root: H256) -> Result<bool, StoreError> {
3941        let trie = self
3942            .trie_cache
3943            .read()
3944            .map_err(|_| StoreError::LockError)?
3945            .clone();
3946        Ok(trie.contains(state_root))
3947    }
3948
3949    /// Returns the highest block number with a `STATE_HISTORY` entry; the cache
3950    /// edge `D` (the deepest block whose post-state is on disk). Returns `None`
3951    /// if the journal is empty (no commits since boot, or fully pruned by finality).
3952    ///
3953    /// O(1) via reverse seek on the column family's last key.
3954    pub fn highest_state_history_block_number(&self) -> Result<Option<BlockNumber>, StoreError> {
3955        let read = self.backend.begin_read()?;
3956        let Some(key) = read.last_key(STATE_HISTORY)? else {
3957            return Ok(None);
3958        };
3959        let arr = <[u8; 8]>::try_from(key.as_slice()).map_err(|_| {
3960            StoreError::Custom(format!(
3961                "STATE_HISTORY key has unexpected length: {}",
3962                key.len()
3963            ))
3964        })?;
3965        Ok(Some(BlockNumber::from_be_bytes(arr)))
3966    }
3967
3968    /// Returns the lowest block number with a `STATE_HISTORY` entry. Returns `None`
3969    /// if the journal is empty (no commits since boot, or fully pruned by finality).
3970    ///
3971    /// O(1) via forward seek on the column family's first key.
3972    pub fn lowest_state_history_block_number(&self) -> Result<Option<BlockNumber>, StoreError> {
3973        let read = self.backend.begin_read()?;
3974        let Some(key) = read.first_key(STATE_HISTORY)? else {
3975            return Ok(None);
3976        };
3977        let arr = <[u8; 8]>::try_from(key.as_slice()).map_err(|_| {
3978            StoreError::Custom(format!(
3979                "STATE_HISTORY key has unexpected length: {}",
3980                key.len()
3981            ))
3982        })?;
3983        Ok(Some(BlockNumber::from_be_bytes(arr)))
3984    }
3985
3986    /// The in-memory diff-layer retention (the layer cache's commit threshold):
3987    /// the deepest reorg the node can serve straight from the layer cache, with no
3988    /// journal/overlay reconstruction. RocksDB default 128, in-memory 10000. Used by
3989    /// `compute_reorg_ceiling` as the physical floor when there is no finality signal.
3990    pub fn reorg_retention(&self) -> Result<u64, StoreError> {
3991        let cache = self.trie_cache.read().map_err(|_| StoreError::LockError)?;
3992        Ok(cache.commit_threshold() as u64)
3993    }
3994
3995    /// Test-only: inserts a pre-encoded `STATE_HISTORY` entry at the given block
3996    /// number. Lets integration tests seed the journal without running enough
3997    /// commits to trip the in-memory cache's flush threshold.
3998    #[doc(hidden)]
3999    pub fn put_state_history_entry_for_test(
4000        &self,
4001        block_number: BlockNumber,
4002        encoded: &[u8],
4003    ) -> Result<(), StoreError> {
4004        let mut tx = self.backend.begin_write()?;
4005        tx.put(STATE_HISTORY, &block_number.to_be_bytes(), encoded)?;
4006        tx.commit()?;
4007        Ok(())
4008    }
4009
4010    /// Atomically prepares the store for a deep-reorg apply pass.
4011    ///
4012    /// Builds an [`Overlay`] from journal entries in `[to_block, from_block]`
4013    /// (inclusive both ends), verifies each entry's `block_hash` against
4014    /// `expected_hash`, then swaps the in-memory layer cache for a fresh one
4015    /// with the overlay installed. After this call:
4016    ///
4017    /// - The layer cache contains zero forward layers.
4018    /// - The overlay is in place; subsequent `TrieWrapper::get` calls cascade
4019    ///   layer cache -> overlay -> disk.
4020    /// - The on-disk trie/flat-KV state is unchanged (still at the OLD chain's
4021    ///   edge `D`).
4022    ///
4023    /// Side-chain blocks `[pivot+1 .. new_head]` should now be executed in
4024    /// chain order through the normal `Blockchain::add_block` path; each
4025    /// block's reads cascade through the overlay, and each block's commit
4026    /// produces a new forward layer.
4027    ///
4028    /// Errors abort the swap: if overlay construction fails (missing entry,
4029    /// hash mismatch, decode error), the existing layer cache is left intact.
4030    pub fn install_overlay_for_reorg(
4031        &self,
4032        from_block: BlockNumber,
4033        to_block: BlockNumber,
4034        expected_hash: impl Fn(BlockNumber) -> Option<H256>,
4035    ) -> Result<(), StoreError> {
4036        // Build the overlay first so any failure aborts before mutating the cache.
4037        let overlay =
4038            Overlay::from_journal(self.backend.as_ref(), from_block, to_block, expected_hash)
4039                .map_err(|e| StoreError::Custom(format!("overlay construction failed: {e}")))?;
4040
4041        let threshold = {
4042            let current = self.trie_cache.read().map_err(|_| StoreError::LockError)?;
4043            current.commit_threshold()
4044        };
4045        // Share the Store's safe-commit-root cell so the canonical commit gate keeps
4046        // working after the cache swap (the cell only advances via forkchoice).
4047        let mut fresh =
4048            TrieLayerCache::new_with_safe_commit(threshold, self.safe_commit_root.clone());
4049        fresh.set_overlay(Arc::new(overlay));
4050
4051        // Wait for the persist worker to be idle before swapping the cache (see
4052        // [`Self::rendezvous_persist_worker`]).
4053        self.rendezvous_persist_worker("install_overlay_for_reorg")?;
4054
4055        let mut guard = self.trie_cache.write().map_err(|_| StoreError::LockError)?;
4056        *guard = Arc::new(fresh);
4057        Ok(())
4058    }
4059
4060    /// Waits for the persist worker to be idle before a layer-cache swap. That
4061    /// worker owns the trie-layer install (`apply_trie_phase1`, run from the
4062    /// `PersistMessage::Block` handler): it reads `trie_cache`, mutates a local
4063    /// clone, and RCU-writes it back; if it is mid-flight when we swap, its
4064    /// write-back can clobber the freshly swapped cache (e.g. drop a just-installed
4065    /// overlay, or install a side-chain layer over a base that no longer has the
4066    /// overlay underneath). `PersistMessage::Ping` carries an ack channel and the
4067    /// worker is FIFO, so its ack proves every earlier `Block` (and thus every
4068    /// earlier trie install) is fully processed and the worker is back at
4069    /// `rx.recv()`. This is the synchronous core of [`wait_for_persistence_idle`];
4070    /// we inline it here because callers are not async. The caller's subsequent
4071    /// `trie_cache.write()` serialising any future RCU makes the swap safe.
4072    fn rendezvous_persist_worker(&self, caller: &str) -> Result<(), StoreError> {
4073        let (ack_tx, ack_rx) = sync_channel::<Result<(), StoreError>>(1);
4074        self.persist_tx
4075            .send(PersistMessage::Ping(ack_tx))
4076            .map_err(|e| {
4077                StoreError::Custom(format!("{caller}: failed to ping persist worker: {e}"))
4078            })?;
4079        ack_rx.recv().map_err(|e| {
4080            StoreError::Custom(format!("{caller}: persist worker ping ack failed: {e}"))
4081        })??;
4082        Ok(())
4083    }
4084
4085    /// Returns `(entry_count, byte_size)` of the currently installed overlay, or
4086    /// `(0, 0)` when no overlay is installed. Used by the observability layer in
4087    /// `fork_choice.rs` immediately after `install_overlay_for_reorg` to emit
4088    /// `ethrex_reorg_overlay_entries` / `ethrex_reorg_overlay_bytes`.
4089    pub fn reorg_overlay_size_hint(&self) -> Result<(usize, usize), StoreError> {
4090        let guard = self.trie_cache.read().map_err(|_| StoreError::LockError)?;
4091        match guard.overlay() {
4092            Some(ov) => Ok((ov.len(), ov.byte_size())),
4093            None => Ok((0, 0)),
4094        }
4095    }
4096
4097    /// Pauses or resumes STATE_HISTORY pruning at finality advance (see the
4098    /// `journal_pruning_paused` field). Called by `Blockchain::enter_reorg` /
4099    /// `ReorgGuard::drop` to bracket a deep-reorg apply pass.
4100    pub fn set_journal_pruning_paused(&self, paused: bool) {
4101        self.journal_pruning_paused
4102            .store(paused, std::sync::atomic::Ordering::Release);
4103    }
4104
4105    /// Removes any installed overlay from the layer cache. Called by the
4106    /// reconciliation path after the first new-chain commit folds
4107    /// the overlay into disk. Idempotent.
4108    pub fn clear_reorg_overlay(&self) -> Result<(), StoreError> {
4109        let mut guard = self.trie_cache.write().map_err(|_| StoreError::LockError)?;
4110        let mut updated = (**guard).clone();
4111        updated.clear_overlay();
4112        *guard = Arc::new(updated);
4113        Ok(())
4114    }
4115
4116    /// Aborts an in-progress deep reorg and resets the layer cache to a fresh
4117    /// empty state with the same commit threshold. Both the overlay and any
4118    /// partially-built new-chain layers are discarded. On-disk state is
4119    /// untouched (still at the OLD chain's `D`), so subsequent FCU evaluations
4120    /// start from a clean foundation.
4121    pub fn abort_reorg(&self) -> Result<(), StoreError> {
4122        // Rendezvous with the persist worker before swapping, exactly like
4123        // `install_overlay_for_reorg`: the live-path ack fires BEFORE
4124        // `apply_trie_phase1` installs the layer, so the worker can still be
4125        // mid-flight with a pre-abort RCU snapshot. Without the rendezvous its
4126        // write-back could install a side-chain layer into the fresh cache with
4127        // no overlay underneath — reads at that root would then cascade into
4128        // old-chain disk state.
4129        self.rendezvous_persist_worker("abort_reorg")?;
4130        let mut guard = self.trie_cache.write().map_err(|_| StoreError::LockError)?;
4131        let threshold = guard.commit_threshold();
4132        *guard = Arc::new(TrieLayerCache::new_with_safe_commit(
4133            threshold,
4134            self.safe_commit_root.clone(),
4135        ));
4136        Ok(())
4137    }
4138
4139    /// Takes a block hash and returns an iterator to its ancestors. Block headers are returned
4140    /// in reverse order, starting from the given block and going up to the genesis block.
4141    pub fn ancestors(&self, block_hash: BlockHash) -> AncestorIterator {
4142        AncestorIterator {
4143            store: self.clone(),
4144            next_hash: block_hash,
4145        }
4146    }
4147
4148    /// Checks if a given block belongs to the current canonical chain. Returns false if the block is not known
4149    pub fn is_canonical_sync(&self, block_hash: BlockHash) -> Result<bool, StoreError> {
4150        let Some(block_number) = self.get_block_number_sync(block_hash)? else {
4151            return Ok(false);
4152        };
4153        Ok(self
4154            .get_canonical_block_hash_sync(block_number)?
4155            .is_some_and(|h| h == block_hash))
4156    }
4157
4158    pub fn generate_flatkeyvalue(&self) -> Result<(), StoreError> {
4159        self.flatkeyvalue_control_tx
4160            .send(FKVGeneratorControlMessage::Continue)
4161            .map_err(|_| StoreError::Custom("FlatKeyValue thread disconnected.".to_string()))
4162    }
4163
4164    pub fn create_checkpoint(&self, path: impl AsRef<Path>) -> Result<(), StoreError> {
4165        self.backend.create_checkpoint(path.as_ref())?;
4166        init_metadata_file(path.as_ref())?;
4167        Ok(())
4168    }
4169
4170    pub fn get_store_directory(&self) -> Result<PathBuf, StoreError> {
4171        Ok(self.db_path.clone())
4172    }
4173
4174    /// Loads the latest block number stored in the database, bypassing the latest block number cache
4175    async fn load_latest_block_number(&self) -> Result<Option<BlockNumber>, StoreError> {
4176        self.read_chain_data_block_number(ChainDataIndex::LatestBlockNumber)
4177            .await
4178    }
4179
4180    fn load_canonical_block_hash(
4181        &self,
4182        block_number: BlockNumber,
4183    ) -> Result<Option<BlockHash>, StoreError> {
4184        let txn = self.backend.begin_read()?;
4185        txn.get(
4186            CANONICAL_BLOCK_HASHES,
4187            block_number.to_le_bytes().as_slice(),
4188        )?
4189        .map(|bytes| H256::decode(bytes.as_slice()))
4190        .transpose()
4191        .map_err(StoreError::from)
4192    }
4193
4194    fn load_block_header(
4195        &self,
4196        block_number: BlockNumber,
4197    ) -> Result<Option<BlockHeader>, StoreError> {
4198        let Some(block_hash) = self.load_canonical_block_hash(block_number)? else {
4199            return Ok(None);
4200        };
4201        self.load_block_header_by_hash(block_hash)
4202    }
4203
4204    /// Load a block header, bypassing the latest header cache
4205    fn load_block_header_by_hash(
4206        &self,
4207        block_hash: BlockHash,
4208    ) -> Result<Option<BlockHeader>, StoreError> {
4209        let txn = self.backend.begin_read()?;
4210        let hash_key = block_hash.encode_to_vec();
4211        let header_value = txn.get(HEADERS, hash_key.as_slice())?;
4212        let mut header = header_value
4213            .map(|bytes| BlockHeaderRLP::from_bytes(bytes).to())
4214            .transpose()
4215            .map_err(StoreError::from)?;
4216        header.as_mut().inspect(|h| {
4217            // Set the hash so we avoid recomputing it later
4218            let _ = h.hash.set(block_hash);
4219        });
4220        Ok(header)
4221    }
4222
4223    pub fn last_written(&self) -> Result<Vec<u8>, StoreError> {
4224        let last_computed_flatkeyvalue = self
4225            .last_computed_flatkeyvalue
4226            .read()
4227            .map_err(|_| StoreError::LockError)?;
4228        Ok(last_computed_flatkeyvalue.clone())
4229    }
4230
4231    /// Returns `true` once the flat-key-value generator has finished its full pass.
4232    ///
4233    /// Completion is recorded by the 1-byte `[0xff]` sentinel the generator writes to
4234    /// `MISC_VALUES["last_written"]` on the final iteration (see `flatkeyvalue_generator`);
4235    /// every non-final frontier value is a real nibble path (bytes `0x00..=0x0f`), so the
4236    /// sentinel is unambiguous. Reads the durable marker rather than the in-memory frontier,
4237    /// which is expanded to `[0xff; 64]`/`[0xff; 131]` and would need length-aware handling.
4238    ///
4239    /// Used to gate journal-backed deep reorgs: entries journaled while generation is still
4240    /// in progress omit past-frontier flat-KV pre-images.
4241    pub fn flatkeyvalue_fully_generated(&self) -> Result<bool, StoreError> {
4242        let tx = self.backend.begin_read()?;
4243        let marker = tx.get(MISC_VALUES, "last_written".as_bytes())?;
4244        Ok(Self::flatkeyvalue_generation_complete(marker.as_deref()))
4245    }
4246
4247    /// Pure completeness test for the durable `last_written` marker: complete iff it is the
4248    /// exact 1-byte `[0xff]` sentinel. Any in-progress frontier is a nibble path (bytes
4249    /// `0x00..=0x0f`) and an unset marker is absent, so neither matches.
4250    fn flatkeyvalue_generation_complete(marker: Option<&[u8]>) -> bool {
4251        marker == Some([0xff].as_slice())
4252    }
4253
4254    fn flatkeyvalue_computed_with_last_written(account: H256, last_written: &[u8]) -> bool {
4255        let account_nibbles = Nibbles::from_bytes(account.as_bytes());
4256        &last_written[0..64] > account_nibbles.as_ref()
4257    }
4258
4259    /// Returns the highest block number durably flushed to disk, or `0` when
4260    /// the marker is absent. Use [`Self::read_flushed_upto_opt`] when you need
4261    /// to distinguish "absent marker" (legacy DB, everything is durable) from
4262    /// "marker present and equal to 0".
4263    pub fn read_flushed_upto(&self) -> Result<BlockNumber, StoreError> {
4264        Ok(self.read_flushed_upto_opt()?.unwrap_or(0))
4265    }
4266
4267    /// Returns `None` when the marker has never been written — a legacy or fresh
4268    /// DB where everything is durable and the head must not be clamped to 0.
4269    fn read_flushed_upto_opt(&self) -> Result<Option<BlockNumber>, StoreError> {
4270        let tx = self.backend.begin_read()?;
4271        match tx.get(MISC_VALUES, FLUSHED_UPTO_KEY)? {
4272            Some(bytes) => Ok(Some(decode_flushed_upto(&bytes)?)),
4273            None => Ok(None),
4274        }
4275    }
4276
4277    /// Insert a block into the in-memory buffer without writing to disk.
4278    /// For testing only — gates production code off.
4279    #[cfg(any(test, feature = "testing"))]
4280    pub fn buffer_block_for_test(&self, block: &Block) {
4281        mutate_block_buffer(&self.block_data_buffer, |b| {
4282            b.insert(block.clone(), vec![], vec![])
4283        })
4284        .expect("block_data_buffer lock poisoned");
4285    }
4286
4287    /// Synchronously flush the block data buffer to disk.
4288    /// For testing only — gates production code off.
4289    #[cfg(any(test, feature = "testing"))]
4290    pub fn flush_block_data_for_test(&self) -> Result<(), StoreError> {
4291        flush_block_data(self.backend.as_ref(), &self.block_data_buffer)
4292    }
4293
4294    /// Read a raw trie node straight from the on-disk account/storage trie-node
4295    /// table by its committed key. For testing only — lets a reopen assert which
4296    /// trie diff-layers a shutdown flush did (or did not) commit to disk.
4297    #[cfg(any(test, feature = "testing"))]
4298    pub fn get_trie_node_for_test(
4299        &self,
4300        is_account: bool,
4301        key: &[u8],
4302    ) -> Result<Option<Vec<u8>>, StoreError> {
4303        let table = if is_account {
4304            ACCOUNT_TRIE_NODES
4305        } else {
4306            STORAGE_TRIE_NODES
4307        };
4308        self.backend.begin_read()?.get(table, key)
4309    }
4310
4311    /// Insert a block plus associated codes into the in-memory buffer without
4312    /// writing to disk.  For testing only — proves the buffer overlay resolves
4313    /// code that has not been persisted yet.
4314    #[cfg(any(test, feature = "testing"))]
4315    pub fn buffer_block_with_codes_for_test(&self, block: &Block, codes: Vec<(H256, Code)>) {
4316        mutate_block_buffer(&self.block_data_buffer, |b| {
4317            b.insert(block.clone(), vec![], codes)
4318        })
4319        .expect("block_data_buffer lock poisoned");
4320    }
4321
4322    /// Mark a state root as in-flight (build pending) without doing a build.
4323    /// For testing only — simulates the window where the persist worker has not
4324    /// yet installed the layer, so reads at this root must block in
4325    /// `gated_snapshot`.
4326    #[cfg(any(test, feature = "testing"))]
4327    pub fn register_pending_root_for_test(&self, root: H256) -> Result<(), StoreError> {
4328        self.pending_trie_roots.register(root)
4329    }
4330
4331    /// Clear an in-flight state root (simulates the worker having installed the
4332    /// layer), unblocking readers waiting in `gated_snapshot`. For testing only.
4333    #[cfg(any(test, feature = "testing"))]
4334    pub fn clear_pending_root_for_test(&self, root: H256) {
4335        self.pending_trie_roots.clear(root)
4336    }
4337
4338    /// Boot-time recovery: clamp `latest_block_header` to the durable head.
4339    ///
4340    /// Durable head = `min(flushed_upto, latest)` when the marker is present
4341    /// (buffered blocks past `flushed_upto` may be lost after a crash; the CL
4342    /// re-sends them via `newPayload`). When the marker is absent the DB
4343    /// predates deferred persistence and everything is on disk — use `latest`
4344    /// as-is, never rewind to 0. On first boot the marker is seeded so a later
4345    /// crash clamps against it rather than an absent (→ 0) marker.
4346    ///
4347    /// The marker tracks the max flushed *block number*, not which hash is
4348    /// canonical at that height. A tip reorg inside the flush window — `Na` at
4349    /// height N is flushed (marker = N), then `newPayload(Nb)` buffers a sibling
4350    /// and FCU durably repoints `canonical[N]` to the still-unflushed `Nb` — can
4351    /// leave `canonical[head]` resolving to a header that never reached disk if
4352    /// we crash before `Nb` flushes. So we walk `head` down to the highest height
4353    /// whose canonical hash actually resolves on disk rather than bricking with
4354    /// `MissingLatestBlockNumber`. A legacy DB (no marker) is exempt: everything
4355    /// there is durable, so a missing header is real corruption and must surface.
4356    async fn anchor_to_durable_head(&self, latest: BlockNumber) -> Result<(), StoreError> {
4357        let marker = self.read_flushed_upto_opt()?;
4358        let start = match marker {
4359            Some(flushed) => flushed.min(latest),
4360            None => latest,
4361        };
4362
4363        let mut head = start;
4364        let latest_block_header = loop {
4365            match self.load_block_header(head)? {
4366                Some(header) => break header,
4367                // Legacy/fresh DB: everything is supposed to be durable, so a
4368                // missing header is real corruption — surface it, don't rewind.
4369                None if marker.is_none() => return Err(StoreError::MissingLatestBlockNumber),
4370                None if head == 0 => return Err(StoreError::MissingLatestBlockNumber),
4371                None => {
4372                    warn!(
4373                        "durable head {head}: canonical hash has no on-disk header \
4374                         (reorg inside flush window); rewinding"
4375                    );
4376                    head -= 1;
4377                }
4378            }
4379        };
4380        self.latest_block_header.update(latest_block_header);
4381
4382        // Re-anchor the persisted head when we moved below `latest`, and (re)write
4383        // the marker to the resolved head: an absent marker is seeded to the
4384        // durable baseline, and a walked-down head lowers the marker so a later
4385        // crash clamps against a hash known to resolve.
4386        let reanchor = head != latest;
4387        let rewrite_marker = marker != Some(head);
4388        if reanchor || rewrite_marker {
4389            let mut tx = self.backend.begin_write()?;
4390            if reanchor {
4391                // Re-anchor the persisted head so `get_latest_block_number` and
4392                // every downstream consumer agree with the clamped head.
4393                let latest_key = chain_data_key(ChainDataIndex::LatestBlockNumber);
4394                tx.put(CHAIN_DATA, &latest_key, &head.to_le_bytes())?;
4395            }
4396            if rewrite_marker {
4397                write_flushed_upto(tx.as_mut(), head)?;
4398            }
4399            tx.commit()?;
4400        }
4401        Ok(())
4402    }
4403}
4404
4405/// Writes the `flushed_upto` block number into an open write batch.
4406///
4407/// The caller is responsible for committing `tx` afterward.
4408pub fn write_flushed_upto(
4409    tx: &mut dyn StorageWriteBatch,
4410    n: BlockNumber,
4411) -> Result<(), StoreError> {
4412    tx.put(MISC_VALUES, FLUSHED_UPTO_KEY, &n.to_le_bytes())
4413}
4414
4415/// Decode an 8-byte little-endian `flushed_upto` marker value.
4416///
4417/// Returns an error for a present-but-malformed value so on-disk corruption is
4418/// surfaced loudly rather than silently resetting the durable marker. Single
4419/// source of truth for both `from_backend` and [`Store::read_flushed_upto`].
4420fn decode_flushed_upto(bytes: &[u8]) -> Result<BlockNumber, StoreError> {
4421    let arr: [u8; 8] = bytes
4422        .try_into()
4423        .map_err(|_| StoreError::Custom("Invalid flushed_upto bytes".to_string()))?;
4424    Ok(BlockNumber::from_le_bytes(arr))
4425}
4426
4427/// RCU-swap the block-data buffer. The persist worker is the sole caller in
4428/// production (no lost-update race); test helpers also call this on one thread.
4429fn mutate_block_buffer(
4430    buffer: &Arc<RwLock<Arc<BlockDataBuffer>>>,
4431    f: impl FnOnce(&mut BlockDataBuffer),
4432) -> Result<(), StoreError> {
4433    let mut new_buf = (*buffer.read().map_err(|_| StoreError::LockError)?.clone()).clone();
4434    f(&mut new_buf);
4435    *buffer.write().map_err(|_| StoreError::LockError)? = Arc::new(new_buf);
4436    Ok(())
4437}
4438
4439/// Default for [`StoreConfig::persist_channel_capacity`].
4440const DEFAULT_PERSIST_CHANNEL_CAPACITY: usize = 2;
4441
4442/// One unit of work for the persist worker: stage block(s), build the trie diff-layer,
4443/// flush to disk. `commit_depth` selects the commit gate (`None` = canonical safe-commit
4444/// root, `Some(depth)` = commit layers deeper than `depth`). `wait_for_flush` selects the
4445/// ack point independently: `false` acks after staging (carrying the prior flush result),
4446/// `true` acks after flush.
4447struct BlockPersist {
4448    blocks: Vec<(Block, Vec<Receipt>)>,
4449    codes: Vec<(H256, Code)>,
4450    parent_state_root: H256,
4451    child_state_root: H256,
4452    account_updates: TrieNodesUpdate,
4453    storage_updates: Vec<(H256, TrieNodesUpdate)>,
4454    commit_depth: Option<usize>,
4455    wait_for_flush: bool,
4456    /// Number of the block whose layer this update represents (the last block in
4457    /// the batch, matching `child_state_root`). Threaded into the trie layer so
4458    /// the committed-layer identity is available for the journal write path.
4459    ///
4460    /// On a multi-block batch this names only the LAST block, which is NOT a usable
4461    /// journal identity for the batch's layer. `wait_for_flush` does not make that
4462    /// safe: it decides when THIS message acks, whereas the layer it creates is
4463    /// journaled or not by whichever LATER commit sweeps it — `TrieLayerCache::commit`
4464    /// takes the target layer and every ancestor, so a per-block commit can reach a
4465    /// batch layer. Any journal entry must therefore be gated on the committed
4466    /// layer covering exactly one block, not on this message's ack mode.
4467    block_number: BlockNumber,
4468    /// Hash of the block whose layer this update represents (see `block_number`).
4469    block_hash: H256,
4470    ack: std::sync::mpsc::SyncSender<Result<(), StoreError>>,
4471}
4472
4473/// Messages for the persist worker. `Ping(ack)` is the idle handshake for
4474/// [`Store::wait_for_persistence_idle`]: the FIFO worker handles it only after
4475/// all earlier `Block` messages are fully processed.
4476enum PersistMessage {
4477    Block(Box<BlockPersist>),
4478    /// Flush the committable layer backlog up to and including this state root, then
4479    /// prune the flushed layers. Sent by `forkchoice_update` when the safe-commit root
4480    /// advances: the commit step otherwise only runs while blocks execute, so an
4481    /// execute-all-then-one-forkchoice flow (e.g. block import) would accumulate every
4482    /// layer and never persist anything to disk.
4483    Commit(H256),
4484    Ping(std::sync::mpsc::SyncSender<Result<(), StoreError>>),
4485    /// Graceful-shutdown handshake. Handled only after every earlier `Block`
4486    /// (FIFO), so it both drains in-flight work and force-flushes the block-data
4487    /// buffer to disk. The trie diff-layers are deliberately left in memory (see
4488    /// [`Store::shutdown`]). The worker acks and exits.
4489    Shutdown {
4490        ack: std::sync::mpsc::SyncSender<Result<(), StoreError>>,
4491    },
4492}
4493
4494/// Write one block's header, body, number, and tx locations into an open batch.
4495/// Shared by [`Store::add_blocks`] (sync import) and [`flush_block_data`]
4496/// (deferred flush) so the on-disk encoding stays in lockstep. Receipts and codes
4497/// are written by callers that need them (only `flush_block_data` does).
4498fn write_block_data(
4499    tx: &mut dyn StorageWriteBatch,
4500    number: BlockNumber,
4501    hash: BlockHash,
4502    header: &BlockHeader,
4503    body: &BlockBody,
4504) -> Result<(), StoreError> {
4505    let hash_key = hash.encode_to_vec();
4506    tx.put(
4507        HEADERS,
4508        &hash_key,
4509        BlockHeaderRLP::from(header.clone()).bytes(),
4510    )?;
4511    tx.put(
4512        BODIES,
4513        &hash_key,
4514        BlockBodyRLP::from_bytes(body.encode_to_vec()).bytes(),
4515    )?;
4516    tx.put(BLOCK_NUMBERS, &hash_key, &number.to_le_bytes())?;
4517    for (index, transaction) in body.transactions.iter().enumerate() {
4518        tx.merge(
4519            TRANSACTION_LOCATIONS,
4520            transaction.hash(&NativeCrypto).as_bytes(),
4521            &encode_tx_location_operand(number, hash, index as u64),
4522        )?;
4523    }
4524    Ok(())
4525}
4526
4527/// Write all unflushed blocks to disk in one tx, advance `flushed_upto`, then
4528/// evict. Eviction is gap-safe: blocks stay buffered until the commit succeeds.
4529fn flush_block_data(
4530    backend: &dyn StorageBackend,
4531    buffer: &Arc<RwLock<Arc<BlockDataBuffer>>>,
4532) -> Result<(), StoreError> {
4533    let snapshot = buffer.read().map_err(|_| StoreError::LockError)?.clone();
4534    let to_flush = snapshot.flushable();
4535    if to_flush.is_empty() {
4536        return Ok(());
4537    }
4538    let hashes: Vec<_> = to_flush.iter().map(|b| b.header.hash()).collect();
4539    let codes = snapshot.codes_for(&hashes);
4540    let mut max_number = snapshot.flushed_upto();
4541
4542    let mut tx = backend.begin_write()?;
4543    for b in &to_flush {
4544        let hash = b.header.hash();
4545        write_block_data(tx.as_mut(), b.number, hash, &b.header, &b.body)?;
4546        for (index, receipt) in b.receipts.iter().enumerate() {
4547            tx.put(
4548                RECEIPTS_V2,
4549                &receipt_key(&hash, index as u64),
4550                &receipt.encode_to_vec(),
4551            )?;
4552        }
4553        max_number = max_number.max(b.number);
4554    }
4555    for (code_hash, code) in codes {
4556        let buf = encode_code(&code);
4557        tx.put(ACCOUNT_CODES, code_hash.as_ref(), &buf)?;
4558        tx.put(
4559            ACCOUNT_CODE_METADATA,
4560            code_hash.as_ref(),
4561            &(code.len() as u64).to_be_bytes(),
4562        )?;
4563    }
4564    write_flushed_upto(tx.as_mut(), max_number)?;
4565    tx.commit()?;
4566
4567    // Phase 3: evict only after the commit succeeded (gap safety).
4568    mutate_block_buffer(buffer, |b| b.evict_flushed(max_number))
4569}
4570
4571type TrieNodesUpdate = Vec<(Nibbles, Vec<u8>)>;
4572
4573/// Tracks state roots whose trie diff-layer is in-flight (building but not yet
4574/// installed in `trie_cache`). `apply_updates` registers a root *before*
4575/// returning; the worker clears it *after* swapping the layer in. This ordering
4576/// is mandatory: a reader opening a trie at a pending root blocks until the
4577/// layer is installed, preventing stale on-disk reads.
4578#[derive(Debug, Default)]
4579struct PendingTrieRoots {
4580    /// Fast-path: when zero, nothing is in flight and readers skip the lock.
4581    count: AtomicUsize,
4582    roots: Mutex<HashSet<H256>>,
4583    ready: Condvar,
4584}
4585
4586impl PendingTrieRoots {
4587    /// Mark `root` as in-flight. MUST be called before the build is handed to
4588    /// the worker (so the worker's `clear` always finds it) and before the head
4589    /// can advance to `root` (so any reader that can reference it sees it pending).
4590    fn register(&self, root: H256) -> Result<(), StoreError> {
4591        let mut roots = self.roots.lock().map_err(|_| StoreError::LockError)?;
4592        if roots.insert(root) {
4593            self.count.fetch_add(1, Ordering::Release);
4594        }
4595        Ok(())
4596    }
4597
4598    /// Mark `root` as installed and wake any waiting readers. MUST be called only
4599    /// after the layer is swapped into `trie_cache`, so a woken reader sees it.
4600    /// Best-effort: a poisoned lock means a reader's `wait_until_ready` also errors,
4601    /// so no reader deadlocks.
4602    fn clear(&self, root: H256) {
4603        let Ok(mut roots) = self.roots.lock() else {
4604            return;
4605        };
4606        if roots.remove(&root) {
4607            self.count.fetch_sub(1, Ordering::Release);
4608            self.ready.notify_all();
4609        }
4610    }
4611
4612    /// Block until `root` is no longer in-flight (its layer is installed). Returns
4613    /// immediately on the fast path when nothing is pending.
4614    fn wait_until_ready(&self, root: H256) -> Result<(), StoreError> {
4615        if self.count.load(Ordering::Acquire) == 0 {
4616            return Ok(());
4617        }
4618        let mut roots = self.roots.lock().map_err(|_| StoreError::LockError)?;
4619        while roots.contains(&root) {
4620            roots = self.ready.wait(roots).map_err(|_| StoreError::LockError)?;
4621        }
4622        Ok(())
4623    }
4624}
4625
4626/// Build the trie diff-layer, RCU-swap it into `trie_cache`, then clear the
4627/// pending root. Swap MUST precede the clear so a woken reader sees the layer.
4628/// On swap failure the root is still cleared so gated readers error, not deadlock.
4629#[allow(clippy::too_many_arguments)]
4630fn apply_trie_phase1(
4631    trie_cache: &Arc<RwLock<Arc<TrieLayerCache>>>,
4632    pending_roots: &PendingTrieRoots,
4633    parent_state_root: H256,
4634    child_state_root: H256,
4635    block_number: BlockNumber,
4636    block_hash: H256,
4637    account_updates: TrieNodesUpdate,
4638    storage_updates: Vec<(H256, TrieNodesUpdate)>,
4639) -> Result<(), StoreError> {
4640    let build: Result<(), StoreError> = (|| {
4641        let new_layer = storage_updates
4642            .into_iter()
4643            .flat_map(|(account_hash, nodes)| {
4644                nodes
4645                    .into_iter()
4646                    .map(move |(path, node)| (apply_prefix(Some(account_hash), path), node))
4647            })
4648            .chain(account_updates)
4649            .collect();
4650        let trie = trie_cache
4651            .read()
4652            .map_err(|_| StoreError::LockError)?
4653            .clone();
4654        let mut trie_mut = (*trie).clone();
4655        trie_mut.put_batch(
4656            parent_state_root,
4657            child_state_root,
4658            block_number,
4659            block_hash,
4660            new_layer,
4661        );
4662        *trie_cache.write().map_err(|_| StoreError::LockError)? = Arc::new(trie_mut);
4663        Ok(())
4664    })();
4665    // Always clear the pending root, whether or not the swap succeeded: on success
4666    // readers see the installed layer; on failure (poisoning) the lock is poisoned
4667    // so gated readers error rather than read stale, and we must not leave them
4668    // blocked forever.
4669    pending_roots.clear(child_state_root);
4670    build
4671}
4672
4673/// Flush and prune the committable trie-layer backlog. No-ops when nothing is committable.
4674///
4675/// `commit_depth` selects the gate. `Some(depth)`: single-canonical-chain execution (batch
4676/// import, full sync, startup regeneration) commits by depth, because the canonical `head - 128`
4677/// safe-commit root never lands on a batch layer boundary; sound because these paths only ever
4678/// extend a single canonical chain (no competing forks to mis-commit). `None`: live block-by-block
4679/// execution uses the canonical safe-commit gate (`TrieLayerCache::get_commitable`) so non-canonical
4680/// `newPayload` state is never persisted.
4681///
4682/// `is_batch` is independent of the gate and only selects journaling (see
4683/// [`commit_to_disk`]). It tracks `wait_for_flush`, not `commit_depth.is_some()`, so every
4684/// per-block path journals and only the bespoke batch path skips it. That keeps the
4685/// full-sync tail and the import tail journaling exactly as they did when `commit_depth`
4686/// and `wait_for_flush` were a single flag.
4687///
4688/// Startup state regeneration is the one path where this is new behavior rather than
4689/// preserved behavior: it runs before any forkchoice update, so the safe-commit cell is
4690/// still zero and the canonical gate committed nothing there, which is the unbounded-layer
4691/// problem the depth gate fixes. Now that it does commit, it also journals. Deriving
4692/// `is_batch` from `commit_depth` instead would suppress that, but it would also suppress
4693/// the full-sync and import tails, and it would leave the journal discontiguous with the
4694/// on-disk root: surviving entries below a non-journaled commit describe pre-images
4695/// relative to a root the disk has already moved past.
4696fn commit_trie_if_due(
4697    backend: &dyn StorageBackend,
4698    trie_cache: &Arc<RwLock<Arc<TrieLayerCache>>>,
4699    fkv_ctl: &SyncSender<FKVGeneratorControlMessage>,
4700    parent_state_root: H256,
4701    commit_depth: Option<usize>,
4702    is_batch: bool,
4703) -> Result<(), StoreError> {
4704    let trie = trie_cache
4705        .read()
4706        .map_err(|_| StoreError::LockError)?
4707        .clone();
4708    // Phase 2 + 3: flush and prune the committable backlog.
4709    let commitable = match commit_depth {
4710        Some(depth) => trie.get_commitable_by_depth(parent_state_root, depth),
4711        None => trie.get_commitable(parent_state_root),
4712    };
4713    let Some(root) = commitable else {
4714        // Nothing to commit to disk, move on.
4715        return Ok(());
4716    };
4717    commit_to_disk(backend, fkv_ctl, trie_cache, &trie, root, is_batch)
4718}
4719
4720/// Flush the layer at `root` and all older ancestors to disk, then prune them from the
4721/// in-memory cache (Phases 2 and 3 of the persistence pipeline).
4722///
4723/// `trie` must be the current cache snapshot and `root` a committable layer (as returned
4724/// by [`TrieLayerCache::get_commitable`]). A `root` that is not a layer commits nothing.
4725///
4726/// Reused by both the per-block path ([`commit_trie_if_due`]) and the forkchoice-driven
4727/// flush ([`PersistMessage::Commit`]): without the latter, an execute-all-then-one-forkchoice
4728/// flow (block import) would never persist, because the commit step only runs while blocks execute.
4729fn commit_to_disk(
4730    backend: &dyn StorageBackend,
4731    fkv_ctl: &SyncSender<FKVGeneratorControlMessage>,
4732    trie_cache: &Arc<RwLock<Arc<TrieLayerCache>>>,
4733    trie: &Arc<TrieLayerCache>,
4734    root: H256,
4735    is_batch: bool,
4736) -> Result<(), StoreError> {
4737    // `root` need not have a layer: the forkchoice `PersistMessage::Commit(root)` path
4738    // forwards the safe-commit root without consulting the cache, and `put_batch` skips
4739    // blocks whose state root equals their parent's. Bail before the side effects below.
4740    if !trie.has_layer(root) {
4741        debug!(
4742            root = ?root,
4743            layers = trie.layer_count(),
4744            is_batch,
4745            "Skipping trie commit: state root has no in-memory layer. Expected when the block \
4746             did not change the state root (empty L2 blocks) or the root was already flushed."
4747        );
4748        return Ok(());
4749    }
4750
4751    // Stop the flat-key-value generator thread, as the underlying trie is about to change.
4752    // Ignore the error, if the channel is closed it means there is no worker to notify.
4753    let _ = fkv_ctl.send(FKVGeneratorControlMessage::Stop);
4754
4755    // RCU to remove the bottom layer: update step needs to happen after disk layer is updated.
4756    let mut trie_mut = (**trie).clone();
4757
4758    // Open the read view BEFORE the write batch so each `.get()` sees disk as it was
4759    // before our writes. The journal records `(key, prev_value)` so a future rollback
4760    // can apply diffs directly without reading state.
4761    //
4762    // NOTE: `StorageReadView` does not currently promise true snapshot isolation
4763    // (see the trait docs in `api/mod.rs`). What makes the pre-image read safe here
4764    // is that `commit_to_disk` only ever runs on the single persist worker thread,
4765    // `write_tx` is a buffered batch that does not become visible until
4766    // `write_tx.commit()` at the end of the function, and the other writers to
4767    // these CFs touch disjoint key space: the flat-KV generator only writes keys
4768    // strictly past the committed `last_written` frontier, and snap-sync healing
4769    // completes before block execution commits layers. So every `.get()` below
4770    // sees on-disk state as of the begin_read call.
4771    let read_view = backend.begin_read()?;
4772    let last_written = read_view
4773        .get(MISC_VALUES, "last_written".as_bytes())?
4774        .unwrap_or_default();
4775
4776    let mut write_tx = backend.begin_write()?;
4777
4778    // Before encoding, accounts have only the account address as their path, while storage keys have
4779    // the account address (32 bytes) + storage path (up to 32 bytes).
4780
4781    // Snapshot the overlay (if any) BEFORE commit so reconciliation can fold its entries
4782    // into this write batch. After a deep reorg, the first
4783    // new-chain commit advances disk from the OLD chain's edge `D` directly to the new
4784    // chain's tip `T` in a single atomic write; the overlay supplies the bridge for keys
4785    // layer_T does not touch. Only meaningful when `!is_batch` (full sync does not journal).
4786    let overlay_for_reconciliation = if !is_batch {
4787        trie.overlay().cloned()
4788    } else {
4789        None
4790    };
4791
4792    // While an overlay is installed, commit only the bottom layer per pass. The
4793    // The reconciliation below is defined for a single layer at the pivot
4794    // tip `T`: bridge entries are folded into that layer's writes, [T, D] is
4795    // delete_ranged, and T's journal entry records pre-images against the
4796    // old-chain disk state. A multi-layer sweep would journal upper layers'
4797    // pre-images against old-chain disk instead of the new-chain/bridge state
4798    // (the intra-batch pre-image map is not seeded with bridge values), silently
4799    // corrupting a future unwind. The backlog above `T` drains in later passes,
4800    // after this commit clears the overlay (see `trie_mut.clear_overlay` below).
4801    let root = if overlay_for_reconciliation.is_some() {
4802        trie.bottom_layer_root(root)
4803    } else {
4804        root
4805    };
4806
4807    // `commit` removes the committed layer(s) and returns one `CommittedLayer` per block
4808    // in oldest-first order. In normal block-by-block operation this is a single layer,
4809    // one commit-cadence behind the just-added block. A forkchoice-driven flush of an
4810    // accumulated backlog (e.g. block import) can return several layers at once, so we
4811    // write one journal entry per block below rather than merging diffs across blocks.
4812    //
4813    // `has_layer` above established `root` is a layer and `trie_mut` is a `Clone` that
4814    // preserves the map, so this must be `Some`. Reaching the error means the clone lost the
4815    // map, which is corruption, not the ordinary no-op handled above.
4816    let committed_layers = trie_mut.commit(root).ok_or_else(|| {
4817        StoreError::Custom(format!(
4818            "trie layer for state root {root:?} disappeared between the has_layer check and \
4819             commit (layers={}, is_batch={is_batch}): the TrieLayerCache clone lost the layer \
4820             map, so this block's state was not written",
4821            trie.layer_count(),
4822        ))
4823    })?;
4824
4825    // Deep-reorg reconciliation is a single new-chain commit advancing disk to the pivot
4826    // tip `T`; it must map to exactly one committed layer. A multi-layer commit with an
4827    // overlay installed would mean a backlog accumulated between overlay install and
4828    // flush, corrupting the [T, D] delete_range below.
4829    debug_assert!(
4830        overlay_for_reconciliation.is_none() || committed_layers.len() == 1,
4831        "overlay-backed reconciliation must commit exactly one layer (T), got {}",
4832        committed_layers.len()
4833    );
4834
4835    // Reconciliation: overlay entries the new chain has NOT rewritten must be
4836    // bridged onto disk so disk fully reflects the pivot->T transition. Keys any committed
4837    // layer touches are skipped (the layer wins, its value is the post-T state). Overlay-only
4838    // entries with `None` become an empty-value write -> deleted on disk, matching the
4839    // "absent at pivot" semantics.
4840    let extra_writes: Vec<(Vec<u8>, Vec<u8>)> = match &overlay_for_reconciliation {
4841        Some(overlay) => {
4842            let layer_keys: rustc_hash::FxHashSet<&Vec<u8>> = committed_layers
4843                .iter()
4844                .flat_map(|l| l.nodes.iter().map(|(k, _)| k))
4845                .collect();
4846            overlay
4847                .iter_all_entries()
4848                .filter(|(_, key, _)| !layer_keys.contains(key))
4849                .map(|(_, key, value)| (key.clone(), value.clone().unwrap_or_default()))
4850                .collect()
4851        }
4852        None => Vec::new(),
4853    };
4854    // Pivot heights (T, D) for the reconciliation commit; `None` in steady state.
4855    let reorg_heights = overlay_for_reconciliation
4856        .as_ref()
4857        .map(|ov| (ov.to_block(), ov.from_block()));
4858
4859    // Intra-batch overlay of values already staged in THIS write batch, so each block's
4860    // reverse diff records the value as of the *previous* committed block's write, not
4861    // just the pre-batch on-disk value. `None` means an earlier block deleted the key.
4862    // For the common single-layer commit this stays empty and every pre-image comes
4863    // straight from `read_view`. Only consulted/maintained when journaling (`!is_batch`).
4864    //
4865    // PERF: the first touch of each key does one synchronous `read_view.get(table, &key)`.
4866    // For large state diffs this is O(N) extra reads on the per-block critical path.
4867    // A follow-up could batch these via `multi_get_cf` if profiling shows it's significant.
4868    let mut overlay: HashMap<Vec<u8>, Option<Vec<u8>>> = HashMap::new();
4869
4870    let mut result = Ok(());
4871    'layers: for layer in &committed_layers {
4872        // Reverse-diff accumulators for this block's journal entry, one per CF. Each entry
4873        // stores the on-disk key as-is (storage CFs carry their nibble-encoded account-hash
4874        // prefix), so a future rollback applies diffs directly without interpretation. For
4875        // full sync (`is_batch == true`), no journal entry is written: reorgs aren't
4876        // supported during full sync, and journaling would slow it down by a read per write.
4877        let mut journal_account_trie: FlatDiff = Vec::new();
4878        let mut journal_storage_trie: FlatDiff = Vec::new();
4879        let mut journal_account_flat: FlatDiff = Vec::new();
4880        let mut journal_storage_flat: FlatDiff = Vec::new();
4881
4882        // The reconciliation commit is single-layer at `T`; fold the overlay bridge entries
4883        // into that layer's writes so they land in T's journal entry. `extra` is empty in
4884        // steady state (no overlay) and for any non-T layer.
4885        // With the bottom-layer-only rule above, a commit made while the overlay
4886        // is installed always contains exactly this one (bottom) layer, so it is
4887        // the reconciliation layer by construction. Matching by block number
4888        // (`layer.block_number == to_block`) would be fragile: root-preserving
4889        // blocks create no layer (L2 empty blocks), so the bottom layer can sit
4890        // above `to_block` and the bridge entries would be neither written nor
4891        // journaled before the overlay is cleared.
4892        let is_reconciliation_layer = overlay_for_reconciliation.is_some();
4893        let extra: &[(Vec<u8>, Vec<u8>)] = if is_reconciliation_layer {
4894            &extra_writes
4895        } else {
4896            &[]
4897        };
4898
4899        for (key, value) in layer.nodes.iter().chain(extra.iter()) {
4900            let (is_leaf, is_account) = classify_trie_key(key.len());
4901
4902            // Keys past the flat-KV generator's frontier aren't written to disk yet, so
4903            // they must not be journaled either (a `Some(None)` entry recorded here
4904            // would cause a rollback to delete a key that was never put). The `continue`
4905            // jumps over both the write and the journal push below.
4906            if is_leaf && key.as_slice() > last_written.as_slice() {
4907                continue;
4908            }
4909            let table = if is_leaf {
4910                if is_account {
4911                    &ACCOUNT_FLATKEYVALUE
4912                } else {
4913                    &STORAGE_FLATKEYVALUE
4914                }
4915            } else if is_account {
4916                &ACCOUNT_TRIE_NODES
4917            } else {
4918                &STORAGE_TRIE_NODES
4919            };
4920
4921            // Pre-image: the intra-batch overlay wins over disk so multi-layer commits
4922            // record each block's true pre-state. Skipped for batch (full-sync) commits.
4923            let prev_value = if !is_batch {
4924                match overlay.get(key) {
4925                    Some(v) => Some(v.clone()),
4926                    None => match read_view.get(table, key) {
4927                        Ok(v) => Some(v),
4928                        Err(e) => {
4929                            result = Err(e);
4930                            break 'layers;
4931                        }
4932                    },
4933                }
4934            } else {
4935                None
4936            };
4937
4938            let new_value = if value.is_empty() {
4939                result = write_tx.delete(table, key);
4940                None
4941            } else {
4942                result = write_tx.put(table, key, value);
4943                Some(value.clone())
4944            };
4945            if result.is_err() {
4946                break 'layers;
4947            }
4948
4949            // Record the reverse-diff entry after the put/delete is staged, so a write
4950            // error doesn't accumulate state we won't persist.
4951            if let Some(prev) = prev_value {
4952                let bucket = match (is_leaf, is_account) {
4953                    (false, true) => &mut journal_account_trie,
4954                    (false, false) => &mut journal_storage_trie,
4955                    (true, true) => &mut journal_account_flat,
4956                    (true, false) => &mut journal_storage_flat,
4957                };
4958                bucket.push((key.clone(), prev));
4959                // Advance the overlay so a later block in this same commit sees this
4960                // block's write as its pre-image.
4961                overlay.insert(key.clone(), new_value);
4962            }
4963        }
4964
4965        // Reconciliation: BEFORE writing this block's journal entry, wipe the
4966        // obsolete OLD-chain entries in `[T, D]` so the new T entry (below) isn't clobbered
4967        // by the range delete. Fires only for the single reconciliation layer at height T.
4968        // T = `overlay.to_block()`, D = `overlay.from_block()`.
4969        if !is_batch && let Some((t, d)) = reorg_heights {
4970            debug_assert_eq!(
4971                layer.block_number, t,
4972                "first new-chain commit must be at the pivot's T height (overlay.to_block)"
4973            );
4974            // Only reconcile when this committed layer is exactly the pivot height `t`.
4975            // The `delete_range` wipes the OLD-chain journal entries `[t, d]`; if the layer
4976            // is not at `t` the range delete would drop new-chain entries that belong in
4977            // `[t, d]` and leave gaps that break later reorg recovery. The debug_assert
4978            // catches this loudly in tests; in release we skip (and log) rather than corrupt
4979            // STATE_HISTORY.
4980            if layer.block_number == t {
4981                let start = t.to_be_bytes();
4982                let end = d.saturating_add(1).to_be_bytes();
4983                result = write_tx.delete_range(STATE_HISTORY, &start, &end);
4984                if result.is_err() {
4985                    break 'layers;
4986                }
4987            } else {
4988                error!(
4989                    block_number = layer.block_number,
4990                    t,
4991                    d,
4992                    "deep-reorg reconciliation skipped: committed layer not at pivot height; skipping STATE_HISTORY delete_range to avoid history gaps"
4993                );
4994            }
4995        }
4996
4997        // Stage this block's journal entry into the same write batch as the trie/flat-KV
4998        // overwrites. `put` is buffered until `commit`, so all CFs and every block's entry
4999        // land atomically (or none do on commit failure). Each entry is keyed and identified
5000        // by its own COMMITTED block, not the in-flight block whose insertion triggered this
5001        // commit (that block commits later, one cadence behind).
5002        if !is_batch {
5003            let entry = JournalEntry {
5004                block_hash: layer.block_hash,
5005                parent_state_root: layer.parent_state_root,
5006                account_trie_diff: journal_account_trie,
5007                storage_trie_diff: journal_storage_trie,
5008                account_flat_diff: journal_account_flat,
5009                storage_flat_diff: journal_storage_flat,
5010            };
5011            result = write_tx.put(
5012                STATE_HISTORY,
5013                &layer.block_number.to_be_bytes(),
5014                &entry.encode(),
5015            );
5016            if result.is_err() {
5017                break 'layers;
5018            }
5019        }
5020    }
5021
5022    if result.is_ok() {
5023        result = write_tx.commit();
5024    }
5025    // We want to send this message even if there was an error during the batch write
5026    let _ = fkv_ctl.send(FKVGeneratorControlMessage::Continue);
5027    result?;
5028
5029    // Reconciliation succeeded: drop the overlay from the cache. Subsequent
5030    // commits revert to the normal one-block path.
5031    if overlay_for_reconciliation.is_some() {
5032        trie_mut.clear_overlay();
5033    }
5034
5035    // Phase 3: update diff layers with the removal of bottom layer.
5036    //
5037    // SAFETY: `install_overlay_for_reorg` rendezvous-pings this worker before
5038    // swapping `trie_cache`, so no concurrent RCU can race the write-back here.
5039    // See the Ping comment in `install_overlay_for_reorg`.
5040    *trie_cache.write().map_err(|_| StoreError::LockError)? = Arc::new(trie_mut);
5041    Ok(())
5042}
5043
5044// NOTE: we don't receive `Store` here to avoid cyclic dependencies
5045// with the other end of `control_rx`
5046fn flatkeyvalue_generator(
5047    backend: &Arc<dyn StorageBackend>,
5048    last_computed_fkv: &RwLock<Vec<u8>>,
5049    control_rx: &std::sync::mpsc::Receiver<FKVGeneratorControlMessage>,
5050) -> Result<(), StoreError> {
5051    info!("Generation of FlatKeyValue started.");
5052    let initial_last_written = backend
5053        .begin_read()?
5054        .get(MISC_VALUES, "last_written".as_bytes())?
5055        .unwrap_or_default();
5056
5057    if initial_last_written.is_empty() {
5058        // First time generating the FKV. Remove all FKV entries just in case
5059        backend.clear_table(ACCOUNT_FLATKEYVALUE)?;
5060        backend.clear_table(STORAGE_FLATKEYVALUE)?;
5061    } else if initial_last_written == [0xff] {
5062        // FKV was already generated
5063        info!("FlatKeyValue already generated. Skipping.");
5064        return Ok(());
5065    }
5066
5067    loop {
5068        // Acquire a fresh read view per iteration so updates performed while the
5069        // generator is paused are visible after a Continue signal.
5070        let read_tx = backend.begin_read()?;
5071        let root = read_tx
5072            .get(ACCOUNT_TRIE_NODES, &[])?
5073            .ok_or(StoreError::MissingLatestBlockNumber)?;
5074        let root: Node = ethrex_trie::Node::decode(&root)?;
5075        let state_root = root.compute_hash(&NativeCrypto).finalize(&NativeCrypto);
5076
5077        let last_written = read_tx
5078            .get(MISC_VALUES, "last_written".as_bytes())?
5079            .unwrap_or_default();
5080        let last_written_account = last_written
5081            .get(0..64)
5082            .map(|v| Nibbles::from_hex(v.to_vec()))
5083            .unwrap_or_default();
5084        let mut last_written_storage = last_written
5085            .get(66..130)
5086            .map(|v| Nibbles::from_hex(v.to_vec()))
5087            .unwrap_or_default();
5088
5089        debug!("Starting FlatKeyValue loop pivot={last_written:?} SR={state_root:x}");
5090
5091        let mut ctr = 0;
5092        let mut write_txn = backend.begin_write()?;
5093        let mut iter = Trie::open(
5094            Box::new(BackendTrieDB::new_for_accounts_with_view(
5095                backend.clone(),
5096                read_tx.clone(),
5097                last_written.clone(),
5098            )?),
5099            state_root,
5100        )
5101        .into_iter();
5102        if last_written_account > Nibbles::default() {
5103            iter.advance(last_written_account.to_bytes())?;
5104        }
5105        let res = iter.try_for_each(|(path, node)| -> Result<(), StoreError> {
5106            let Node::Leaf(node) = node else {
5107                return Ok(());
5108            };
5109            let account_state = AccountState::decode(&node.value)?;
5110            let account_hash = H256::from_slice(&path.to_bytes());
5111            write_txn.put(MISC_VALUES, "last_written".as_bytes(), path.as_ref())?;
5112            write_txn.put(ACCOUNT_FLATKEYVALUE, path.as_ref(), &node.value)?;
5113            ctr += 1;
5114            if ctr > 10_000 {
5115                write_txn.commit()?;
5116                write_txn = backend.begin_write()?;
5117                *last_computed_fkv
5118                    .write()
5119                    .map_err(|_| StoreError::LockError)? = path.as_ref().to_vec();
5120                ctr = 0;
5121            }
5122
5123            let mut iter_inner = Trie::open(
5124                Box::new(BackendTrieDB::new_for_account_storage_with_view(
5125                    backend.clone(),
5126                    read_tx.clone(),
5127                    account_hash,
5128                    path.as_ref().to_vec(),
5129                )?),
5130                account_state.storage_root,
5131            )
5132            .into_iter();
5133            if last_written_storage > Nibbles::default() {
5134                iter_inner.advance(last_written_storage.to_bytes())?;
5135                last_written_storage = Nibbles::default();
5136            }
5137            iter_inner.try_for_each(|(path, node)| -> Result<(), StoreError> {
5138                let Node::Leaf(node) = node else {
5139                    return Ok(());
5140                };
5141                let key = apply_prefix(Some(account_hash), path);
5142                write_txn.put(MISC_VALUES, "last_written".as_bytes(), key.as_ref())?;
5143                write_txn.put(STORAGE_FLATKEYVALUE, key.as_ref(), &node.value)?;
5144                ctr += 1;
5145                if ctr > 10_000 {
5146                    write_txn.commit()?;
5147                    write_txn = backend.begin_write()?;
5148                    *last_computed_fkv
5149                        .write()
5150                        .map_err(|_| StoreError::LockError)? = key.into_vec();
5151                    ctr = 0;
5152                }
5153                fkv_check_for_stop_msg(control_rx)?;
5154                Ok(())
5155            })?;
5156            fkv_check_for_stop_msg(control_rx)?;
5157            Ok(())
5158        });
5159        match res {
5160            Err(StoreError::PivotChanged) => {
5161                match control_rx.recv() {
5162                    Ok(FKVGeneratorControlMessage::Continue) => {}
5163                    Ok(FKVGeneratorControlMessage::Stop) => {
5164                        return Err(StoreError::Custom("Unexpected Stop message".to_string()));
5165                    }
5166                    // If the channel was closed, we stop generation prematurely
5167                    Err(std::sync::mpsc::RecvError) => {
5168                        info!("Store closed, stopping FlatKeyValue generation.");
5169                        return Ok(());
5170                    }
5171                }
5172            }
5173            Err(err) => return Err(err),
5174            Ok(()) => {
5175                write_txn.put(MISC_VALUES, "last_written".as_bytes(), &[0xff])?;
5176                write_txn.commit()?;
5177                *last_computed_fkv
5178                    .write()
5179                    .map_err(|_| StoreError::LockError)? = vec![0xff; 131];
5180                info!("FlatKeyValue generation finished.");
5181                return Ok(());
5182            }
5183        };
5184    }
5185}
5186
5187fn fkv_check_for_stop_msg(
5188    control_rx: &std::sync::mpsc::Receiver<FKVGeneratorControlMessage>,
5189) -> Result<(), StoreError> {
5190    match control_rx.try_recv() {
5191        Ok(FKVGeneratorControlMessage::Stop) | Err(TryRecvError::Disconnected) => {
5192            return Err(StoreError::PivotChanged);
5193        }
5194        Ok(FKVGeneratorControlMessage::Continue) => {
5195            return Err(StoreError::Custom(
5196                "Unexpected Continue message".to_string(),
5197            ));
5198        }
5199        Err(TryRecvError::Empty) => {}
5200    }
5201    Ok(())
5202}
5203
5204fn state_trie_locked_backend(
5205    backend: &dyn StorageBackend,
5206    last_written: Vec<u8>,
5207) -> Result<BackendTrieDBLocked, StoreError> {
5208    // No address prefix for state trie
5209    BackendTrieDBLocked::new(backend, last_written)
5210}
5211
5212pub struct AccountProof {
5213    pub proof: Vec<NodeRLP>,
5214    pub account: AccountState,
5215    pub storage_proof: Vec<StorageSlotProof>,
5216}
5217
5218pub struct StorageSlotProof {
5219    pub proof: Vec<NodeRLP>,
5220    pub key: H256,
5221    pub value: U256,
5222}
5223
5224pub struct AncestorIterator {
5225    store: Store,
5226    next_hash: BlockHash,
5227}
5228
5229impl Iterator for AncestorIterator {
5230    type Item = Result<(BlockHash, BlockHeader), StoreError>;
5231
5232    fn next(&mut self) -> Option<Self::Item> {
5233        let next_hash = self.next_hash;
5234        // Buffer-aware: a not-yet-flushed ancestor (e.g. on a side branch during
5235        // a reorg) must be visible here, or a BLOCKHASH opcode resolving through
5236        // this walk would wrongly reject a valid block.
5237        match self.store.get_block_header_by_hash(next_hash) {
5238            Ok(Some(header)) => {
5239                let ret_hash = self.next_hash;
5240                self.next_hash = header.parent_hash;
5241                Some(Ok((ret_hash, header)))
5242            }
5243            Ok(None) => None,
5244            Err(e) => Some(Err(e)),
5245        }
5246    }
5247}
5248
5249pub fn hash_address(address: &Address) -> Vec<u8> {
5250    keccak_hash(address.to_fixed_bytes()).to_vec()
5251}
5252
5253fn hash_address_fixed(address: &Address) -> H256 {
5254    keccak(address.to_fixed_bytes())
5255}
5256
5257pub fn hash_key(key: &H256) -> Vec<u8> {
5258    keccak_hash(key.to_fixed_bytes()).to_vec()
5259}
5260
5261pub fn hash_key_fixed(key: &H256) -> [u8; 32] {
5262    keccak_hash(key.to_fixed_bytes())
5263}
5264
5265fn chain_data_key(index: ChainDataIndex) -> Vec<u8> {
5266    (index as u8).encode_to_vec()
5267}
5268
5269/// Decodes a block number stored as little-endian bytes.
5270fn decode_block_number(bytes: Vec<u8>) -> Result<BlockNumber, StoreError> {
5271    let array: [u8; 8] = bytes
5272        .try_into()
5273        .map_err(|_| StoreError::Custom("Invalid BlockNumber bytes".to_string()))?;
5274    Ok(BlockNumber::from_le_bytes(array))
5275}
5276
5277fn snap_state_key(index: SnapStateIndex) -> Vec<u8> {
5278    (index as u8).encode_to_vec()
5279}
5280
5281/// Builds a fixed-width RECEIPTS key: block_hash (32B) || index (8B BE).
5282pub fn receipt_key(block_hash: &BlockHash, index: u64) -> Vec<u8> {
5283    let mut key = Vec::with_capacity(40);
5284    key.extend_from_slice(block_hash.as_bytes());
5285    key.extend_from_slice(&index.to_be_bytes());
5286    key
5287}
5288
5289fn encode_code(code: &Code) -> Vec<u8> {
5290    let mut buf =
5291        Vec::with_capacity(6 + code.len() + std::mem::size_of_val::<[u32]>(&code.jump_targets));
5292    code.code().encode(&mut buf);
5293    // `Arc<[u32]>` (the in-memory share) has no `RLPEncode` impl; encode through an
5294    // owned `Vec` on this cold DB-write path (code is persisted once per hash).
5295    code.jump_targets.to_vec().encode(&mut buf);
5296    buf
5297}
5298
5299#[derive(Debug, Default, Clone)]
5300struct LatestBlockHeaderCache {
5301    current: Arc<Mutex<Arc<BlockHeader>>>,
5302}
5303
5304impl LatestBlockHeaderCache {
5305    pub fn get(&self) -> Arc<BlockHeader> {
5306        self.current.lock().expect("poisoned mutex").clone()
5307    }
5308
5309    pub fn update(&self, header: BlockHeader) {
5310        let new = Arc::new(header);
5311        *self.current.lock().expect("poisoned mutex") = new;
5312    }
5313}
5314
5315#[derive(Debug, Serialize, Deserialize)]
5316pub struct StoreMetadata {
5317    pub schema_version: u64,
5318}
5319
5320impl StoreMetadata {
5321    pub fn new(schema_version: u64) -> Self {
5322        Self { schema_version }
5323    }
5324}
5325
5326/// Reads the schema version from the metadata file, if it exists.
5327///
5328/// Returns `Some(version)` when metadata.json is present and valid,
5329/// or `None` when the file does not exist.
5330fn read_store_schema_version(path: &Path) -> Result<Option<u64>, StoreError> {
5331    let metadata_path = path.join(STORE_METADATA_FILENAME);
5332    if !metadata_path.exists() {
5333        return Ok(None);
5334    }
5335    if !metadata_path.is_file() {
5336        return Err(StoreError::Custom(
5337            "store schema path exists but is not a file".to_string(),
5338        ));
5339    }
5340    let file_contents = std::fs::read_to_string(metadata_path)?;
5341    let metadata: StoreMetadata = serde_json::from_str(&file_contents)?;
5342    Ok(Some(metadata.schema_version))
5343}
5344
5345fn init_metadata_file(parent_path: &Path) -> Result<(), StoreError> {
5346    std::fs::create_dir_all(parent_path)?;
5347
5348    let metadata_path = parent_path.join(STORE_METADATA_FILENAME);
5349    let metadata = StoreMetadata::new(STORE_SCHEMA_VERSION);
5350    let serialized_metadata = serde_json::to_string_pretty(&metadata)?;
5351    let mut new_file = std::fs::File::create_new(metadata_path)?;
5352    new_file.write_all(serialized_metadata.as_bytes())?;
5353    Ok(())
5354}
5355
5356/// Returns `true` if `path` contains a *legacy* database — one written before
5357/// the metadata file existed, so it has no `metadata.json` to identify it.
5358/// Detected by RocksDB's own marker files, as opposed to unrelated files that
5359/// merely share the datadir. Only meaningful once metadata has been confirmed
5360/// absent; otherwise prefer `has_valid_db`, which keys off the metadata file.
5361///
5362/// Previously the caller treated *any* non-empty directory as such a legacy
5363/// database, which made startup fail when unrelated files lived alongside the DB
5364/// — e.g. EthDocker writes the JWT secret into the datadir (issue #5680). We
5365/// instead look for RocksDB's marker files, so a datadir that only contains such
5366/// unrelated files is correctly treated as fresh.
5367fn dir_contains_legacy_db(path: &Path) -> Result<bool, StoreError> {
5368    // `CURRENT` has a fixed name and is written by every RocksDB instance, so
5369    // check for it directly instead of scanning a datadir that may hold many
5370    // unrelated files.
5371    if path.join("CURRENT").is_file() {
5372        return Ok(true);
5373    }
5374    // The manifest has a numeric suffix (`MANIFEST-<n>`), so it can only be
5375    // found by scanning. Restrict to plain files: a directory that happens to
5376    // share the name is not a database marker.
5377    for entry in std::fs::read_dir(path)? {
5378        let entry = entry?;
5379        if !entry.file_type()?.is_file() {
5380            continue;
5381        }
5382        if entry.file_name().to_string_lossy().starts_with("MANIFEST-") {
5383            return Ok(true);
5384        }
5385    }
5386    Ok(false)
5387}
5388
5389/// Checks whether a valid (or migratable) database exists at the given path
5390/// by looking for a metadata.json file with a schema version between 1 and
5391/// `STORE_SCHEMA_VERSION` (inclusive).
5392pub fn has_valid_db(path: &Path) -> bool {
5393    let metadata_path = path.join(STORE_METADATA_FILENAME);
5394    if !metadata_path.is_file() {
5395        return false;
5396    }
5397    let Ok(contents) = std::fs::read_to_string(&metadata_path) else {
5398        return false;
5399    };
5400    let Ok(metadata) = serde_json::from_str::<StoreMetadata>(&contents) else {
5401        return false;
5402    };
5403    metadata.schema_version >= 1 && metadata.schema_version <= STORE_SCHEMA_VERSION
5404}
5405
5406/// Reads the chain ID from an existing database without performing a full
5407/// store initialization. Returns `None` if the database doesn't exist or
5408/// the chain config can't be read. Always returns `None` when compiled
5409/// without the `rocksdb` feature.
5410///
5411/// Each failure mode logs a warning so callers (and operators) can diagnose
5412/// why an existing database was not usable — previously every error was
5413/// silently swallowed by `.ok()?`.
5414pub fn read_chain_id_from_db(path: &Path) -> Option<u64> {
5415    if !has_valid_db(path) {
5416        return None;
5417    }
5418    #[cfg(feature = "rocksdb")]
5419    {
5420        // The cache size is irrelevant for this one-shot chain-id read (the LRU
5421        // is sized as a ceiling, not pre-allocated), so we use the default.
5422        let backend = match RocksDBBackend::open(path, DEFAULT_ROCKSDB_BLOCK_CACHE_SIZE_BYTES) {
5423            Ok(backend) => backend,
5424            Err(e) => {
5425                warn!("Failed to open RocksDB at {path:?} to read chain ID: {e}");
5426                return None;
5427            }
5428        };
5429        let read = match backend.begin_read() {
5430            Ok(read) => read,
5431            Err(e) => {
5432                warn!("Failed to begin read transaction at {path:?}: {e}");
5433                return None;
5434            }
5435        };
5436        let key = chain_data_key(ChainDataIndex::ChainConfig);
5437        let bytes = match read.get(CHAIN_DATA, &key) {
5438            Ok(Some(bytes)) => bytes,
5439            Ok(None) => {
5440                warn!("Chain config entry not found in database at {path:?}");
5441                return None;
5442            }
5443            Err(e) => {
5444                warn!("Failed to read chain config from database at {path:?}: {e}");
5445                return None;
5446            }
5447        };
5448        // Only extract chain_id here: the stored `ChainConfig` JSON may include
5449        // fields whose serialization changed across releases (e.g. pre-v10 wrote
5450        // `terminal_total_difficulty` as a plain number, v10 expects hex string).
5451        // Deserializing the full struct would reject otherwise-migratable v9 data.
5452        #[derive(serde::Deserialize)]
5453        #[serde(rename_all = "camelCase")]
5454        struct ChainIdOnly {
5455            chain_id: u64,
5456        }
5457        match serde_json::from_slice::<ChainIdOnly>(&bytes) {
5458            Ok(partial) => Some(partial.chain_id),
5459            Err(e) => {
5460                warn!("Failed to deserialize chain ID from database at {path:?}: {e}");
5461                None
5462            }
5463        }
5464    }
5465    #[cfg(not(feature = "rocksdb"))]
5466    {
5467        let _ = path;
5468        None
5469    }
5470}
5471
5472#[cfg(test)]
5473mod state_history_tests {
5474    use super::*;
5475    use crate::api::tables::STATE_HISTORY;
5476    use crate::backend::in_memory::InMemoryBackend;
5477    use crate::journal::JournalEntry;
5478    use ethrex_common::types::{BlockBody, BlockHeader};
5479    use ethrex_trie::Nibbles;
5480    use std::time::{Duration, Instant};
5481
5482    fn make_block(number: BlockNumber, parent_hash: H256, state_root: H256) -> Block {
5483        let header = BlockHeader {
5484            number,
5485            parent_hash,
5486            state_root,
5487            ..Default::default()
5488        };
5489        Block::new(header, BlockBody::default())
5490    }
5491
5492    /// Polls `STATE_HISTORY` for an entry at the given block number, up to `timeout`.
5493    /// The trie worker commits to disk asynchronously after `store_block_updates`
5494    /// returns, so a small wait window is required.
5495    fn await_journal_entry(
5496        backend: &Arc<dyn StorageBackend>,
5497        block_number: BlockNumber,
5498        timeout: Duration,
5499    ) -> Option<Vec<u8>> {
5500        let key = block_number.to_be_bytes();
5501        let deadline = Instant::now() + timeout;
5502        loop {
5503            let read = backend.begin_read().ok()?;
5504            if let Ok(Some(v)) = read.get(STATE_HISTORY, &key) {
5505                return Some(v);
5506            }
5507            if Instant::now() >= deadline {
5508                return None;
5509            }
5510            std::thread::sleep(Duration::from_millis(10));
5511        }
5512    }
5513
5514    /// Asserts no STATE_HISTORY entry materializes for `block_number` within the
5515    /// given window. Polls repeatedly; if an entry appears at any poll, fails
5516    /// loudly. Absence at every poll over the full window counts as verified.
5517    /// More robust than a single fixed sleep under CI load.
5518    fn assert_no_journal_entry(backend: &Arc<dyn StorageBackend>, block_number: BlockNumber) {
5519        let window = Duration::from_millis(500);
5520        let key = block_number.to_be_bytes();
5521        let deadline = Instant::now() + window;
5522        loop {
5523            let read = backend.begin_read().expect("read view");
5524            let v = read.get(STATE_HISTORY, &key).expect("get");
5525            assert!(
5526                v.is_none(),
5527                "expected no STATE_HISTORY entry for block {block_number}, got {v:?}"
5528            );
5529            if Instant::now() >= deadline {
5530                return;
5531            }
5532            std::thread::sleep(Duration::from_millis(20));
5533        }
5534    }
5535
5536    /// Live commits gate on the canonical safe-commit root cell (see `get_commitable`),
5537    /// which only advances via forkchoice. We simulate that by calling
5538    /// `set_safe_commit_root` to the parent block's state root before storing the next
5539    /// block: storing block N+1 then commits block N's layer to disk, producing one
5540    /// journal entry per committed block. We verify entries for block 1 and block 2.
5541    #[test]
5542    fn journal_entry_written_per_block_in_regular_mode() {
5543        let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
5544        let dir = tempfile::tempdir().unwrap();
5545        let store = Store::from_backend(
5546            backend.clone(),
5547            dir.path().to_path_buf(),
5548            1,
5549            DEFAULT_PERSIST_CHANNEL_CAPACITY,
5550        )
5551        .unwrap();
5552
5553        let state_root_1 = H256::repeat_byte(0x11);
5554        let block1 = make_block(1, H256::zero(), state_root_1);
5555        let block1_hash = block1.hash();
5556        store
5557            .store_block_updates(UpdateBatch {
5558                account_updates: vec![(Nibbles::from_raw(&[0x00, 0x01], false), vec![0xab, 0xcd])],
5559                storage_updates: vec![],
5560                blocks: vec![block1],
5561                receipts: vec![],
5562                code_updates: vec![],
5563                commit_depth: None,
5564                wait_for_flush: false,
5565            })
5566            .unwrap();
5567
5568        // Advance the safe-commit root to block 1's state root, then store block 2:
5569        // the canonical gate now finds block 1's layer committable and flushes it.
5570        store.set_safe_commit_root(state_root_1).unwrap();
5571        let state_root_2 = H256::repeat_byte(0x22);
5572        let block2 = make_block(2, block1_hash, state_root_2);
5573        let block2_hash = block2.hash();
5574        store
5575            .store_block_updates(UpdateBatch {
5576                account_updates: vec![(Nibbles::from_raw(&[0x00, 0x02], false), vec![0xef, 0x11])],
5577                storage_updates: vec![],
5578                blocks: vec![block2],
5579                receipts: vec![],
5580                code_updates: vec![],
5581                commit_depth: None,
5582                wait_for_flush: false,
5583            })
5584            .unwrap();
5585
5586        let bytes = await_journal_entry(&backend, 1, Duration::from_secs(2))
5587            .expect("STATE_HISTORY entry for block 1 should appear after block 2 commits it");
5588        let entry = JournalEntry::decode(&bytes).unwrap();
5589        assert_eq!(entry.block_hash, block1_hash);
5590        assert_eq!(entry.parent_state_root, H256::zero());
5591        assert!(!entry.account_trie_diff.is_empty());
5592        let (path, prev) = &entry.account_trie_diff[0];
5593        assert_eq!(prev, &None, "first-time write means previous value is None");
5594        assert!(path.len() < 65);
5595
5596        // Advance the safe-commit root to block 2's state root, then store block 3:
5597        // block 2's layer is now committable.
5598        store.set_safe_commit_root(state_root_2).unwrap();
5599        let state_root_3 = H256::repeat_byte(0x33);
5600        let block3 = make_block(3, block2_hash, state_root_3);
5601        store
5602            .store_block_updates(UpdateBatch {
5603                account_updates: vec![(Nibbles::from_raw(&[0x00, 0x03], false), vec![0x77])],
5604                storage_updates: vec![],
5605                blocks: vec![block3],
5606                receipts: vec![],
5607                code_updates: vec![],
5608                commit_depth: None,
5609                wait_for_flush: false,
5610            })
5611            .unwrap();
5612
5613        let bytes = await_journal_entry(&backend, 2, Duration::from_secs(2))
5614            .expect("STATE_HISTORY entry for block 2 should appear after block 3 commits it");
5615        let entry = JournalEntry::decode(&bytes).unwrap();
5616        assert_eq!(entry.block_hash, block2_hash);
5617        assert_eq!(entry.parent_state_root, state_root_1);
5618        assert!(!entry.account_trie_diff.is_empty());
5619    }
5620
5621    /// A depth-gated commit that is NOT in batch mode SHALL journal, and its entries
5622    /// SHALL carry the committed layer's own identity and pre-image.
5623    ///
5624    /// `commit_depth: Some(d)` together with `wait_for_flush: false` — the startup
5625    /// regeneration and sync/import tail paths — is a combination that did not exist while
5626    /// the commit gate and the ack timing were a single flag, so no other test covers it.
5627    /// Journaling follows `wait_for_flush`, so deriving it from `commit_depth.is_some()`
5628    /// instead would silently stop journaling on exactly these paths; this test fails in
5629    /// that case.
5630    #[test]
5631    fn depth_gated_per_block_commit_journals_with_correct_pre_image() {
5632        let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
5633        let dir = tempfile::tempdir().unwrap();
5634        let store = Store::from_backend(
5635            backend.clone(),
5636            dir.path().to_path_buf(),
5637            1,
5638            DEFAULT_PERSIST_CHANNEL_CAPACITY,
5639        )
5640        .unwrap();
5641
5642        // One shared key rewritten every block, so pre-images are checkable.
5643        let shared = Nibbles::from_raw(&[0x0a, 0x0b], false);
5644        let mut prev_hash = H256::zero();
5645        let mut hashes = vec![H256::zero()];
5646        let mut roots = vec![H256::zero()];
5647        for n in 1..=6u64 {
5648            let state_root = H256::repeat_byte(0xc0 | (n as u8));
5649            let block = make_block(n, prev_hash, state_root);
5650            prev_hash = block.hash();
5651            hashes.push(prev_hash);
5652            roots.push(state_root);
5653            store
5654                .store_block_updates(UpdateBatch {
5655                    account_updates: vec![(shared.clone(), vec![0xb0 | (n as u8)])],
5656                    storage_updates: vec![],
5657                    blocks: vec![block],
5658                    receipts: vec![],
5659                    code_updates: vec![],
5660                    commit_depth: Some(3),
5661                    wait_for_flush: false,
5662                })
5663                .unwrap();
5664        }
5665
5666        let bytes = await_journal_entry(&backend, 1, Duration::from_secs(2))
5667            .expect("a depth-gated per-block commit must journal");
5668        let entry = JournalEntry::decode(&bytes).unwrap();
5669        assert_eq!(
5670            entry.block_hash, hashes[1],
5671            "entry 1 carries block 1's identity"
5672        );
5673        assert_eq!(entry.parent_state_root, H256::zero());
5674        assert_eq!(
5675            entry.account_trie_diff[0].1, None,
5676            "block 1 first-writes the key, so its pre-image is absence"
5677        );
5678
5679        let bytes =
5680            await_journal_entry(&backend, 2, Duration::from_secs(2)).expect("entry for block 2");
5681        let entry = JournalEntry::decode(&bytes).unwrap();
5682        assert_eq!(
5683            entry.block_hash, hashes[2],
5684            "entry 2 carries block 2's identity"
5685        );
5686        assert_eq!(
5687            entry.parent_state_root, roots[1],
5688            "entry 2's parent_state_root is block 1's state root"
5689        );
5690        assert_eq!(
5691            entry.account_trie_diff[0].1,
5692            Some(vec![0xb0 | 1u8]),
5693            "block 2's pre-image is the value block 1 wrote"
5694        );
5695    }
5696
5697    /// The bespoke batch path SHALL skip the journal entirely. To actually exercise the
5698    /// gating we push enough batches to trigger a commit under
5699    /// `BATCH_COMMIT_THRESHOLD = 4`, then verify no STATE_HISTORY entry materializes
5700    /// despite the commit happening.
5701    ///
5702    /// That path is `wait_for_flush: true` with `commit_depth: Some(BATCH_COMMIT_THRESHOLD)`
5703    /// — the combination the single `batch_mode` flag used to stand for. Journaling follows
5704    /// `wait_for_flush`, so the depth-gated per-block paths are not covered here.
5705    #[test]
5706    fn journal_skipped_in_batch_mode() {
5707        let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
5708        let dir = tempfile::tempdir().unwrap();
5709        let store = Store::from_backend(
5710            backend.clone(),
5711            dir.path().to_path_buf(),
5712            1,
5713            DEFAULT_PERSIST_CHANNEL_CAPACITY,
5714        )
5715        .unwrap();
5716
5717        let mut prev_hash = H256::zero();
5718        for n in 1..=5u64 {
5719            let state_root = H256::repeat_byte(0xa0 | (n as u8));
5720            let block = make_block(n, prev_hash, state_root);
5721            prev_hash = block.hash();
5722            store
5723                .store_block_updates(UpdateBatch {
5724                    account_updates: vec![(
5725                        Nibbles::from_raw(&[n as u8], false),
5726                        vec![0xde, 0xad, n as u8],
5727                    )],
5728                    storage_updates: vec![],
5729                    blocks: vec![block],
5730                    receipts: vec![],
5731                    code_updates: vec![],
5732                    commit_depth: Some(BATCH_COMMIT_THRESHOLD),
5733                    wait_for_flush: true,
5734                })
5735                .unwrap();
5736        }
5737
5738        for n in 1..=5u64 {
5739            assert_no_journal_entry(&backend, n);
5740        }
5741    }
5742
5743    /// Storage trie updates SHALL appear in `storage_trie_diff` (not
5744    /// `account_trie_diff`), with their on-disk keys as written.
5745    #[test]
5746    fn journal_storage_updates_appear_in_storage_diff() {
5747        let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
5748        let dir = tempfile::tempdir().unwrap();
5749        let store = Store::from_backend(
5750            backend.clone(),
5751            dir.path().to_path_buf(),
5752            1,
5753            DEFAULT_PERSIST_CHANNEL_CAPACITY,
5754        )
5755        .unwrap();
5756
5757        let account_hash_a = H256::repeat_byte(0xa0);
5758        let account_hash_b = H256::repeat_byte(0xb0);
5759
5760        let state_root_1 = H256::repeat_byte(0x33);
5761        let block1 = make_block(1, H256::zero(), state_root_1);
5762        let block1_hash = block1.hash();
5763        store
5764            .store_block_updates(UpdateBatch {
5765                account_updates: vec![],
5766                storage_updates: vec![
5767                    (
5768                        account_hash_a,
5769                        vec![(Nibbles::from_raw(&[0x05], false), vec![0x01])],
5770                    ),
5771                    (
5772                        account_hash_b,
5773                        vec![(Nibbles::from_raw(&[0x06], false), vec![0x02])],
5774                    ),
5775                ],
5776                blocks: vec![block1],
5777                receipts: vec![],
5778                code_updates: vec![],
5779                commit_depth: None,
5780                wait_for_flush: false,
5781            })
5782            .unwrap();
5783
5784        // Advance the safe-commit root to block 1's state root, then store block 2:
5785        // block 1's layer is now committable and flushes to disk.
5786        store.set_safe_commit_root(state_root_1).unwrap();
5787        let state_root_2 = H256::repeat_byte(0x44);
5788        let block2 = make_block(2, block1_hash, state_root_2);
5789        store
5790            .store_block_updates(UpdateBatch {
5791                account_updates: vec![(Nibbles::from_raw(&[0xee], false), vec![0xff])],
5792                storage_updates: vec![],
5793                blocks: vec![block2],
5794                receipts: vec![],
5795                code_updates: vec![],
5796                commit_depth: None,
5797                wait_for_flush: false,
5798            })
5799            .unwrap();
5800
5801        let bytes = await_journal_entry(&backend, 1, Duration::from_secs(2))
5802            .expect("STATE_HISTORY entry for block 1");
5803        let entry = JournalEntry::decode(&bytes).unwrap();
5804        assert_eq!(entry.block_hash, block1_hash);
5805        assert_eq!(
5806            entry.storage_trie_diff.len(),
5807            2,
5808            "two distinct account hashes must produce two storage_trie entries"
5809        );
5810        for (_key, prev) in &entry.storage_trie_diff {
5811            assert_eq!(prev, &None, "first-time storage write has None pre-image");
5812        }
5813        assert!(entry.account_trie_diff.is_empty());
5814    }
5815
5816    fn seed_journal_entries(backend: &Arc<dyn StorageBackend>, block_numbers: &[BlockNumber]) {
5817        let mut tx = backend.begin_write().unwrap();
5818        for n in block_numbers {
5819            let entry = JournalEntry {
5820                block_hash: H256::repeat_byte(*n as u8),
5821                parent_state_root: H256::zero(),
5822                account_trie_diff: vec![(vec![*n as u8], None)],
5823                storage_trie_diff: vec![],
5824                account_flat_diff: vec![],
5825                storage_flat_diff: vec![],
5826            };
5827            tx.put(STATE_HISTORY, &n.to_be_bytes(), &entry.encode())
5828                .unwrap();
5829        }
5830        tx.commit().unwrap();
5831    }
5832
5833    fn journal_entry_exists(backend: &Arc<dyn StorageBackend>, block_number: BlockNumber) -> bool {
5834        backend
5835            .begin_read()
5836            .unwrap()
5837            .get(STATE_HISTORY, &block_number.to_be_bytes())
5838            .unwrap()
5839            .is_some()
5840    }
5841
5842    /// Finality advance SHALL prune every STATE_HISTORY entry at or below the
5843    /// new finalized number, in the same atomic txn as the finalized-number update.
5844    #[tokio::test]
5845    async fn finality_advance_prunes_journal_below_boundary() {
5846        let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
5847        let dir = tempfile::tempdir().unwrap();
5848        let store = Store::from_backend(
5849            backend.clone(),
5850            dir.path().to_path_buf(),
5851            1,
5852            DEFAULT_PERSIST_CHANNEL_CAPACITY,
5853        )
5854        .unwrap();
5855
5856        seed_journal_entries(&backend, &(1..=10).collect::<Vec<_>>());
5857        for n in 1..=10 {
5858            assert!(journal_entry_exists(&backend, n), "seed entry {n} present");
5859        }
5860
5861        store
5862            .forkchoice_update_inner(vec![], 100, H256::zero(), None, Some(5))
5863            .await
5864            .unwrap();
5865
5866        for n in 1..=5 {
5867            assert!(
5868                !journal_entry_exists(&backend, n),
5869                "entry {n} should have been pruned (<= finalized)"
5870            );
5871        }
5872        for n in 6..=10 {
5873            assert!(
5874                journal_entry_exists(&backend, n),
5875                "entry {n} should remain (> finalized)"
5876            );
5877        }
5878    }
5879
5880    /// Forkchoice updates that don't advance finalized SHALL NOT prune the journal.
5881    #[tokio::test]
5882    async fn finality_no_op_does_not_prune() {
5883        let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
5884        let dir = tempfile::tempdir().unwrap();
5885        let store = Store::from_backend(
5886            backend.clone(),
5887            dir.path().to_path_buf(),
5888            1,
5889            DEFAULT_PERSIST_CHANNEL_CAPACITY,
5890        )
5891        .unwrap();
5892
5893        store
5894            .forkchoice_update_inner(vec![], 100, H256::zero(), None, Some(5))
5895            .await
5896            .unwrap();
5897
5898        seed_journal_entries(&backend, &(6..=10).collect::<Vec<_>>());
5899
5900        // FCU re-asserting finalized = 5: must not prune anything.
5901        store
5902            .forkchoice_update_inner(vec![], 100, H256::zero(), None, Some(5))
5903            .await
5904            .unwrap();
5905
5906        for n in 6..=10 {
5907            assert!(
5908                journal_entry_exists(&backend, n),
5909                "entry {n} should still exist after no-op finality update"
5910            );
5911        }
5912
5913        // FCU with finalized = None: also a no-op for pruning.
5914        store
5915            .forkchoice_update_inner(vec![], 100, H256::zero(), None, None)
5916            .await
5917            .unwrap();
5918
5919        for n in 6..=10 {
5920            assert!(
5921                journal_entry_exists(&backend, n),
5922                "entry {n} should still exist when finalized is None"
5923            );
5924        }
5925    }
5926
5927    /// A malformed (wrong-length) `FinalizedBlockNumber` value SHALL surface as a
5928    /// hard error rather than a silent fallback to 0, which would over-prune the
5929    /// journal on the next FCU.
5930    #[tokio::test]
5931    async fn malformed_finalized_returns_error_not_silent_zero() {
5932        let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
5933        let dir = tempfile::tempdir().unwrap();
5934        let store = Store::from_backend(
5935            backend.clone(),
5936            dir.path().to_path_buf(),
5937            1,
5938            DEFAULT_PERSIST_CHANNEL_CAPACITY,
5939        )
5940        .unwrap();
5941
5942        // Plant a 4-byte (instead of 8-byte) FinalizedBlockNumber value.
5943        let mut tx = backend.begin_write().unwrap();
5944        let finalized_key = chain_data_key(ChainDataIndex::FinalizedBlockNumber);
5945        tx.put(CHAIN_DATA, &finalized_key, &[0u8, 0, 0, 0]).unwrap();
5946        tx.commit().unwrap();
5947
5948        seed_journal_entries(&backend, &(1..=5).collect::<Vec<_>>());
5949
5950        let err = store
5951            .forkchoice_update_inner(vec![], 100, H256::zero(), None, Some(3))
5952            .await
5953            .expect_err("malformed finalized must surface as an error");
5954        let msg = format!("{err}");
5955        assert!(
5956            msg.contains("FinalizedBlockNumber has unexpected length"),
5957            "expected length-mismatch error, got: {msg}"
5958        );
5959
5960        // Journal must not have been pruned.
5961        for n in 1..=5 {
5962            assert!(
5963                journal_entry_exists(&backend, n),
5964                "entry {n} must NOT be pruned when finalized read failed"
5965            );
5966        }
5967    }
5968
5969    // -----------------------------------------------------------------------
5970    // Deep-reorg primitive tests.
5971    // -----------------------------------------------------------------------
5972
5973    /// `highest_state_history_block_number` SHALL return the max key present in
5974    /// `STATE_HISTORY`, or `None` when the table is empty.
5975    #[tokio::test]
5976    async fn highest_state_history_block_number_finds_max() {
5977        let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
5978        let dir = tempfile::tempdir().unwrap();
5979        let store = Store::from_backend(
5980            backend.clone(),
5981            dir.path().to_path_buf(),
5982            1,
5983            DEFAULT_PERSIST_CHANNEL_CAPACITY,
5984        )
5985        .unwrap();
5986
5987        assert_eq!(store.highest_state_history_block_number().unwrap(), None);
5988
5989        seed_journal_entries(&backend, &[3, 7, 5, 11, 8]);
5990        assert_eq!(
5991            store.highest_state_history_block_number().unwrap(),
5992            Some(11),
5993            "max over present entries"
5994        );
5995    }
5996
5997    /// `lowest_state_history_block_number` SHALL return the min key present in
5998    /// `STATE_HISTORY`, or `None` when the table is empty. Phase 2's cap fallback
5999    /// depends on this when no finalized hash is known.
6000    #[tokio::test]
6001    async fn lowest_state_history_block_number_finds_min() {
6002        let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
6003        let dir = tempfile::tempdir().unwrap();
6004        let store = Store::from_backend(
6005            backend.clone(),
6006            dir.path().to_path_buf(),
6007            1,
6008            DEFAULT_PERSIST_CHANNEL_CAPACITY,
6009        )
6010        .unwrap();
6011
6012        assert_eq!(store.lowest_state_history_block_number().unwrap(), None);
6013
6014        seed_journal_entries(&backend, &[7, 3, 11, 5, 8]);
6015        assert_eq!(
6016            store.lowest_state_history_block_number().unwrap(),
6017            Some(3),
6018            "min over present entries"
6019        );
6020    }
6021
6022    /// `install_overlay_for_reorg` SHALL atomically swap the layer cache for a
6023    /// fresh empty one with the overlay installed; layer-cache hits in the OLD
6024    /// cache MUST NOT survive the swap.
6025    #[tokio::test]
6026    async fn install_overlay_replaces_cache_with_fresh_one() {
6027        let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
6028        let dir = tempfile::tempdir().unwrap();
6029        let store = Store::from_backend(
6030            backend.clone(),
6031            dir.path().to_path_buf(),
6032            4,
6033            DEFAULT_PERSIST_CHANNEL_CAPACITY,
6034        )
6035        .unwrap();
6036
6037        seed_journal_entries(&backend, &[3, 4]);
6038
6039        // Pre-populate the cache with a sentinel layer to verify the swap discards it.
6040        {
6041            let mut guard = store.trie_cache.write().unwrap();
6042            let mut updated = (**guard).clone();
6043            updated.put_batch(
6044                H256::zero(),
6045                H256::repeat_byte(0xff),
6046                42,
6047                H256::repeat_byte(0x42),
6048                vec![],
6049            );
6050            *guard = Arc::new(updated);
6051        }
6052        assert!(
6053            store
6054                .is_state_in_layer_cache(H256::repeat_byte(0xff))
6055                .unwrap()
6056        );
6057
6058        store
6059            .install_overlay_for_reorg(4, 3, |_| None)
6060            .expect("install must succeed");
6061
6062        assert!(
6063            !store
6064                .is_state_in_layer_cache(H256::repeat_byte(0xff))
6065                .unwrap(),
6066            "old cache layer must be gone after swap"
6067        );
6068        let cache = store.trie_cache.read().unwrap().clone();
6069        assert!(cache.overlay().is_some(), "overlay must be installed");
6070        assert_eq!(
6071            cache.commit_threshold(),
6072            4,
6073            "fresh cache must inherit threshold"
6074        );
6075    }
6076
6077    /// `install_overlay_for_reorg` SHALL leave the existing cache intact when
6078    /// overlay construction fails (missing journal entry).
6079    #[tokio::test]
6080    async fn install_overlay_failure_preserves_cache() {
6081        let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
6082        let dir = tempfile::tempdir().unwrap();
6083        let store = Store::from_backend(
6084            backend.clone(),
6085            dir.path().to_path_buf(),
6086            4,
6087            DEFAULT_PERSIST_CHANNEL_CAPACITY,
6088        )
6089        .unwrap();
6090
6091        // Only seed block 5; constructor will walk down through 4 and 3 and fail.
6092        seed_journal_entries(&backend, &[5]);
6093        {
6094            let mut guard = store.trie_cache.write().unwrap();
6095            let mut updated = (**guard).clone();
6096            updated.put_batch(
6097                H256::zero(),
6098                H256::repeat_byte(0xaa),
6099                7,
6100                H256::repeat_byte(0x77),
6101                vec![],
6102            );
6103            *guard = Arc::new(updated);
6104        }
6105        assert!(
6106            store
6107                .is_state_in_layer_cache(H256::repeat_byte(0xaa))
6108                .unwrap()
6109        );
6110
6111        let err = store
6112            .install_overlay_for_reorg(5, 3, |_| None)
6113            .expect_err("missing entries 3 and 4 must abort");
6114        let msg = format!("{err}");
6115        assert!(
6116            msg.contains("overlay construction failed"),
6117            "error should explain construction failure: {msg}"
6118        );
6119
6120        // Cache must be intact: the sentinel layer must still be there.
6121        assert!(
6122            store
6123                .is_state_in_layer_cache(H256::repeat_byte(0xaa))
6124                .unwrap(),
6125            "cache must survive a failed overlay install"
6126        );
6127        let cache = store.trie_cache.read().unwrap().clone();
6128        assert!(cache.overlay().is_none(), "no overlay should be installed");
6129    }
6130
6131    /// `abort_reorg` SHALL discard both the overlay and any partial new-chain
6132    /// layers, restoring the cache to a fresh empty state with the same
6133    /// commit threshold.
6134    #[tokio::test]
6135    async fn abort_reorg_resets_cache_to_fresh() {
6136        let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
6137        let dir = tempfile::tempdir().unwrap();
6138        let store = Store::from_backend(
6139            backend.clone(),
6140            dir.path().to_path_buf(),
6141            4,
6142            DEFAULT_PERSIST_CHANNEL_CAPACITY,
6143        )
6144        .unwrap();
6145
6146        seed_journal_entries(&backend, &[3, 4]);
6147        store.install_overlay_for_reorg(4, 3, |_| None).unwrap();
6148        // Simulate partial new-chain layer.
6149        {
6150            let mut guard = store.trie_cache.write().unwrap();
6151            let mut updated = (**guard).clone();
6152            updated.put_batch(
6153                H256::zero(),
6154                H256::repeat_byte(0xbb),
6155                4,
6156                H256::repeat_byte(0xb4),
6157                vec![],
6158            );
6159            *guard = Arc::new(updated);
6160        }
6161        assert!(
6162            store
6163                .is_state_in_layer_cache(H256::repeat_byte(0xbb))
6164                .unwrap()
6165        );
6166
6167        store.abort_reorg().unwrap();
6168
6169        assert!(
6170            !store
6171                .is_state_in_layer_cache(H256::repeat_byte(0xbb))
6172                .unwrap(),
6173            "partial new-chain layer must be discarded"
6174        );
6175        let cache = store.trie_cache.read().unwrap().clone();
6176        assert!(cache.overlay().is_none(), "overlay must be cleared");
6177        assert_eq!(
6178            cache.commit_threshold(),
6179            4,
6180            "fresh cache must inherit threshold"
6181        );
6182    }
6183
6184    /// `clear_reorg_overlay` SHALL remove an installed overlay and SHALL be
6185    /// idempotent (safe to call when no overlay is installed).
6186    #[tokio::test]
6187    async fn clear_reorg_overlay_removes_overlay_and_is_idempotent() {
6188        let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
6189        let dir = tempfile::tempdir().unwrap();
6190        let store = Store::from_backend(
6191            backend.clone(),
6192            dir.path().to_path_buf(),
6193            4,
6194            DEFAULT_PERSIST_CHANNEL_CAPACITY,
6195        )
6196        .unwrap();
6197
6198        // Idempotent when no overlay is installed.
6199        store.clear_reorg_overlay().unwrap();
6200        assert!(store.trie_cache.read().unwrap().overlay().is_none());
6201
6202        seed_journal_entries(&backend, &[7]);
6203        store.install_overlay_for_reorg(7, 7, |_| None).unwrap();
6204        assert!(store.trie_cache.read().unwrap().overlay().is_some());
6205
6206        store.clear_reorg_overlay().unwrap();
6207        assert!(store.trie_cache.read().unwrap().overlay().is_none());
6208
6209        // Calling again is still a no-op.
6210        store.clear_reorg_overlay().unwrap();
6211        assert!(store.trie_cache.read().unwrap().overlay().is_none());
6212    }
6213
6214    /// Regression test: journal entries written while the FKV generator
6215    /// was running lack flat-KV pre-images for keys past the generator frontier,
6216    /// and the generator's own flat-KV writes are never journaled. While an
6217    /// overlay serves the read's state root, every flat-KV fast path must be
6218    /// disabled so reads fall back to the (always journaled) trie nodes;
6219    /// otherwise they serve the generator's stale values for the chain being
6220    /// reorged away. Roots the overlay does not serve must keep the fast path.
6221    #[tokio::test]
6222    async fn deep_reorg_overlay_disables_flat_kv_fast_paths() {
6223        let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
6224
6225        // Simulate a finished FKV generation BEFORE the Store boots, so its
6226        // in-memory frontier starts at [0xff; 64] and every path counts as
6227        // "computed" (the Store expands the durable [0xff] sentinel at open).
6228        {
6229            let mut tx = backend.begin_write().unwrap();
6230            tx.put(MISC_VALUES, "last_written".as_bytes(), &[0xff])
6231                .unwrap();
6232            tx.commit().unwrap();
6233        }
6234
6235        let dir = tempfile::tempdir().unwrap();
6236        let store = Store::from_backend(
6237            backend.clone(),
6238            dir.path().to_path_buf(),
6239            1,
6240            DEFAULT_PERSIST_CHANNEL_CAPACITY,
6241        )
6242        .unwrap();
6243
6244        // The pivot state is empty, so the correct value for every read at the
6245        // pivot root is None. The old chain created the account/slot after the
6246        // pivot; the generator then wrote their flat-KV leaves against the old
6247        // chain — writes that are never journaled.
6248        let address = Address::from_low_u64_be(0xbeef);
6249        let account_hash = hash_address_fixed(&address);
6250        let slot = H256::from_low_u64_be(1);
6251        let slot_hash = hash_key_fixed(&slot);
6252        let generator_account = AccountState {
6253            nonce: 7,
6254            balance: U256::from(999u64),
6255            storage_root: H256::zero(),
6256            code_hash: H256::zero(),
6257        };
6258        let generator_slot_value = U256::from(42u64);
6259        {
6260            let mut tx = backend.begin_write().unwrap();
6261            tx.put(
6262                ACCOUNT_FLATKEYVALUE,
6263                &Nibbles::from_bytes(account_hash.as_bytes()).into_vec(),
6264                &generator_account.encode_to_vec(),
6265            )
6266            .unwrap();
6267            tx.put(
6268                STORAGE_FLATKEYVALUE,
6269                &apply_prefix(Some(account_hash), Nibbles::from_bytes(&slot_hash)).into_vec(),
6270                &generator_slot_value.encode_to_vec(),
6271            )
6272            .unwrap();
6273            tx.commit().unwrap();
6274        }
6275
6276        // Journal entries for blocks 10 and 11 above the pivot (block 9), as
6277        // written DURING generation: flat diffs are empty because the keys were
6278        // past the frontier at commit time (the incomplete-journal hole). The pivot's state
6279        // root is the empty trie.
6280        let pivot_root = EMPTY_TRIE_HASH;
6281        {
6282            let mut tx = backend.begin_write().unwrap();
6283            for (n, parent_root) in [(10u64, pivot_root), (11, H256::repeat_byte(0x10))] {
6284                let entry = JournalEntry {
6285                    block_hash: H256::repeat_byte(n as u8),
6286                    parent_state_root: parent_root,
6287                    account_trie_diff: vec![],
6288                    storage_trie_diff: vec![],
6289                    account_flat_diff: vec![],
6290                    storage_flat_diff: vec![],
6291                };
6292                tx.put(STATE_HISTORY, &n.to_be_bytes(), &entry.encode())
6293                    .unwrap();
6294            }
6295            tx.commit().unwrap();
6296        }
6297
6298        store
6299            .install_overlay_for_reorg(11, 10, |_| None)
6300            .expect("overlay install must succeed");
6301
6302        // Reads at the pivot root must return the pivot state (account and slot
6303        // absent), NOT the generator's stale flat-KV values.
6304        let accounts = store
6305            .get_account_states_batch_by_root(pivot_root, &[address])
6306            .unwrap();
6307        assert_eq!(
6308            accounts,
6309            vec![None],
6310            "batch account read must not serve the generator's stale flat-KV value"
6311        );
6312
6313        let slot_value = store
6314            .get_storage_at_root(pivot_root, address, slot)
6315            .unwrap();
6316        assert_eq!(
6317            slot_value, None,
6318            "storage read must not serve the generator's stale flat-KV value"
6319        );
6320
6321        let batched_slots = store
6322            .get_storage_values_batch_by_root(
6323                pivot_root,
6324                &[(hash_address_fixed(&address), EMPTY_TRIE_HASH, slot)],
6325            )
6326            .unwrap();
6327        assert_eq!(
6328            batched_slots,
6329            vec![None],
6330            "batch storage read must not serve the generator's stale flat-KV value"
6331        );
6332
6333        let state_trie = store.open_state_trie(pivot_root).unwrap();
6334        assert_eq!(
6335            state_trie.get(account_hash.as_bytes()).unwrap(),
6336            None,
6337            "trie read must not serve the generator's stale flat-KV value"
6338        );
6339
6340        // Outside the overlay window the fast paths are untouched: a root the
6341        // overlay does not serve still reads flat-KV straight from disk.
6342        let unserved_root = H256::repeat_byte(0x99);
6343        let accounts = store
6344            .get_account_states_batch_by_root(unserved_root, &[address])
6345            .unwrap();
6346        assert_eq!(
6347            accounts,
6348            vec![Some(generator_account)],
6349            "unserved roots must keep the flat-KV fast path"
6350        );
6351        let slot_value = store
6352            .get_storage_at_root(unserved_root, address, slot)
6353            .unwrap();
6354        assert_eq!(
6355            slot_value,
6356            Some(generator_slot_value),
6357            "unserved roots must keep the flat-KV fast path"
6358        );
6359
6360        let batched_slots = store
6361            .get_storage_values_batch_by_root(
6362                unserved_root,
6363                &[(hash_address_fixed(&address), EMPTY_TRIE_HASH, slot)],
6364            )
6365            .unwrap();
6366        assert_eq!(
6367            batched_slots,
6368            vec![Some(generator_slot_value)],
6369            "unserved roots must keep the flat-KV fast path"
6370        );
6371    }
6372
6373    /// Adversarial regression for the multi-layer reconciliation finding: with an
6374    /// overlay installed, a commit targeting a root SEVERAL layers above the pivot
6375    /// must commit ONLY the bottom layer (the pivot tip `T`) per pass. Before the
6376    /// fix this swept every layer at once: the `debug_assert!(len == 1)` panicked in
6377    /// debug builds, and in release the upper layers' journal entries recorded
6378    /// pre-images against the OLD-chain disk state instead of the new-chain/bridge
6379    /// state — silent corruption for a future unwind.
6380    #[tokio::test]
6381    async fn overlay_backed_commit_only_commits_bottom_layer_per_pass() {
6382        let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
6383        let dir = tempfile::tempdir().unwrap();
6384        let store = Store::from_backend(
6385            backend.clone(),
6386            dir.path().to_path_buf(),
6387            4,
6388            DEFAULT_PERSIST_CHANNEL_CAPACITY,
6389        )
6390        .unwrap();
6391
6392        // Pivot at block 9's root; journal entries for blocks 10 and 11 (the old
6393        // chain above the pivot). The overlay serves the pivot root.
6394        let pivot_root = H256::repeat_byte(0x09);
6395        {
6396            let mut tx = backend.begin_write().unwrap();
6397            for (n, parent_root) in [(10u64, pivot_root), (11, H256::repeat_byte(0x0a))] {
6398                let entry = JournalEntry {
6399                    block_hash: H256::repeat_byte(n as u8),
6400                    parent_state_root: parent_root,
6401                    account_trie_diff: vec![(vec![0x00, n as u8], None)],
6402                    storage_trie_diff: vec![],
6403                    account_flat_diff: vec![],
6404                    storage_flat_diff: vec![],
6405                };
6406                tx.put(STATE_HISTORY, &n.to_be_bytes(), &entry.encode())
6407                    .unwrap();
6408            }
6409            tx.commit().unwrap();
6410        }
6411        store.install_overlay_for_reorg(11, 10, |_| None).unwrap();
6412
6413        // Three new-chain layers above the pivot: blocks 10, 11, 12.
6414        let roots: Vec<H256> = (10..=12u8).map(H256::repeat_byte).collect();
6415        {
6416            let mut guard = store.trie_cache.write().unwrap();
6417            let mut updated = (**guard).clone();
6418            let mut parent = pivot_root;
6419            for (i, root) in roots.iter().enumerate() {
6420                let n = 10 + i as u64;
6421                updated.put_batch(
6422                    parent,
6423                    *root,
6424                    n,
6425                    H256::repeat_byte(n as u8),
6426                    vec![(Nibbles::from_raw(&[0x01, n as u8], false), vec![n as u8])],
6427                );
6428                parent = *root;
6429            }
6430            *guard = Arc::new(updated);
6431        }
6432
6433        // Commit targeting the TOP layer (3 deep). With the overlay installed this
6434        // MUST reduce to the bottom layer only.
6435        let trie = store.trie_cache.read().unwrap().clone();
6436        commit_to_disk(
6437            store.backend.as_ref(),
6438            &store.flatkeyvalue_control_tx,
6439            &store.trie_cache,
6440            &trie,
6441            roots[2],
6442            false,
6443        )
6444        .expect("bottom-layer commit must succeed (no debug_assert panic)");
6445
6446        // Only the bottom layer (block 10) is committed and pruned; 11 and 12 stay resident.
6447        assert!(
6448            !store.is_state_in_layer_cache(roots[0]).unwrap(),
6449            "bottom layer must be committed and pruned"
6450        );
6451        assert!(
6452            store.is_state_in_layer_cache(roots[1]).unwrap()
6453                && store.is_state_in_layer_cache(roots[2]).unwrap(),
6454            "upper layers must remain resident after the bottom-layer-only commit"
6455        );
6456        // The overlay was consumed by the reconciliation commit.
6457        assert!(
6458            store.trie_cache.read().unwrap().overlay().is_none(),
6459            "reconciliation must clear the overlay"
6460        );
6461        // Journal: an entry exists for block 10, none yet for 11/12.
6462        assert!(
6463            journal_entry_exists(&backend, 10),
6464            "bottom layer T must be journaled"
6465        );
6466        assert!(!journal_entry_exists(&backend, 11));
6467        assert!(!journal_entry_exists(&backend, 12));
6468
6469        // The reconciliation fold MUST land in the bottom layer's journal entry:
6470        // the overlay's bridge keys (untouched by the layer) appear with their
6471        // old-chain pre-images. This is what block-number-keyed reconciliation
6472        // matching loses when the bottom layer sits above `to_block`.
6473        let entry_bytes = backend
6474            .begin_read()
6475            .unwrap()
6476            .get(STATE_HISTORY, &10u64.to_be_bytes())
6477            .unwrap()
6478            .expect("journal entry for block 10");
6479        let entry = JournalEntry::decode(&entry_bytes).unwrap();
6480        let diff_keys: Vec<&Vec<u8>> = entry.account_trie_diff.iter().map(|(k, _)| k).collect();
6481        assert!(
6482            diff_keys.contains(&&vec![0x00, 10]) && diff_keys.contains(&&vec![0x00, 11]),
6483            "bridge keys from the overlay must be folded into T's journal entry, got {diff_keys:?}"
6484        );
6485
6486        // Second pass: with the overlay gone the remaining backlog commits normally.
6487        let trie = store.trie_cache.read().unwrap().clone();
6488        commit_to_disk(
6489            store.backend.as_ref(),
6490            &store.flatkeyvalue_control_tx,
6491            &store.trie_cache,
6492            &trie,
6493            roots[2],
6494            false,
6495        )
6496        .unwrap();
6497        assert!(!store.is_state_in_layer_cache(roots[1]).unwrap());
6498        assert!(!store.is_state_in_layer_cache(roots[2]).unwrap());
6499        assert!(journal_entry_exists(&backend, 11));
6500        assert!(journal_entry_exists(&backend, 12));
6501    }
6502
6503    /// Adversarial regression for the journal-pruning race: while a deep-reorg
6504    /// apply pass holds the pause flag, a finality advance must NOT prune
6505    /// STATE_HISTORY (a concurrent `Overlay::from_journal` reads entries with no
6506    /// snapshot isolation). Pruning must catch up once the pause is released.
6507    #[tokio::test]
6508    async fn journal_pruning_pauses_during_reorg_and_catches_up_after() {
6509        let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
6510        let dir = tempfile::tempdir().unwrap();
6511        let store = Store::from_backend(
6512            backend.clone(),
6513            dir.path().to_path_buf(),
6514            1,
6515            DEFAULT_PERSIST_CHANNEL_CAPACITY,
6516        )
6517        .unwrap();
6518
6519        seed_journal_entries(&backend, &(1..=5).collect::<Vec<_>>());
6520
6521        // Paused: finality advance to 3 must not prune anything.
6522        store.set_journal_pruning_paused(true);
6523        store
6524            .forkchoice_update_inner(vec![], 100, H256::zero(), None, Some(3))
6525            .await
6526            .unwrap();
6527        for n in 1..=5 {
6528            assert!(
6529                journal_entry_exists(&backend, n),
6530                "entry {n} must survive pruning while the reorg pause is held"
6531            );
6532        }
6533
6534        // Released: the next advance prunes cumulatively from zero.
6535        store.set_journal_pruning_paused(false);
6536        store
6537            .forkchoice_update_inner(vec![], 100, H256::zero(), None, Some(4))
6538            .await
6539            .unwrap();
6540        for n in 1..=4 {
6541            assert!(
6542                !journal_entry_exists(&backend, n),
6543                "entry {n} must be pruned once the pause is released"
6544            );
6545        }
6546        assert!(journal_entry_exists(&backend, 5));
6547    }
6548}
6549
6550#[cfg(test)]
6551mod merge_tests {
6552    use super::*;
6553
6554    fn h256(b: u8) -> H256 {
6555        H256::from_low_u64_be(b as u64)
6556    }
6557
6558    fn op(bn: BlockNumber, bh: H256, idx: Index) -> Vec<u8> {
6559        encode_tx_location_operand(bn, bh, idx)
6560    }
6561
6562    fn decode(v: &[u8]) -> Vec<(BlockNumber, BlockHash, Index)> {
6563        <Vec<(BlockNumber, BlockHash, Index)>>::decode(v).unwrap()
6564    }
6565
6566    #[test]
6567    fn single_operand_on_empty_base() {
6568        let out = tx_locations_merge(None, vec![op(100, h256(0x10), 0)]).unwrap();
6569        assert_eq!(decode(&out), vec![(100, h256(0x10), 0)]);
6570    }
6571
6572    #[test]
6573    fn operand_appended_to_existing_base() {
6574        let base = vec![(100u64, h256(0x10), 0u64)].encode_to_vec();
6575        let out = tx_locations_merge(Some(&base), vec![op(101, h256(0x11), 5)]).unwrap();
6576        let mut got = decode(&out);
6577        got.sort();
6578        let mut want = vec![(100, h256(0x10), 0), (101, h256(0x11), 5)];
6579        want.sort();
6580        assert_eq!(got, want);
6581    }
6582
6583    #[test]
6584    fn multiple_operands_combined() {
6585        let out = tx_locations_merge(
6586            None,
6587            vec![
6588                op(100, h256(0x10), 0),
6589                op(100, h256(0x11), 1),
6590                op(101, h256(0x12), 2),
6591            ],
6592        )
6593        .unwrap();
6594        assert_eq!(decode(&out).len(), 3);
6595    }
6596
6597    #[test]
6598    fn same_block_hash_is_deduped() {
6599        // Two operands with the same block_hash: the later one replaces the earlier.
6600        let out =
6601            tx_locations_merge(None, vec![op(100, h256(0x10), 0), op(100, h256(0x10), 7)]).unwrap();
6602        assert_eq!(decode(&out), vec![(100, h256(0x10), 7)]);
6603    }
6604
6605    #[test]
6606    fn malformed_operand_aborts_merge() {
6607        // Fail loud: a malformed operand must abort the merge (return None), not
6608        // silently drop it and commit a partial result.
6609        let out = tx_locations_merge(None, vec![vec![0xff, 0xff], op(100, h256(0x10), 0)]);
6610        assert!(out.is_none(), "merge must abort on a malformed operand");
6611    }
6612
6613    #[test]
6614    fn malformed_base_value_aborts_merge() {
6615        let out = tx_locations_merge(Some(&[0xff, 0xff]), vec![op(100, h256(0x10), 0)]);
6616        assert!(out.is_none(), "merge must abort on a corrupt base value");
6617    }
6618
6619    /// Regression for the associative-merge format bug: a PartialMerge result
6620    /// must be re-mergeable as an operand. RocksDB folds operands together
6621    /// without a base value during compaction, then feeds that result back into
6622    /// a later merge. If the operand format differed from the output format,
6623    /// the re-fed result would fail to decode and entries would be dropped
6624    /// (observed as 1664 silent drops during a compaction pass on mainnet).
6625    #[test]
6626    fn partial_merge_result_is_a_valid_operand() {
6627        // Step 1: PartialMerge — combine operands with NO base value.
6628        let partial =
6629            tx_locations_merge(None, vec![op(100, h256(0x10), 0), op(101, h256(0x11), 1)]).unwrap();
6630
6631        // Step 2: the partial result is now itself an operand in a later merge,
6632        // on top of an existing base value. This is the path that used to drop
6633        // entries.
6634        let base = vec![(99u64, h256(0x09), 9u64)].encode_to_vec();
6635        let out = tx_locations_merge(Some(&base), vec![partial]).unwrap();
6636
6637        let mut got = decode(&out);
6638        got.sort();
6639        let mut want = vec![
6640            (99, h256(0x09), 9),
6641            (100, h256(0x10), 0),
6642            (101, h256(0x11), 1),
6643        ];
6644        want.sort();
6645        assert_eq!(
6646            got, want,
6647            "no entries may be lost when re-merging a partial result"
6648        );
6649    }
6650
6651    /// Operand and stored-value encodings must be byte-identical types, so a
6652    /// freshly-encoded operand round-trips through the value decoder.
6653    #[test]
6654    fn operand_encoding_matches_value_encoding() {
6655        let operand = op(100, h256(0x10), 3);
6656        // Decoding the operand as the stored Vec type must succeed.
6657        assert_eq!(decode(&operand), vec![(100, h256(0x10), 3)]);
6658    }
6659
6660    /// Chained PartialMerges (operand-only folds applied repeatedly) stay valid.
6661    #[test]
6662    fn chained_partial_merges() {
6663        let p1 = tx_locations_merge(None, vec![op(1, h256(0x01), 0)]).unwrap();
6664        let p2 = tx_locations_merge(None, vec![p1, op(2, h256(0x02), 0)]).unwrap();
6665        let p3 = tx_locations_merge(None, vec![p2, op(3, h256(0x03), 0)]).unwrap();
6666        let out = tx_locations_merge(None, vec![p3]).unwrap();
6667        assert_eq!(decode(&out).len(), 3);
6668    }
6669}
6670
6671#[cfg(test)]
6672mod backfill_write_tests {
6673    use super::*;
6674    use ethrex_common::types::{EIP1559Transaction, TxType};
6675
6676    fn test_header(number: BlockNumber) -> BlockHeader {
6677        BlockHeader {
6678            number,
6679            ..Default::default()
6680        }
6681    }
6682
6683    fn test_tx(nonce: u64) -> Transaction {
6684        Transaction::EIP1559Transaction(EIP1559Transaction {
6685            nonce,
6686            ..Default::default()
6687        })
6688    }
6689
6690    fn test_receipt() -> Receipt {
6691        Receipt {
6692            tx_type: TxType::EIP1559,
6693            succeeded: true,
6694            cumulative_gas_used: 21_000,
6695            logs: vec![],
6696            payer: None,
6697            frame_receipts: None,
6698        }
6699    }
6700
6701    /// `add_backfilled_blocks` writes the body + receipts + transaction index and
6702    /// lowers the frontier (`earliest_block_number`) in one shot; the indexed tx
6703    /// is then locatable for its (canonical) block.
6704    #[tokio::test]
6705    async fn writes_data_indexes_txs_and_lowers_frontier() {
6706        let store = Store::new("", EngineType::InMemory).expect("in-memory store");
6707        store.update_earliest_block_number(100).await.unwrap();
6708
6709        let header = test_header(99);
6710        let hash = header.hash();
6711        let tx = test_tx(7);
6712        let body = BlockBody {
6713            transactions: vec![tx.clone()],
6714            ommers: vec![],
6715            withdrawals: Some(vec![]),
6716        };
6717        // In production the header is already stored and canonical from snap
6718        // header backfill; mirror the canonical mapping so the tx-index lookup
6719        // (which is canonical-filtered) can resolve.
6720        store.add_block_headers(vec![header.clone()]).await.unwrap();
6721        store
6722            .forkchoice_update(vec![(99, hash)], 99, hash, None, None)
6723            .await
6724            .unwrap();
6725
6726        store
6727            .add_backfilled_blocks(
6728                vec![BackfilledBlock {
6729                    header,
6730                    body,
6731                    receipts: vec![test_receipt()],
6732                    index_transactions: true,
6733                }],
6734                99,
6735            )
6736            .await
6737            .unwrap();
6738
6739        assert_eq!(
6740            store.get_earliest_block_number().await.unwrap(),
6741            99,
6742            "frontier must be lowered atomically with the data write"
6743        );
6744        assert!(
6745            store.get_block_body_by_hash(hash).await.unwrap().is_some(),
6746            "body must be stored"
6747        );
6748        assert_eq!(
6749            store.get_receipts_for_block(&hash).await.unwrap().len(),
6750            1,
6751            "receipts must be stored"
6752        );
6753        assert_eq!(
6754            store
6755                .get_transaction_location(tx.hash(&NativeCrypto))
6756                .await
6757                .unwrap(),
6758            Some((99, hash, 0)),
6759            "indexed transaction must be locatable"
6760        );
6761    }
6762
6763    /// With `index_transactions = false` (a block outside the
6764    /// `--history.transactions` horizon) the body and receipts are still written,
6765    /// but the transaction index is skipped.
6766    #[tokio::test]
6767    async fn skips_tx_index_when_disabled() {
6768        let store = Store::new("", EngineType::InMemory).expect("in-memory store");
6769        store.update_earliest_block_number(100).await.unwrap();
6770
6771        let header = test_header(99);
6772        let hash = header.hash();
6773        let tx = test_tx(7);
6774        let body = BlockBody {
6775            transactions: vec![tx.clone()],
6776            ommers: vec![],
6777            withdrawals: Some(vec![]),
6778        };
6779        store.add_block_headers(vec![header.clone()]).await.unwrap();
6780        store
6781            .forkchoice_update(vec![(99, hash)], 99, hash, None, None)
6782            .await
6783            .unwrap();
6784
6785        store
6786            .add_backfilled_blocks(
6787                vec![BackfilledBlock {
6788                    header,
6789                    body,
6790                    receipts: vec![test_receipt()],
6791                    index_transactions: false,
6792                }],
6793                99,
6794            )
6795            .await
6796            .unwrap();
6797
6798        assert!(
6799            store.get_block_body_by_hash(hash).await.unwrap().is_some(),
6800            "body must still be stored"
6801        );
6802        assert_eq!(
6803            store.get_receipts_for_block(&hash).await.unwrap().len(),
6804            1,
6805            "receipts must still be stored"
6806        );
6807        assert_eq!(
6808            store
6809                .get_transaction_location(tx.hash(&NativeCrypto))
6810                .await
6811                .unwrap(),
6812            None,
6813            "transaction index must be skipped when index_transactions is false"
6814        );
6815    }
6816
6817    /// End-to-end at the storage/RPC boundary: the getters the RPC handlers use
6818    /// (`get_block_body`, `get_transaction_by_hash`, `get_receipt`) return nothing
6819    /// for a headers-only (post-snap) block and real data after backfill.
6820    #[tokio::test]
6821    async fn historical_getters_transition_from_null_to_present() {
6822        let store = Store::new("", EngineType::InMemory).expect("in-memory store");
6823        store.update_earliest_block_number(100).await.unwrap();
6824
6825        let header = test_header(99);
6826        let hash = header.hash();
6827        let tx = test_tx(7);
6828        let tx_hash = tx.hash(&NativeCrypto);
6829        let body = BlockBody {
6830            transactions: vec![tx],
6831            ommers: vec![],
6832            withdrawals: Some(vec![]),
6833        };
6834        // Post-snap state: header stored and canonical, but no body/receipts.
6835        store.add_block_headers(vec![header.clone()]).await.unwrap();
6836        store
6837            .forkchoice_update(vec![(99, hash)], 99, hash, None, None)
6838            .await
6839            .unwrap();
6840
6841        // Previously-null historical queries.
6842        assert!(
6843            store.get_block_body(99).await.unwrap().is_none(),
6844            "body absent before backfill"
6845        );
6846        assert!(
6847            store
6848                .get_transaction_by_hash(tx_hash)
6849                .await
6850                .unwrap()
6851                .is_none(),
6852            "tx unlocatable before backfill"
6853        );
6854        assert!(
6855            store.get_receipt(99, 0).await.unwrap().is_none(),
6856            "receipt absent before backfill"
6857        );
6858
6859        store
6860            .add_backfilled_blocks(
6861                vec![BackfilledBlock {
6862                    header,
6863                    body,
6864                    receipts: vec![test_receipt()],
6865                    index_transactions: true,
6866                }],
6867                99,
6868            )
6869            .await
6870            .unwrap();
6871
6872        // The same queries now return real data.
6873        assert!(
6874            store.get_block_body(99).await.unwrap().is_some(),
6875            "body present after backfill"
6876        );
6877        assert!(
6878            store
6879                .get_transaction_by_hash(tx_hash)
6880                .await
6881                .unwrap()
6882                .is_some(),
6883            "tx locatable after backfill"
6884        );
6885        assert!(
6886            store.get_receipt(99, 0).await.unwrap().is_some(),
6887            "receipt present after backfill"
6888        );
6889    }
6890
6891    /// Backfill fills downward in batches; the frontier lowers after each batch,
6892    /// persists (doubling as the resume cursor), and leaves no gaps.
6893    #[tokio::test]
6894    async fn resume_fills_contiguously_without_gaps() {
6895        let store = Store::new("", EngineType::InMemory).expect("in-memory store");
6896        store.update_earliest_block_number(100).await.unwrap();
6897
6898        let make_batch = |range: std::ops::Range<u64>| -> Vec<BackfilledBlock> {
6899            range
6900                .rev()
6901                .map(|n| BackfilledBlock {
6902                    header: test_header(n),
6903                    body: BlockBody {
6904                        transactions: vec![],
6905                        ommers: vec![],
6906                        withdrawals: Some(vec![]),
6907                    },
6908                    receipts: vec![],
6909                    index_transactions: false,
6910                })
6911                .collect()
6912        };
6913
6914        // Batch 1: fill [90, 99], lowering the frontier to 90.
6915        store
6916            .add_backfilled_blocks(make_batch(90..100), 90)
6917            .await
6918            .unwrap();
6919        assert_eq!(store.get_earliest_block_number().await.unwrap(), 90);
6920
6921        // Simulate a restart: the task resumes from the persisted frontier.
6922        let resume_from = store.get_earliest_block_number().await.unwrap();
6923        assert_eq!(resume_from, 90, "resume reads the persisted frontier");
6924
6925        // Batch 2: fill [80, 89], lowering the frontier to 80.
6926        store
6927            .add_backfilled_blocks(make_batch(80..90), 80)
6928            .await
6929            .unwrap();
6930        assert_eq!(store.get_earliest_block_number().await.unwrap(), 80);
6931
6932        // No gaps: every block in [80, 99] has a stored body.
6933        for n in 80..100 {
6934            assert!(
6935                store
6936                    .get_block_body_by_hash(test_header(n).hash())
6937                    .await
6938                    .unwrap()
6939                    .is_some(),
6940                "block {n} must have a body (no gap left by resume)"
6941            );
6942        }
6943    }
6944}
6945
6946#[cfg(test)]
6947mod datadir_tests {
6948    use super::*;
6949    use std::fs;
6950
6951    #[test]
6952    fn empty_dir_has_no_existing_db() {
6953        let dir = tempfile::tempdir().unwrap();
6954        assert!(!dir_contains_legacy_db(dir.path()).unwrap());
6955    }
6956
6957    #[test]
6958    fn dir_with_only_unrelated_files_has_no_existing_db() {
6959        // Regression for #5680: a JWT secret (or any unrelated file) in the
6960        // datadir must not be mistaken for an existing database.
6961        let dir = tempfile::tempdir().unwrap();
6962        fs::write(dir.path().join("jwt.hex"), "0xdeadbeef").unwrap();
6963        fs::write(dir.path().join("LOG"), "noise").unwrap();
6964        assert!(!dir_contains_legacy_db(dir.path()).unwrap());
6965    }
6966
6967    #[test]
6968    fn dir_with_rocksdb_markers_has_existing_db() {
6969        // A `CURRENT` file (and, separately, a `MANIFEST-*` file) marks a real DB.
6970        let dir = tempfile::tempdir().unwrap();
6971        fs::write(dir.path().join("CURRENT"), "MANIFEST-000001\n").unwrap();
6972        assert!(dir_contains_legacy_db(dir.path()).unwrap());
6973
6974        let dir2 = tempfile::tempdir().unwrap();
6975        fs::write(dir2.path().join("MANIFEST-000007"), "x").unwrap();
6976        assert!(dir_contains_legacy_db(dir2.path()).unwrap());
6977    }
6978
6979    #[test]
6980    fn dir_with_marker_named_subdirectories_has_no_existing_db() {
6981        // A *directory* named like a marker file must not be mistaken for a DB;
6982        // RocksDB only ever visits these as plain files.
6983        let dir = tempfile::tempdir().unwrap();
6984        fs::create_dir(dir.path().join("CURRENT")).unwrap();
6985        fs::create_dir(dir.path().join("MANIFEST-000001")).unwrap();
6986        assert!(!dir_contains_legacy_db(dir.path()).unwrap());
6987    }
6988}
6989
6990#[cfg(test)]
6991mod flatkeyvalue_completeness_tests {
6992    use super::*;
6993
6994    /// The `last_written` marker signals a finished FKV pass ONLY as the exact 1-byte
6995    /// `[0xff]` sentinel. This gates journal-backed deep reorgs, so the
6996    /// boundary must be exact: an unset marker, the initial all-zero frontier, and any
6997    /// mid-generation nibble path must all read as "not complete".
6998    #[test]
6999    fn only_the_one_byte_ff_sentinel_counts_as_complete() {
7000        // Complete: the durable completion sentinel the generator writes.
7001        assert!(Store::flatkeyvalue_generation_complete(Some(&[0xff])));
7002
7003        // Not complete:
7004        assert!(!Store::flatkeyvalue_generation_complete(None)); // marker never written
7005        assert!(!Store::flatkeyvalue_generation_complete(Some(&[]))); // empty
7006        assert!(!Store::flatkeyvalue_generation_complete(Some(&[0u8; 64]))); // initial frontier
7007        assert!(!Store::flatkeyvalue_generation_complete(Some(&[0x0a; 64]))); // mid-gen nibble path
7008        // The in-memory frontier is expanded to all-0xff (64/131 bytes), but that is never
7009        // the durable marker; only the 1-byte form means complete, so these read false.
7010        assert!(!Store::flatkeyvalue_generation_complete(Some(&[0xff; 64])));
7011        assert!(!Store::flatkeyvalue_generation_complete(Some(&[
7012            0xff, 0xff
7013        ])));
7014    }
7015}