Skip to main content

ethrex_storage/
store.rs

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