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