Skip to main content

ethrex_storage/
store.rs

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