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