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            BLOCK_ACCESS_LISTS, BLOCK_NUMBERS, BODIES, CANONICAL_BLOCK_HASHES, CHAIN_DATA,
10            EXECUTION_WITNESSES, FULLSYNC_HEADERS, HEADERS, INVALID_CHAINS, MISC_VALUES,
11            PENDING_BLOCKS, RECEIPTS_V2, SNAP_STATE, STORAGE_FLATKEYVALUE, STORAGE_TRIE_NODES,
12            TRANSACTION_LOCATIONS,
13        },
14    },
15    apply_prefix,
16    backend::in_memory::InMemoryBackend,
17    block_data_buffer::BlockDataBuffer,
18    error::StoreError,
19    layering::{TrieLayerCache, TrieWrapper},
20    rlp::{BlockBodyRLP, BlockHeaderRLP, BlockRLP},
21    trie::{BackendTrieDB, BackendTrieDBLocked},
22    utils::{ChainDataIndex, SnapStateIndex},
23};
24
25use ethrex_common::{
26    Address, H256, U256,
27    types::{
28        AccountInfo, AccountState, AccountUpdate, Block, BlockBody, BlockHash, BlockHeader,
29        BlockNumber, ChainConfig, Code, CodeMetadata, ForkId, Genesis, GenesisAccount, Index,
30        Receipt, Transaction,
31        block_access_list::BlockAccessList,
32        block_execution_witness::{ExecutionWitness, RpcExecutionWitness},
33    },
34    utils::keccak,
35};
36use ethrex_crypto::{NativeCrypto, keccak::keccak_hash};
37use ethrex_rlp::{
38    decode::{RLPDecode, decode_bytes},
39    encode::RLPEncode,
40};
41use ethrex_trie::{EMPTY_TRIE_HASH, Nibbles, Trie, TrieLogger, TrieNode, TrieWitness};
42use ethrex_trie::{Node, NodeRLP};
43use lru::LruCache;
44use rayon::prelude::*;
45use rustc_hash::FxBuildHasher;
46use serde::{Deserialize, Serialize};
47use std::{
48    collections::{BTreeMap, HashMap, HashSet, hash_map::Entry},
49    fmt::Debug,
50    io::Write,
51    path::{Path, PathBuf},
52    sync::{
53        Arc, Condvar, Mutex, RwLock,
54        atomic::{AtomicUsize, Ordering},
55        mpsc::{SyncSender, TryRecvError, sync_channel},
56    },
57    thread::JoinHandle,
58};
59use tracing::{debug, error, info, warn};
60
61/// Maximum number of execution witnesses to keep in the database
62pub const MAX_WITNESSES: u64 = 128;
63
64// We use one constant for in-memory and another for on-disk backends.
65// This is due to tests requiring state older than 128 blocks.
66// TODO: unify these
67#[allow(unused)]
68const DB_COMMIT_THRESHOLD: usize = 128;
69const IN_MEMORY_COMMIT_THRESHOLD: usize = 10000;
70
71/// Commit threshold for batch (full sync) mode. Each batch layer holds ~1024
72/// blocks of trie diffs (~1 GB), so we flush aggressively to bound memory.
73const BATCH_COMMIT_THRESHOLD: usize = 4;
74
75/// Default size in bytes of the RocksDB shared block cache: 12 GiB.
76///
77/// This cache holds both data blocks AND the index/bloom-filter blocks for every
78/// open SST file (because we enable `cache_index_and_filter_blocks`), so its size
79/// is the effective upper bound on RocksDB's resident memory footprint. 12 GiB
80/// keeps the filter/index working set resident plus hot EVM state; a sweep on a
81/// synced mainnet node (32 GiB cap) found 8-16 GiB all keep up with head-following,
82/// with larger giving no gain (the OS page cache backstops the uncompressed state
83/// CFs) and ~8 GiB the floor where the filter set starts to thrash.
84pub const DEFAULT_ROCKSDB_BLOCK_CACHE_SIZE_BYTES: usize = 12 * 1024 * 1024 * 1024;
85
86/// Tunable configuration for [`Store::new_with_config`] and related constructors.
87///
88/// Use [`StoreConfig::default()`] for production-tuned defaults; callers that
89/// don't need to override anything should keep calling [`Store::new`] directly.
90#[derive(Debug, Clone, Copy)]
91pub struct StoreConfig {
92    /// Size in bytes of the RocksDB shared block cache. With
93    /// `cache_index_and_filter_blocks` enabled (the ethrex default), this is
94    /// the effective ceiling on RocksDB's resident memory. Ignored for
95    /// in-memory backends.
96    pub rocksdb_block_cache_size: usize,
97    /// Bound on the persist worker's channel: number of staged (acked) live
98    /// messages whose flush may still be in flight. Once full, the next send
99    /// blocks — that is the backpressure that throttles `newPayload`.
100    /// Clamped to `max(1)` at construction (0 would make a rendezvous channel).
101    pub persist_channel_capacity: usize,
102}
103
104impl Default for StoreConfig {
105    fn default() -> Self {
106        Self {
107            rocksdb_block_cache_size: DEFAULT_ROCKSDB_BLOCK_CACHE_SIZE_BYTES,
108            persist_channel_capacity: DEFAULT_PERSIST_CHANNEL_CAPACITY,
109        }
110    }
111}
112
113/// Control messages for the FlatKeyValue generator
114#[derive(Debug, PartialEq)]
115enum FKVGeneratorControlMessage {
116    Stop,
117    Continue,
118}
119
120// 64mb
121const CODE_CACHE_MAX_SIZE: u64 = 64 * 1024 * 1024;
122
123/// Key used to persist the `flushed_upto` block number in `MISC_VALUES`.
124const FLUSHED_UPTO_KEY: &[u8] = b"bodies_flushed_upto";
125
126#[derive(Debug)]
127struct CodeCache {
128    inner_cache: LruCache<H256, Code, FxBuildHasher>,
129    cache_size: u64,
130}
131
132impl Default for CodeCache {
133    fn default() -> Self {
134        Self {
135            inner_cache: LruCache::unbounded_with_hasher(FxBuildHasher),
136            cache_size: 0,
137        }
138    }
139}
140
141impl CodeCache {
142    fn get(&mut self, code_hash: &H256) -> Result<Option<Code>, StoreError> {
143        Ok(self.inner_cache.get(code_hash).cloned())
144    }
145
146    fn insert(&mut self, code: &Code) -> Result<(), StoreError> {
147        let code_size = code.size();
148        let cache_len = self.inner_cache.len() + 1;
149        self.cache_size += code_size as u64;
150        let current_size = self.cache_size;
151        debug!(
152            "[ACCOUNT CODE CACHE] cache elements (): {cache_len}, total size: {current_size} bytes"
153        );
154
155        while self.cache_size > CODE_CACHE_MAX_SIZE {
156            if let Some((_, code)) = self.inner_cache.pop_lru() {
157                self.cache_size -= code.size() as u64;
158            } else {
159                break;
160            }
161        }
162
163        self.inner_cache.get_or_insert(code.hash, || code.clone());
164        Ok(())
165    }
166}
167
168/// Main storage interface for the ethrex client.
169///
170/// `Store` is `Clone` and thread-safe; all clones share the same backend and
171/// caches via `Arc`. Reads consult an in-memory block-data buffer before disk
172/// so not-yet-flushed blocks are always visible.
173#[derive(Debug, Clone)]
174pub struct Store {
175    /// Path to the database directory.
176    db_path: PathBuf,
177    /// Storage backend (InMemory or RocksDB).
178    backend: Arc<dyn StorageBackend>,
179    /// Chain configuration (fork schedule, chain ID, etc.).
180    chain_config: ChainConfig,
181    /// Cache for trie nodes from recent blocks.
182    trie_cache: Arc<RwLock<Arc<TrieLayerCache>>>,
183    /// Channel for controlling the FlatKeyValue generator background task.
184    flatkeyvalue_control_tx: std::sync::mpsc::SyncSender<FKVGeneratorControlMessage>,
185    /// In-memory overlay of block data not yet flushed to disk.
186    block_data_buffer: Arc<RwLock<Arc<BlockDataBuffer>>>,
187    /// Channel to the single persist worker (`apply_updates` → `PersistMessage::Block`,
188    /// `wait_for_persistence_idle` → `PersistMessage::Ping`). The worker is the
189    /// sole mutator of `block_data_buffer` in production.
190    persist_tx: std::sync::mpsc::SyncSender<PersistMessage>,
191    /// Roots whose trie diff-layer is being built but not yet installed in
192    /// `trie_cache`. Trie opens block on these so a just-added block's state is
193    /// never read as stale before its layer lands.
194    pending_trie_roots: Arc<PendingTrieRoots>,
195    /// Cached latest canonical block header. May be slightly stale, which is
196    /// acceptable for RPC "latest" queries and sync operations.
197    latest_block_header: LatestBlockHeaderCache,
198    /// Last computed FlatKeyValue for incremental updates.
199    last_computed_flatkeyvalue: Arc<RwLock<Vec<u8>>>,
200
201    /// Cache for account bytecodes, keyed by the bytecode hash.
202    /// Note that we don't remove entries on account code changes, since
203    /// those changes already affect the code hash stored in the account, and only
204    /// may result in this cache having useless data.
205    account_code_cache: Arc<Mutex<CodeCache>>,
206
207    /// Cache for code metadata (code length), keyed by the bytecode hash.
208    /// Uses FxHashMap for efficient lookups, much smaller than code cache.
209    code_metadata_cache: Arc<Mutex<rustc_hash::FxHashMap<H256, CodeMetadata>>>,
210
211    /// Serializes concurrent `forkchoice_update` callers so that the cache
212    /// update and the DB write transaction remain mutually ordered.
213    fcu_lock: Arc<tokio::sync::Mutex<()>>,
214
215    background_threads: Arc<ThreadList>,
216}
217
218#[derive(Debug, Default)]
219struct ThreadList {
220    list: Vec<JoinHandle<()>>,
221}
222
223impl Drop for ThreadList {
224    fn drop(&mut self) {
225        for handle in self.list.drain(..) {
226            let _ = handle.join();
227        }
228    }
229}
230
231/// Storage trie nodes grouped by account address hash.
232///
233/// Each entry contains the hashed account address and the trie nodes
234/// for that account's storage trie.
235pub type StorageTrieNodes = Vec<(H256, Vec<(Nibbles, Vec<u8>)>)>;
236type StorageTries = HashMap<Address, (TrieWitness, Trie)>;
237
238/// Storage backend type selection.
239///
240/// Used when creating a new [`Store`] to specify which backend to use.
241#[derive(Debug, Clone, Copy, PartialEq, Eq)]
242pub enum EngineType {
243    /// In-memory storage, non-persistent. Suitable for testing.
244    InMemory,
245    /// RocksDB storage, persistent. Suitable for production.
246    #[cfg(feature = "rocksdb")]
247    RocksDB,
248}
249
250/// Batch of updates to apply to the store atomically.
251///
252/// Used during block execution to collect all state changes before
253/// committing them to the database in a single transaction.
254pub struct UpdateBatch {
255    /// New nodes to add to the state trie.
256    pub account_updates: Vec<TrieNode>,
257    /// Storage trie updates per account (keyed by hashed address).
258    pub storage_updates: Vec<(H256, Vec<TrieNode>)>,
259    /// Blocks to store.
260    pub blocks: Vec<Block>,
261    /// Receipts to store, grouped by block hash.
262    pub receipts: Vec<(H256, Vec<Receipt>)>,
263    /// Contract code updates (code hash -> bytecode).
264    pub code_updates: Vec<(H256, Code)>,
265    /// Whether this batch comes from full sync (batch execution mode).
266    /// When true, uses `BATCH_COMMIT_THRESHOLD` (aggressive) instead of
267    /// `DB_COMMIT_THRESHOLD` to bound memory during bulk block import.
268    pub batch_mode: bool,
269}
270
271/// Storage trie updates grouped by account address hash.
272pub type StorageUpdates = Vec<(H256, Vec<(Nibbles, Vec<u8>)>)>;
273
274/// Collection of account state changes from block execution.
275///
276/// Contains all the data needed to update the state trie after
277/// executing a block: account updates, storage updates, and code deployments.
278pub struct AccountUpdatesList {
279    /// Root hash of the state trie after applying these updates.
280    pub state_trie_hash: H256,
281    /// State trie node updates (path -> RLP-encoded node).
282    pub state_updates: Vec<(Nibbles, Vec<u8>)>,
283    /// Storage trie updates per account.
284    pub storage_updates: StorageUpdates,
285    /// New contract bytecode deployments.
286    pub code_updates: Vec<(H256, Code)>,
287}
288
289/// Encodes a tx-location entry as the operand passed to `merge_cf`.
290///
291/// The operand uses the **same encoding as the stored value** — a
292/// `Vec<(BlockNumber, BlockHash, Index)>` with a single element. This is
293/// required for an *associative* merge operator: RocksDB folds operands
294/// together with PartialMerge (during compaction, without a base value), and
295/// the result becomes an operand for a later merge. If the operand format
296/// differed from the merge output (e.g. operand = bare tuple, output = Vec),
297/// the re-fed result would fail to decode and entries would be silently
298/// dropped. Keeping both as `Vec` makes the merge truly associative.
299pub(crate) fn encode_tx_location_operand(
300    block_number: BlockNumber,
301    block_hash: BlockHash,
302    index: Index,
303) -> Vec<u8> {
304    vec![(block_number, block_hash, index)].encode_to_vec()
305}
306
307/// Merge function for the `TRANSACTION_LOCATIONS` column family.
308///
309/// The CF stores `Vec<(BlockNumber, BlockHash, Index)>` keyed by tx hash.
310/// Both stored values and operands use this same `Vec` encoding — this
311/// associativity requirement is mandatory: RocksDB folds operands together
312/// during compaction without a base value (PartialMerge), then feeds that
313/// result back into a later merge. A differing format would silently drop
314/// entries. See `encode_tx_location_operand`.
315///
316/// Within the fold, a later entry with the same `block_hash` replaces an
317/// earlier one (reorg dedupe). On decode failure the merge returns `None`
318/// so RocksDB surfaces a corruption error rather than silently dropping
319/// locations.
320///
321/// Merge instead of read-modify-write avoids the ~5–20 ms/block per-tx point
322/// lookup on the write path; consolidation is deferred to compaction or the
323/// next read.
324pub fn tx_locations_merge(
325    existing: Option<&[u8]>,
326    operands: impl IntoIterator<Item = impl AsRef<[u8]>>,
327) -> Option<Vec<u8>> {
328    // Fold one RLP-encoded `Vec` chunk into `list`, deduping by block_hash
329    // (later entry wins). Returns false on decode failure so the caller can
330    // abort the whole merge.
331    fn fold_chunk(
332        list: &mut Vec<(BlockNumber, BlockHash, Index)>,
333        bytes: &[u8],
334        what: &str,
335    ) -> bool {
336        match <Vec<(BlockNumber, BlockHash, Index)>>::decode(bytes) {
337            Ok(entries) => {
338                for (bn, bh, idx) in entries {
339                    list.retain(|(_, existing_bh, _)| *existing_bh != bh);
340                    list.push((bn, bh, idx));
341                }
342                true
343            }
344            Err(e) => {
345                error!(
346                    "tx_locations_merge: failed to decode {what} ({} bytes): {e}; \
347                     aborting merge to avoid silent data loss",
348                    bytes.len()
349                );
350                false
351            }
352        }
353    }
354
355    let mut list: Vec<(BlockNumber, BlockHash, Index)> = Vec::new();
356
357    // Order matters: RocksDB delivers operands oldest-first.
358    if let Some(bytes) = existing
359        && !fold_chunk(&mut list, bytes, "existing value")
360    {
361        return None;
362    }
363    for op in operands {
364        if !fold_chunk(&mut list, op.as_ref(), "operand") {
365            return None;
366        }
367    }
368    Some(list.encode_to_vec())
369}
370
371impl Store {
372    /// Block until the persist worker has fully processed all previously-sent
373    /// `Block` messages (staged, trie-layer built, flushed, evicted).
374    ///
375    /// Uses an ack-based `Ping` rather than a bare send because the channel is
376    /// buffered — a bare send proves nothing about prior message completion. The
377    /// worker is FIFO, so it handles the `Ping` only after every earlier `Block`
378    /// is done.
379    ///
380    /// Concurrent-producer caveat: if another thread sends a `Block` after the
381    /// `Ping` is enqueued, that block may not be flushed by the time this returns.
382    pub async fn wait_for_persistence_idle(&self) -> Result<(), StoreError> {
383        let tx = self.persist_tx.clone();
384        tokio::task::spawn_blocking(move || {
385            let (ack_tx, ack_rx) = sync_channel::<Result<(), StoreError>>(1);
386            tx.send(PersistMessage::Ping(ack_tx))
387                .map_err(|e| StoreError::Custom(format!("wait_for_persistence_idle send: {e}")))?;
388            ack_rx
389                .recv()
390                .map_err(|e| StoreError::Custom(format!("wait_for_persistence_idle ack: {e}")))?
391        })
392        .await
393        .map_err(|e| StoreError::Custom(format!("wait_for_persistence_idle join: {e}")))?
394    }
395
396    /// Flushes all in-memory state to disk for a clean shutdown.
397    ///
398    /// Sends a `Shutdown` handshake to the persist worker, which (being FIFO)
399    /// first drains every queued `Block`, then force-flushes the block-data
400    /// buffer to disk. Once the worker acks, this syncs the backend (memtables +
401    /// WAL) so the next process start needs no WAL recovery.
402    ///
403    /// The in-memory trie diff-layers are intentionally *not* force-committed.
404    /// The on-disk trie is a single-version, path-based store, so folding the
405    /// non-finalized tail into it would leave a post-restart reorg unable to
406    /// reconstruct the overwritten ancestor state (the node would wedge). The
407    /// recent (< `DB_COMMIT_THRESHOLD`) layers are dropped and re-executed on the
408    /// next start from the deep, reorg-safe on-disk base — exactly as after any
409    /// restart today.
410    ///
411    /// After this returns the persist worker has exited; the store must not be
412    /// used for further writes. Idempotent only in the sense that a second call
413    /// errors on the closed channel — call it exactly once, on shutdown.
414    pub async fn shutdown(&self) -> Result<(), StoreError> {
415        let tx = self.persist_tx.clone();
416        let backend = self.backend.clone();
417        tokio::task::spawn_blocking(move || {
418            let (ack_tx, ack_rx) = sync_channel::<Result<(), StoreError>>(1);
419            tx.send(PersistMessage::Shutdown { ack: ack_tx })
420                .map_err(|e| StoreError::Custom(format!("shutdown send: {e}")))?;
421            ack_rx
422                .recv()
423                .map_err(|e| StoreError::Custom(format!("shutdown ack: {e}")))??;
424            // Worker has flushed block data to the WAL/memtables; make it durable
425            // and recovery-free.
426            backend.flush()
427        })
428        .await
429        .map_err(|e| StoreError::Custom(format!("shutdown join: {e}")))?
430    }
431
432    /// Add a block in a single transaction.
433    /// This will store -> BlockHeader, BlockBody, BlockTransactions, BlockNumber.
434    pub async fn add_block(&self, block: Block) -> Result<(), StoreError> {
435        self.add_blocks(vec![block]).await
436    }
437
438    /// Add a batch of blocks in a single transaction.
439    /// This will store -> BlockHeader, BlockBody, BlockTransactions, BlockNumber.
440    pub async fn add_blocks(&self, blocks: Vec<Block>) -> Result<(), StoreError> {
441        let db = self.backend.clone();
442        tokio::task::spawn_blocking(move || {
443            let mut tx = db.begin_write()?;
444
445            for block in blocks {
446                write_block_data(
447                    tx.as_mut(),
448                    block.header.number,
449                    block.hash(),
450                    &block.header,
451                    &block.body,
452                )?;
453            }
454
455            tx.commit()
456        })
457        .await
458        .map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
459    }
460
461    /// Add block header
462    pub async fn add_block_header(
463        &self,
464        block_hash: BlockHash,
465        block_header: BlockHeader,
466    ) -> Result<(), StoreError> {
467        let hash_key = block_hash.encode_to_vec();
468        let header_value = BlockHeaderRLP::from(block_header).into_vec();
469        self.write_async(HEADERS, hash_key, header_value).await
470    }
471
472    /// Add a batch of block headers
473    pub async fn add_block_headers(
474        &self,
475        block_headers: Vec<BlockHeader>,
476    ) -> Result<(), StoreError> {
477        let mut txn = self.backend.begin_write()?;
478
479        for header in block_headers {
480            let block_hash = header.hash();
481            let block_number = header.number;
482            let hash_key = block_hash.encode_to_vec();
483            let header_value = BlockHeaderRLP::from(header).into_vec();
484
485            txn.put(HEADERS, &hash_key, &header_value)?;
486
487            let number_key = block_number.to_le_bytes().to_vec();
488            txn.put(BLOCK_NUMBERS, &hash_key, &number_key)?;
489        }
490        txn.commit()?;
491        Ok(())
492    }
493
494    /// Obtain canonical block header
495    pub fn get_block_header(
496        &self,
497        block_number: BlockNumber,
498    ) -> Result<Option<BlockHeader>, StoreError> {
499        let latest = self.latest_block_header.get();
500        if block_number == latest.number {
501            return Ok(Some((*latest).clone()));
502        }
503        // Resolve the canonical hash, then read through the buffer-aware by-hash
504        // path so a canonical-but-still-buffered block is visible (mirrors
505        // `get_block_body`). `load_block_header` is disk-only and would return
506        // `None` for a block whose header has not been flushed yet.
507        let Some(block_hash) = self.get_canonical_block_hash_sync(block_number)? else {
508            return Ok(None);
509        };
510        self.get_block_header_by_hash(block_hash)
511    }
512
513    /// Add block body
514    pub async fn add_block_body(
515        &self,
516        block_hash: BlockHash,
517        block_body: BlockBody,
518    ) -> Result<(), StoreError> {
519        let hash_key = block_hash.encode_to_vec();
520        let body_value = BlockBodyRLP::from(block_body).into_vec();
521        self.write_async(BODIES, hash_key, body_value).await
522    }
523
524    /// Obtain canonical block body
525    pub async fn get_block_body(
526        &self,
527        block_number: BlockNumber,
528    ) -> Result<Option<BlockBody>, StoreError> {
529        let Some(block_hash) = self.get_canonical_block_hash_sync(block_number)? else {
530            return Ok(None);
531        };
532
533        self.get_block_body_by_hash(block_hash).await
534    }
535
536    /// Remove canonical block
537    pub async fn remove_block(&self, block_number: BlockNumber) -> Result<(), StoreError> {
538        let Some(hash) = self.get_canonical_block_hash_sync(block_number)? else {
539            return Ok(());
540        };
541
542        let backend = self.backend.clone();
543        tokio::task::spawn_blocking(move || {
544            let hash_key = hash.encode_to_vec();
545
546            let mut txn = backend.begin_write()?;
547            txn.delete(
548                CANONICAL_BLOCK_HASHES,
549                block_number.to_le_bytes().as_slice(),
550            )?;
551            txn.delete(BODIES, &hash_key)?;
552            txn.delete(HEADERS, &hash_key)?;
553            txn.delete(BLOCK_NUMBERS, &hash_key)?;
554            txn.commit()
555        })
556        .await
557        .map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
558    }
559
560    /// Obtain canonical block bodies in from..=to
561    pub async fn get_block_bodies(
562        &self,
563        from: BlockNumber,
564        to: BlockNumber,
565    ) -> Result<Vec<Option<BlockBody>>, StoreError> {
566        // TODO: Implement read bulk
567        let buffer = self.buffer()?;
568        let backend = self.backend.clone();
569        tokio::task::spawn_blocking(move || {
570            let numbers: Vec<BlockNumber> = (from..=to).collect();
571            let mut block_bodies = Vec::new();
572
573            let txn = backend.begin_read()?;
574            for number in numbers {
575                let Some(hash) = txn
576                    .get(CANONICAL_BLOCK_HASHES, number.to_le_bytes().as_slice())?
577                    .map(|bytes| H256::decode(bytes.as_slice()))
578                    .transpose()?
579                else {
580                    block_bodies.push(None);
581                    continue;
582                };
583                // Consult the in-memory buffer first so a not-yet-flushed body
584                // is not reported as missing (mirrors get_block_bodies_by_hash).
585                if let Some(body) = buffer.get_body(&hash) {
586                    block_bodies.push(Some(body));
587                    continue;
588                }
589                let hash_key = hash.encode_to_vec();
590                let block_body_opt = txn
591                    .get(BODIES, &hash_key)?
592                    .map(|bytes| BlockBodyRLP::from_bytes(bytes).to())
593                    .transpose()
594                    .map_err(StoreError::from)?;
595
596                block_bodies.push(block_body_opt);
597            }
598
599            Ok(block_bodies)
600        })
601        .await
602        .map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
603    }
604
605    /// Obtain block bodies from a list of hashes
606    pub async fn get_block_bodies_by_hash(
607        &self,
608        hashes: Vec<BlockHash>,
609    ) -> Result<Vec<BlockBody>, StoreError> {
610        let buffer = self.buffer()?;
611        let backend = self.backend.clone();
612        // TODO: Implement read bulk
613        tokio::task::spawn_blocking(move || {
614            let txn = backend.begin_read()?;
615            let mut block_bodies = Vec::new();
616            for hash in hashes {
617                // Consult the in-memory buffer first, like the single-hash reader,
618                // so a not-yet-flushed body is not reported as missing.
619                if let Some(body) = buffer.get_body(&hash) {
620                    block_bodies.push(body);
621                    continue;
622                }
623                let hash_key = hash.encode_to_vec();
624
625                let Some(block_body) = txn
626                    .get(BODIES, &hash_key)?
627                    .map(|bytes| BlockBodyRLP::from_bytes(bytes).to())
628                    .transpose()
629                    .map_err(StoreError::from)?
630                else {
631                    return Err(StoreError::Custom(format!(
632                        "Block body not found for hash: {hash}"
633                    )));
634                };
635                block_bodies.push(block_body);
636            }
637            Ok(block_bodies)
638        })
639        .await
640        .map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
641    }
642
643    /// Obtain any block body using the hash
644    pub async fn get_block_body_by_hash(
645        &self,
646        block_hash: BlockHash,
647    ) -> Result<Option<BlockBody>, StoreError> {
648        if let Some(b) = self.buffer()?.get_body(&block_hash) {
649            return Ok(Some(b));
650        }
651        self.read_async(BODIES, block_hash.encode_to_vec())
652            .await?
653            .map(|bytes| BlockBodyRLP::from_bytes(bytes).to())
654            .transpose()
655            .map_err(StoreError::from)
656    }
657
658    pub fn get_block_header_by_hash(
659        &self,
660        block_hash: BlockHash,
661    ) -> Result<Option<BlockHeader>, StoreError> {
662        let latest = self.latest_block_header.get();
663        if block_hash == latest.hash() {
664            return Ok(Some((*latest).clone()));
665        }
666        if let Some(h) = self.buffer()?.get_header(&block_hash) {
667            return Ok(Some(h));
668        }
669        self.load_block_header_by_hash(block_hash)
670    }
671
672    pub fn add_pending_block(&self, block: Block) -> Result<(), StoreError> {
673        let block_hash = block.hash();
674        let block_value = BlockRLP::from(block).into_vec();
675        self.write(PENDING_BLOCKS, block_hash.as_bytes().to_vec(), block_value)
676    }
677
678    pub async fn get_pending_block(
679        &self,
680        block_hash: BlockHash,
681    ) -> Result<Option<Block>, StoreError> {
682        self.read_async(PENDING_BLOCKS, block_hash.as_bytes().to_vec())
683            .await?
684            .map(|bytes| BlockRLP::from_bytes(bytes).to())
685            .transpose()
686            .map_err(StoreError::from)
687    }
688
689    /// Add block number for a given hash
690    pub async fn add_block_number(
691        &self,
692        block_hash: BlockHash,
693        block_number: BlockNumber,
694    ) -> Result<(), StoreError> {
695        let number_value = block_number.to_le_bytes().to_vec();
696        self.write_async(BLOCK_NUMBERS, block_hash.encode_to_vec(), number_value)
697            .await
698    }
699
700    /// Obtain block number for a given hash
701    pub async fn get_block_number(
702        &self,
703        block_hash: BlockHash,
704    ) -> Result<Option<BlockNumber>, StoreError> {
705        if let Some(n) = self.buffer()?.get_number(&block_hash) {
706            return Ok(Some(n));
707        }
708        self.read_async(BLOCK_NUMBERS, block_hash.encode_to_vec())
709            .await?
710            .map(|bytes| -> Result<BlockNumber, StoreError> {
711                let array: [u8; 8] = bytes
712                    .try_into()
713                    .map_err(|_| StoreError::Custom("Invalid BlockNumber bytes".to_string()))?;
714                Ok(BlockNumber::from_le_bytes(array))
715            })
716            .transpose()
717    }
718
719    /// Store transaction location (block number and index of the transaction within the block)
720    pub async fn add_transaction_location(
721        &self,
722        transaction_hash: H256,
723        block_number: BlockNumber,
724        block_hash: BlockHash,
725        index: Index,
726    ) -> Result<(), StoreError> {
727        self.add_transaction_locations(vec![(transaction_hash, block_number, block_hash, index)])
728            .await
729    }
730
731    /// Store transaction locations in batch (one db transaction for all)
732    pub async fn add_transaction_locations(
733        &self,
734        locations: Vec<(H256, BlockNumber, BlockHash, Index)>,
735    ) -> Result<(), StoreError> {
736        let db = self.backend.clone();
737        tokio::task::spawn_blocking(move || {
738            let mut tx = db.begin_write()?;
739            for (tx_hash, block_number, block_hash, index) in locations {
740                tx.merge(
741                    TRANSACTION_LOCATIONS,
742                    tx_hash.as_bytes(),
743                    &encode_tx_location_operand(block_number, block_hash, index),
744                )?;
745            }
746            tx.commit()
747        })
748        .await
749        .map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
750    }
751
752    /// Obtain transaction location (block hash and index)
753    pub async fn get_transaction_location(
754        &self,
755        transaction_hash: H256,
756    ) -> Result<Option<(BlockNumber, BlockHash, Index)>, StoreError> {
757        let buffered = self.buffer()?.get_tx_locations(&transaction_hash);
758        let db = self.backend.clone();
759        tokio::task::spawn_blocking(move || {
760            let tx = db.begin_read()?;
761            let mut locations = buffered;
762            if let Some(bytes) = tx.get(TRANSACTION_LOCATIONS, transaction_hash.as_bytes())? {
763                locations.extend(<Vec<(BlockNumber, BlockHash, Index)>>::decode(&bytes)?);
764            }
765            for (block_number, block_hash, index) in locations {
766                let canonical_hash = tx
767                    .get(
768                        CANONICAL_BLOCK_HASHES,
769                        block_number.to_le_bytes().as_slice(),
770                    )?
771                    .map(|bytes| H256::decode(bytes.as_slice()))
772                    .transpose()?;
773                if canonical_hash == Some(block_hash) {
774                    return Ok(Some((block_number, block_hash, index)));
775                }
776            }
777            Ok(None)
778        })
779        .await
780        .map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
781    }
782
783    /// Add receipt
784    pub async fn add_receipt(
785        &self,
786        block_hash: BlockHash,
787        index: Index,
788        receipt: Receipt,
789    ) -> Result<(), StoreError> {
790        let key = receipt_key(&block_hash, index);
791        // Storage codec (NOT wire/consensus): preserves frame-receipt
792        // `succeeded` + aggregated logs; identical to encode_to_vec for
793        // non-frame receipts.
794        let value = receipt.encode_storage();
795        self.write_async(RECEIPTS_V2, key, value).await
796    }
797
798    /// Add receipts
799    pub async fn add_receipts(
800        &self,
801        block_hash: BlockHash,
802        receipts: Vec<Receipt>,
803    ) -> Result<(), StoreError> {
804        let batch_items: Vec<_> = receipts
805            .into_iter()
806            .enumerate()
807            .map(|(index, receipt)| {
808                let key = receipt_key(&block_hash, index as u64);
809                let value = receipt.encode_storage();
810                (key, value)
811            })
812            .collect();
813        self.write_batch_async(RECEIPTS_V2, batch_items).await
814    }
815
816    /// Obtain receipt for a canonical block represented by the block number.
817    pub async fn get_receipt(
818        &self,
819        block_number: BlockNumber,
820        index: Index,
821    ) -> Result<Option<Receipt>, StoreError> {
822        // FIXME (#4353)
823        let Some(block_hash) = self.get_canonical_block_hash(block_number).await? else {
824            return Ok(None);
825        };
826        self.get_receipt_by_block_hash(block_hash, index).await
827    }
828
829    /// Obtain receipt by block hash and index
830    async fn get_receipt_by_block_hash(
831        &self,
832        block_hash: BlockHash,
833        index: Index,
834    ) -> Result<Option<Receipt>, StoreError> {
835        if let Some(r) = self.buffer()?.get_receipt(&block_hash, index) {
836            return Ok(Some(r));
837        }
838        let key = receipt_key(&block_hash, index);
839        self.read_async(RECEIPTS_V2, key)
840            .await?
841            .map(|bytes| Receipt::decode_storage(bytes.as_slice()))
842            .transpose()
843            .map_err(StoreError::from)
844    }
845
846    /// Get account code by its hash.
847    ///
848    /// Checks the in-memory block-data buffer first, then the LRU cache
849    /// (`account_code_cache`), and finally the database.  Code that has been
850    /// inserted via `engine_newPayload` but not yet flushed to disk is therefore
851    /// visible to callers without an explicit flush.
852    pub fn get_account_code(&self, code_hash: H256) -> Result<Option<Code>, StoreError> {
853        if let Some(code) = self.buffer()?.get_code(&code_hash) {
854            return Ok(Some(code));
855        }
856        // check cache first
857        if let Some(code) = self
858            .account_code_cache
859            .lock()
860            .map_err(|_| StoreError::LockError)?
861            .get(&code_hash)?
862        {
863            return Ok(Some(code));
864        }
865
866        let Some(bytes) = self
867            .backend
868            .begin_read()?
869            .get(ACCOUNT_CODES, code_hash.as_bytes())?
870        else {
871            return Ok(None);
872        };
873        let (bytecode_slice, targets) = decode_bytes(&bytes)?;
874        let code = Code::from_parts_unchecked(
875            code_hash,
876            bytecode_slice,
877            <Vec<u32>>::decode(targets)?.into(),
878        );
879
880        // insert into cache and evict if needed
881        self.account_code_cache
882            .lock()
883            .map_err(|_| StoreError::LockError)?
884            .insert(&code)?;
885
886        Ok(Some(code))
887    }
888
889    /// Check if account code exists by its hash, without constructing the full `Code` struct.
890    /// More efficient than `get_account_code` for existence checks since it skips
891    /// RLP decoding and `Code` struct construction (no `jump_targets` deserialization).
892    /// Note: The underlying `get()` still reads the value from RocksDB (including blob files).
893    pub fn code_exists(&self, code_hash: H256) -> Result<bool, StoreError> {
894        // Code introduced by a not-yet-flushed block lives only in the buffer; check
895        // it first so a contract created in the current block is visible (matches
896        // get_account_code / get_code_metadata).
897        if self.buffer()?.get_code(&code_hash).is_some() {
898            return Ok(true);
899        }
900        // Check cache first
901        if self
902            .account_code_cache
903            .lock()
904            .map_err(|_| StoreError::LockError)?
905            .get(&code_hash)?
906            .is_some()
907        {
908            return Ok(true);
909        }
910        // Check DB without reading the full value
911        Ok(self
912            .backend
913            .begin_read()?
914            .get(ACCOUNT_CODES, code_hash.as_bytes())?
915            .is_some())
916    }
917
918    /// Get code metadata (length) by its hash.
919    ///
920    /// Checks cache first, falls back to database. If metadata is missing,
921    /// falls back to loading full code and extracts length (auto-migration).
922    pub fn get_code_metadata(&self, code_hash: H256) -> Result<Option<CodeMetadata>, StoreError> {
923        use ethrex_common::constants::EMPTY_KECCAK_HASH;
924
925        // Empty code special case
926        if code_hash == *EMPTY_KECCAK_HASH {
927            return Ok(Some(CodeMetadata { length: 0 }));
928        }
929
930        // Check cache first
931        if let Some(metadata) = self
932            .code_metadata_cache
933            .lock()
934            .map_err(|_| StoreError::LockError)?
935            .get(&code_hash)
936            .copied()
937        {
938            return Ok(Some(metadata));
939        }
940
941        // Try reading from metadata table
942        let metadata = if let Some(bytes) = self
943            .backend
944            .begin_read()?
945            .get(ACCOUNT_CODE_METADATA, code_hash.as_bytes())?
946        {
947            let length =
948                u64::from_be_bytes(bytes.try_into().map_err(|_| {
949                    StoreError::Custom("Invalid metadata length encoding".to_string())
950                })?);
951            CodeMetadata { length }
952        } else {
953            // Fallback: load full code and extract length (auto-migration)
954            let Some(code) = self.get_account_code(code_hash)? else {
955                return Ok(None);
956            };
957            let metadata = CodeMetadata {
958                length: code.len() as u64,
959            };
960
961            // Write metadata for future use (async, fire and forget)
962            let metadata_buf = metadata.length.to_be_bytes().to_vec();
963            let hash_key = code_hash.0.to_vec();
964            let backend = self.backend.clone();
965            tokio::task::spawn(async move {
966                if let Err(e) = async {
967                    let mut tx = backend.begin_write()?;
968                    tx.put(ACCOUNT_CODE_METADATA, &hash_key, &metadata_buf)?;
969                    tx.commit()
970                }
971                .await
972                {
973                    tracing::warn!("Failed to write code metadata during auto-migration: {}", e);
974                }
975            });
976
977            metadata
978        };
979
980        // Update cache
981        self.code_metadata_cache
982            .lock()
983            .map_err(|_| StoreError::LockError)?
984            .insert(code_hash, metadata);
985
986        Ok(Some(metadata))
987    }
988
989    /// Add account code
990    pub async fn add_account_code(&self, code: Code) -> Result<(), StoreError> {
991        let hash_key = code.hash.0.to_vec();
992        let buf = encode_code(&code);
993        let metadata_buf = (code.len() as u64).to_be_bytes();
994
995        // Write both code and metadata atomically
996        let backend = self.backend.clone();
997        tokio::task::spawn_blocking(move || {
998            let mut tx = backend.begin_write()?;
999            tx.put(ACCOUNT_CODES, &hash_key, &buf)?;
1000            tx.put(ACCOUNT_CODE_METADATA, &hash_key, &metadata_buf)?;
1001            tx.commit()
1002        })
1003        .await
1004        .map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
1005    }
1006
1007    /// Clears all checkpoint data created during the last snap sync
1008    pub async fn clear_snap_state(&self) -> Result<(), StoreError> {
1009        let db = self.backend.clone();
1010        tokio::task::spawn_blocking(move || db.clear_table(SNAP_STATE))
1011            .await
1012            .map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
1013    }
1014
1015    pub async fn get_transaction_by_hash(
1016        &self,
1017        transaction_hash: H256,
1018    ) -> Result<Option<Transaction>, StoreError> {
1019        let (_block_number, block_hash, index) =
1020            match self.get_transaction_location(transaction_hash).await? {
1021                Some(location) => location,
1022                None => return Ok(None),
1023            };
1024        self.get_transaction_by_location(block_hash, index).await
1025    }
1026
1027    pub async fn get_transaction_by_location(
1028        &self,
1029        block_hash: H256,
1030        index: u64,
1031    ) -> Result<Option<Transaction>, StoreError> {
1032        let block_body = match self.get_block_body_by_hash(block_hash).await? {
1033            Some(body) => body,
1034            None => return Ok(None),
1035        };
1036        let index: usize = index.try_into()?;
1037        Ok(block_body.transactions.get(index).cloned())
1038    }
1039
1040    pub async fn get_block_by_hash(
1041        &self,
1042        block_hash: BlockHash,
1043    ) -> Result<Option<Block>, StoreError> {
1044        let header = match self.get_block_header_by_hash(block_hash)? {
1045            Some(header) => header,
1046            None => return Ok(None),
1047        };
1048        let body = match self.get_block_body_by_hash(block_hash).await? {
1049            Some(body) => body,
1050            None => return Ok(None),
1051        };
1052        Ok(Some(Block::new(header, body)))
1053    }
1054
1055    pub async fn get_block_by_number(
1056        &self,
1057        block_number: BlockNumber,
1058    ) -> Result<Option<Block>, StoreError> {
1059        let Some(block_hash) = self.get_canonical_block_hash(block_number).await? else {
1060            return Ok(None);
1061        };
1062        self.get_block_by_hash(block_hash).await
1063    }
1064
1065    // Get the canonical block hash for a given block number.
1066    pub async fn get_canonical_block_hash(
1067        &self,
1068        block_number: BlockNumber,
1069    ) -> Result<Option<BlockHash>, StoreError> {
1070        let last = self.latest_block_header.get();
1071        if last.number == block_number {
1072            return Ok(Some(last.hash()));
1073        }
1074        let backend = self.backend.clone();
1075        tokio::task::spawn_blocking(move || {
1076            backend
1077                .begin_read()?
1078                .get(
1079                    CANONICAL_BLOCK_HASHES,
1080                    block_number.to_le_bytes().as_slice(),
1081                )?
1082                .map(|bytes| H256::decode(bytes.as_slice()))
1083                .transpose()
1084                .map_err(StoreError::from)
1085        })
1086        .await
1087        .map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
1088    }
1089
1090    /// Stores the chain configuration values, should only be called once after reading the genesis file
1091    /// Ignores previously stored values if present
1092    pub async fn set_chain_config(&mut self, chain_config: &ChainConfig) -> Result<(), StoreError> {
1093        self.chain_config = *chain_config;
1094        let key = chain_data_key(ChainDataIndex::ChainConfig);
1095        let value = serde_json::to_string(chain_config)
1096            .map_err(|_| StoreError::Custom("Failed to serialize chain config".to_string()))?
1097            .into_bytes();
1098        self.write_async(CHAIN_DATA, key, value).await
1099    }
1100
1101    /// Update earliest block number
1102    pub async fn update_earliest_block_number(
1103        &self,
1104        block_number: BlockNumber,
1105    ) -> Result<(), StoreError> {
1106        let key = chain_data_key(ChainDataIndex::EarliestBlockNumber);
1107        let value = block_number.to_le_bytes().to_vec();
1108        self.write_async(CHAIN_DATA, key, value).await
1109    }
1110
1111    /// Obtain earliest block number
1112    pub async fn get_earliest_block_number(&self) -> Result<BlockNumber, StoreError> {
1113        let key = chain_data_key(ChainDataIndex::EarliestBlockNumber);
1114        self.read_async(CHAIN_DATA, key)
1115            .await?
1116            .map(|bytes| -> Result<BlockNumber, StoreError> {
1117                let array: [u8; 8] = bytes
1118                    .try_into()
1119                    .map_err(|_| StoreError::Custom("Invalid BlockNumber bytes".to_string()))?;
1120                Ok(BlockNumber::from_le_bytes(array))
1121            })
1122            .ok_or(StoreError::MissingEarliestBlockNumber)?
1123    }
1124
1125    /// Obtain finalized block number
1126    pub async fn get_finalized_block_number(&self) -> Result<Option<BlockNumber>, StoreError> {
1127        let key = chain_data_key(ChainDataIndex::FinalizedBlockNumber);
1128        self.read_async(CHAIN_DATA, key)
1129            .await?
1130            .map(|bytes| -> Result<BlockNumber, StoreError> {
1131                let array: [u8; 8] = bytes
1132                    .try_into()
1133                    .map_err(|_| StoreError::Custom("Invalid BlockNumber bytes".to_string()))?;
1134                Ok(BlockNumber::from_le_bytes(array))
1135            })
1136            .transpose()
1137    }
1138
1139    /// Obtain safe block number
1140    pub async fn get_safe_block_number(&self) -> Result<Option<BlockNumber>, StoreError> {
1141        let key = chain_data_key(ChainDataIndex::SafeBlockNumber);
1142        self.read_async(CHAIN_DATA, key)
1143            .await?
1144            .map(|bytes| -> Result<BlockNumber, StoreError> {
1145                let array: [u8; 8] = bytes
1146                    .try_into()
1147                    .map_err(|_| StoreError::Custom("Invalid BlockNumber bytes".to_string()))?;
1148                Ok(BlockNumber::from_le_bytes(array))
1149            })
1150            .transpose()
1151    }
1152
1153    /// Obtain latest block number
1154    pub async fn get_latest_block_number(&self) -> Result<BlockNumber, StoreError> {
1155        Ok(self.latest_block_header.get().number)
1156    }
1157
1158    /// Update pending block number
1159    pub async fn update_pending_block_number(
1160        &self,
1161        block_number: BlockNumber,
1162    ) -> Result<(), StoreError> {
1163        let key = chain_data_key(ChainDataIndex::PendingBlockNumber);
1164        let value = block_number.to_le_bytes().to_vec();
1165        self.write_async(CHAIN_DATA, key, value).await
1166    }
1167
1168    /// Obtain pending block number
1169    pub async fn get_pending_block_number(&self) -> Result<Option<BlockNumber>, StoreError> {
1170        let key = chain_data_key(ChainDataIndex::PendingBlockNumber);
1171        self.read_async(CHAIN_DATA, key)
1172            .await?
1173            .map(|bytes| -> Result<BlockNumber, StoreError> {
1174                let array: [u8; 8] = bytes
1175                    .try_into()
1176                    .map_err(|_| StoreError::Custom("Invalid BlockNumber bytes".to_string()))?;
1177                Ok(BlockNumber::from_le_bytes(array))
1178            })
1179            .transpose()
1180    }
1181
1182    /// DB mutation step of `forkchoice_update`.
1183    ///
1184    /// Callers MUST hold `fcu_lock` (only `forkchoice_update` should invoke this).
1185    /// The read of `LatestBlockNumber` below happens outside the write
1186    /// transaction and would be a TOCTOU window without that serialization.
1187    async fn forkchoice_update_inner(
1188        &self,
1189        new_canonical_blocks: Vec<(BlockNumber, BlockHash)>,
1190        head_number: BlockNumber,
1191        head_hash: BlockHash,
1192        safe: Option<BlockNumber>,
1193        finalized: Option<BlockNumber>,
1194    ) -> Result<(), StoreError> {
1195        let latest = self.load_latest_block_number().await?.unwrap_or(0);
1196        let db = self.backend.clone();
1197        tokio::task::spawn_blocking(move || {
1198            let mut txn = db.begin_write()?;
1199
1200            for (block_number, block_hash) in new_canonical_blocks {
1201                let head_key = block_number.to_le_bytes();
1202                let head_value = block_hash.encode_to_vec();
1203                txn.put(CANONICAL_BLOCK_HASHES, &head_key, &head_value)?;
1204            }
1205
1206            // Delete canonical entries above the new head by enumerating each key.
1207            // `delete_range` is not safe here: keys are `u64::to_le_bytes()`, and
1208            // RocksDB's lexicographic comparator does not match LE numeric order
1209            // (e.g. block 256 = [0x00, 0x01, ..] sorts before block 11 = [0x0B, ..]),
1210            // so a range-delete would silently miss blocks whose LE first byte is
1211            // smaller than `head+1`'s first byte.
1212            for number in (head_number + 1)..=(latest) {
1213                txn.delete(CANONICAL_BLOCK_HASHES, number.to_le_bytes().as_slice())?;
1214            }
1215
1216            // Make head canonical
1217            let head_key = head_number.to_le_bytes();
1218            let head_value = head_hash.encode_to_vec();
1219            txn.put(CANONICAL_BLOCK_HASHES, &head_key, &head_value)?;
1220
1221            // Update chain data
1222            let latest_key = chain_data_key(ChainDataIndex::LatestBlockNumber);
1223            txn.put(CHAIN_DATA, &latest_key, &head_number.to_le_bytes())?;
1224
1225            if let Some(safe) = safe {
1226                let safe_key = chain_data_key(ChainDataIndex::SafeBlockNumber);
1227                txn.put(CHAIN_DATA, &safe_key, &safe.to_le_bytes())?;
1228            }
1229
1230            if let Some(finalized) = finalized {
1231                let finalized_key = chain_data_key(ChainDataIndex::FinalizedBlockNumber);
1232                txn.put(CHAIN_DATA, &finalized_key, &finalized.to_le_bytes())?;
1233            }
1234
1235            txn.commit()
1236        })
1237        .await
1238        .map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
1239    }
1240
1241    pub async fn get_receipts_for_block(
1242        &self,
1243        block_hash: &BlockHash,
1244    ) -> Result<Vec<Receipt>, StoreError> {
1245        self.get_receipts_for_block_from_index(block_hash, 0, None)
1246            .await
1247    }
1248
1249    /// Retrieves receipts for a block starting from the given index,
1250    /// optionally limited to `max_count` receipts.
1251    ///
1252    /// Uses cursor-based prefix iteration over the 32-byte block hash prefix
1253    /// for efficient batch retrieval. Used by:
1254    /// - eth/70 partial receipt requests (EIP-7975) via p2p
1255    /// - `eth_getTransactionReceipt` RPC with a count limit to avoid
1256    ///   fetching the entire block's receipts
1257    pub async fn get_receipts_for_block_from_index(
1258        &self,
1259        block_hash: &BlockHash,
1260        start_index: u64,
1261        max_count: Option<usize>,
1262    ) -> Result<Vec<Receipt>, StoreError> {
1263        if let Some(all) = self.buffer()?.get_receipts(block_hash) {
1264            let start = start_index as usize;
1265            let slice = all.into_iter().skip(start);
1266            return Ok(match max_count {
1267                Some(max) => slice.take(max).collect(),
1268                None => slice.collect(),
1269            });
1270        }
1271        let backend = self.backend.clone();
1272        let block_hash = *block_hash;
1273
1274        tokio::task::spawn_blocking(move || {
1275            let txn = backend.begin_read()?;
1276            let prefix = block_hash.as_bytes().to_vec();
1277            // Seek directly to block_hash || start_index to avoid O(start_index) scan.
1278            // Keys are big-endian u64, so lexicographic order matches numeric order.
1279            let mut seek_key = prefix.clone();
1280            seek_key.extend_from_slice(&start_index.to_be_bytes());
1281            let iter = txn.prefix_iterator(RECEIPTS_V2, &seek_key)?;
1282            let mut receipts = Vec::new();
1283            for result in iter {
1284                let (k, v) = result?;
1285                if !k.starts_with(&prefix) {
1286                    break;
1287                }
1288                if k.len() != 40 {
1289                    continue;
1290                }
1291                receipts.push(Receipt::decode_storage(v.as_ref())?);
1292                if let Some(max) = max_count
1293                    && receipts.len() >= max
1294                {
1295                    break;
1296                }
1297            }
1298            Ok(receipts)
1299        })
1300        .await
1301        .map_err(|e| StoreError::Custom(format!("Task panicked: {e}")))?
1302    }
1303
1304    // Snap State methods
1305
1306    /// Sets the hash of the last header downloaded during a snap sync
1307    pub async fn set_header_download_checkpoint(
1308        &self,
1309        block_hash: BlockHash,
1310    ) -> Result<(), StoreError> {
1311        let key = snap_state_key(SnapStateIndex::HeaderDownloadCheckpoint);
1312        let value = block_hash.encode_to_vec();
1313        self.write_async(SNAP_STATE, key, value).await
1314    }
1315
1316    /// Gets the hash of the last header downloaded during a snap sync
1317    pub async fn get_header_download_checkpoint(&self) -> Result<Option<BlockHash>, StoreError> {
1318        let key = snap_state_key(SnapStateIndex::HeaderDownloadCheckpoint);
1319        self.backend
1320            .begin_read()?
1321            .get(SNAP_STATE, &key)?
1322            .map(|bytes| H256::decode(bytes.as_slice()))
1323            .transpose()
1324            .map_err(StoreError::from)
1325    }
1326
1327    /// The `forkchoice_update` and `new_payload` methods require the `latest_valid_hash`
1328    /// when processing an invalid payload. To provide this, we must track invalid chains.
1329    ///
1330    /// We only store the last known valid head upon encountering a bad block,
1331    /// rather than tracking every subsequent invalid block.
1332    pub async fn set_latest_valid_ancestor(
1333        &self,
1334        bad_block: BlockHash,
1335        latest_valid: BlockHash,
1336    ) -> Result<(), StoreError> {
1337        let value = latest_valid.encode_to_vec();
1338        self.write_async(INVALID_CHAINS, bad_block.as_bytes().to_vec(), value)
1339            .await
1340    }
1341
1342    /// Returns the latest valid ancestor hash for a given invalid block hash.
1343    /// Used to provide `latest_valid_hash` in the Engine API when processing invalid payloads.
1344    pub async fn get_latest_valid_ancestor(
1345        &self,
1346        block: BlockHash,
1347    ) -> Result<Option<BlockHash>, StoreError> {
1348        self.read_async(INVALID_CHAINS, block.as_bytes().to_vec())
1349            .await?
1350            .map(|bytes| H256::decode(bytes.as_slice()))
1351            .transpose()
1352            .map_err(StoreError::from)
1353    }
1354
1355    /// Obtain block number for a given hash
1356    pub fn get_block_number_sync(
1357        &self,
1358        block_hash: BlockHash,
1359    ) -> Result<Option<BlockNumber>, StoreError> {
1360        if let Some(n) = self.buffer()?.get_number(&block_hash) {
1361            return Ok(Some(n));
1362        }
1363        let txn = self.backend.begin_read()?;
1364        txn.get(BLOCK_NUMBERS, &block_hash.encode_to_vec())?
1365            .map(|bytes| -> Result<BlockNumber, StoreError> {
1366                let array: [u8; 8] = bytes
1367                    .try_into()
1368                    .map_err(|_| StoreError::Custom("Invalid BlockNumber bytes".to_string()))?;
1369                Ok(BlockNumber::from_le_bytes(array))
1370            })
1371            .transpose()
1372    }
1373
1374    /// Get the canonical block hash for a given block number.
1375    pub fn get_canonical_block_hash_sync(
1376        &self,
1377        block_number: BlockNumber,
1378    ) -> Result<Option<BlockHash>, StoreError> {
1379        let last = self.latest_block_header.get();
1380        if last.number == block_number {
1381            return Ok(Some(last.hash()));
1382        }
1383        let txn = self.backend.begin_read()?;
1384        txn.get(
1385            CANONICAL_BLOCK_HASHES,
1386            block_number.to_le_bytes().as_slice(),
1387        )?
1388        .map(|bytes| H256::decode(bytes.as_slice()))
1389        .transpose()
1390        .map_err(StoreError::from)
1391    }
1392
1393    /// CAUTION: This method writes directly to the underlying database, bypassing any caching layer.
1394    /// For updating the state after block execution, use [`Self::store_block_updates`].
1395    pub async fn write_storage_trie_nodes_batch(
1396        &self,
1397        storage_trie_nodes: StorageUpdates,
1398    ) -> Result<(), StoreError> {
1399        let mut txn = self.backend.begin_write()?;
1400        tokio::task::spawn_blocking(move || {
1401            for (address_hash, nodes) in storage_trie_nodes {
1402                for (node_path, node_data) in nodes {
1403                    let key = apply_prefix(Some(address_hash), node_path);
1404                    if node_data.is_empty() {
1405                        txn.delete(STORAGE_TRIE_NODES, key.as_ref())?;
1406                    } else {
1407                        txn.put(STORAGE_TRIE_NODES, key.as_ref(), &node_data)?;
1408                    }
1409                }
1410            }
1411            txn.commit()
1412        })
1413        .await
1414        .map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
1415    }
1416
1417    /// CAUTION: This method writes directly to the underlying database, bypassing any caching layer.
1418    /// For updating the state after block execution, use [`Self::store_block_updates`].
1419    pub async fn write_account_code_batch(
1420        &self,
1421        account_codes: Vec<(H256, Code)>,
1422    ) -> Result<(), StoreError> {
1423        let mut code_batch_items = Vec::new();
1424        let mut metadata_batch_items = Vec::new();
1425
1426        for (code_hash, code) in account_codes {
1427            let buf = encode_code(&code);
1428            let metadata_buf = (code.len() as u64).to_be_bytes().to_vec();
1429            code_batch_items.push((code_hash.as_bytes().to_vec(), buf));
1430            metadata_batch_items.push((code_hash.as_bytes().to_vec(), metadata_buf));
1431        }
1432
1433        // Write both batches
1434        self.write_batch_async(ACCOUNT_CODES, code_batch_items)
1435            .await?;
1436        self.write_batch_async(ACCOUNT_CODE_METADATA, metadata_batch_items)
1437            .await
1438    }
1439
1440    /// Returns a snapshot of the current block-data buffer.
1441    fn buffer(&self) -> Result<Arc<BlockDataBuffer>, StoreError> {
1442        Ok(self
1443            .block_data_buffer
1444            .read()
1445            .map_err(|_| StoreError::LockError)?
1446            .clone())
1447    }
1448
1449    // Helper methods for async operations with spawn_blocking
1450    // These methods ensure RocksDB I/O doesn't block the tokio runtime
1451
1452    /// Helper method for async writes
1453    /// Spawns blocking task to avoid blocking tokio runtime
1454    pub fn write(
1455        &self,
1456        table: &'static str,
1457        key: Vec<u8>,
1458        value: Vec<u8>,
1459    ) -> Result<(), StoreError> {
1460        let backend = self.backend.clone();
1461        let mut txn = backend.begin_write()?;
1462        txn.put(table, &key, &value)?;
1463        txn.commit()
1464    }
1465
1466    /// Helper method for async writes
1467    /// Spawns blocking task to avoid blocking tokio runtime
1468    async fn write_async(
1469        &self,
1470        table: &'static str,
1471        key: Vec<u8>,
1472        value: Vec<u8>,
1473    ) -> Result<(), StoreError> {
1474        let backend = self.backend.clone();
1475
1476        tokio::task::spawn_blocking(move || {
1477            let mut txn = backend.begin_write()?;
1478            txn.put(table, &key, &value)?;
1479            txn.commit()
1480        })
1481        .await
1482        .map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
1483    }
1484
1485    /// Helper method for async reads
1486    /// Spawns blocking task to avoid blocking tokio runtime
1487    pub async fn read_async(
1488        &self,
1489        table: &'static str,
1490        key: Vec<u8>,
1491    ) -> Result<Option<Vec<u8>>, StoreError> {
1492        let backend = self.backend.clone();
1493
1494        tokio::task::spawn_blocking(move || {
1495            let txn = backend.begin_read()?;
1496            txn.get(table, &key)
1497        })
1498        .await
1499        .map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
1500    }
1501
1502    /// Helper method for sync reads
1503    /// Spawns blocking task to avoid blocking tokio runtime
1504    pub fn read(&self, table: &'static str, key: Vec<u8>) -> Result<Option<Vec<u8>>, StoreError> {
1505        let backend = self.backend.clone();
1506        let txn = backend.begin_read()?;
1507        txn.get(table, &key)
1508    }
1509
1510    /// Helper method for batch writes
1511    /// Spawns blocking task to avoid blocking tokio runtime
1512    /// This is the most important optimization for healing performance
1513    pub async fn write_batch_async(
1514        &self,
1515        table: &'static str,
1516        batch_ops: Vec<(Vec<u8>, Vec<u8>)>,
1517    ) -> Result<(), StoreError> {
1518        let backend = self.backend.clone();
1519
1520        tokio::task::spawn_blocking(move || {
1521            let mut txn = backend.begin_write()?;
1522            txn.put_batch(table, batch_ops)?;
1523            txn.commit()
1524        })
1525        .await
1526        .map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
1527    }
1528
1529    /// Helper method for batch writes
1530    pub fn write_batch(
1531        &self,
1532        table: &'static str,
1533        batch_ops: Vec<(Vec<u8>, Vec<u8>)>,
1534    ) -> Result<(), StoreError> {
1535        let backend = self.backend.clone();
1536        let mut txn = backend.begin_write()?;
1537        txn.put_batch(table, batch_ops)?;
1538        txn.commit()
1539    }
1540
1541    pub async fn add_fullsync_batch(&self, headers: Vec<BlockHeader>) -> Result<(), StoreError> {
1542        self.write_batch_async(
1543            FULLSYNC_HEADERS,
1544            headers
1545                .into_iter()
1546                .map(|header| (header.number.to_le_bytes().to_vec(), header.encode_to_vec()))
1547                .collect(),
1548        )
1549        .await
1550    }
1551
1552    pub async fn read_fullsync_batch(
1553        &self,
1554        start: BlockNumber,
1555        limit: u64,
1556    ) -> Result<Vec<Option<BlockHeader>>, StoreError> {
1557        let mut res = vec![];
1558        let read_tx = self.backend.begin_read()?;
1559        // TODO: use read_bulk here
1560        for key in start..start + limit {
1561            let header_opt = read_tx
1562                .get(FULLSYNC_HEADERS, &key.to_le_bytes())?
1563                .map(|header| BlockHeader::decode(&header))
1564                .transpose()?;
1565            res.push(header_opt);
1566        }
1567        Ok(res)
1568    }
1569
1570    pub async fn clear_fullsync_headers(&self) -> Result<(), StoreError> {
1571        self.backend.clear_table(FULLSYNC_HEADERS)
1572    }
1573
1574    /// Delete a key from a table
1575    pub fn delete(&self, table: &'static str, key: Vec<u8>) -> Result<(), StoreError> {
1576        let mut txn = self.backend.begin_write()?;
1577        txn.delete(table, &key)?;
1578        txn.commit()
1579    }
1580
1581    pub fn store_block_updates(&self, update_batch: UpdateBatch) -> Result<(), StoreError> {
1582        self.apply_updates(update_batch)
1583    }
1584
1585    /// Compute `(parent_state_root, last_state_root)` for a batch's trie update:
1586    /// the state root of the first block's parent and the last block's own state
1587    /// root. Used by `apply_updates` for both the live and full-sync paths (which
1588    /// share the single persist worker).
1589    fn batch_state_roots(&self, update_batch: &UpdateBatch) -> Result<(H256, H256), StoreError> {
1590        let parent_state_root = self
1591            .get_block_header_by_hash(
1592                update_batch
1593                    .blocks
1594                    .first()
1595                    .ok_or(StoreError::UpdateBatchNoBlocks)?
1596                    .header
1597                    .parent_hash,
1598            )?
1599            .map(|header| header.state_root)
1600            .unwrap_or_default();
1601        let last_state_root = update_batch
1602            .blocks
1603            .last()
1604            .ok_or(StoreError::UpdateBatchNoBlocks)?
1605            .header
1606            .state_root;
1607        Ok((parent_state_root, last_state_root))
1608    }
1609
1610    /// Single path for both live (`batch_mode == false`) and full-sync
1611    /// (`batch_mode == true`) updates. Both hand the whole unit (block data +
1612    /// one aggregate trie diff) to the SINGLE persist worker and wait for its ack;
1613    /// `wait_for_flush` (= `batch_mode`) selects when the worker acks.
1614    fn apply_updates(&self, update_batch: UpdateBatch) -> Result<(), StoreError> {
1615        let (parent_state_root, last_state_root) = self.batch_state_roots(&update_batch)?;
1616
1617        let UpdateBatch {
1618            account_updates,
1619            storage_updates,
1620            blocks,
1621            receipts,
1622            code_updates,
1623            batch_mode,
1624        } = update_batch;
1625
1626        // Register before handing off to the worker and before this returns, so
1627        // any reader opening this root blocks in `gated_snapshot` until the
1628        // layer is installed rather than snapshotting a stale cache.
1629        self.pending_trie_roots.register(last_state_root)?;
1630
1631        // Pair blocks with receipts. Single-block fast path avoids a HashMap
1632        // allocation; full-sync batch joins by hash.
1633        let blocks_with_receipts: Vec<(Block, Vec<Receipt>)> = if blocks.len() == 1 {
1634            let block = blocks.into_iter().next().expect("len == 1");
1635            let hash = block.hash();
1636            let r = receipts
1637                .into_iter()
1638                .find(|(h, _)| *h == hash)
1639                .map(|(_, r)| r)
1640                .unwrap_or_default();
1641            vec![(block, r)]
1642        } else {
1643            let mut receipts_by_hash: std::collections::HashMap<BlockHash, Vec<Receipt>> =
1644                receipts.into_iter().collect();
1645            blocks
1646                .into_iter()
1647                .map(|b| {
1648                    let r = receipts_by_hash.remove(&b.hash()).unwrap_or_default();
1649                    (b, r)
1650                })
1651                .collect()
1652        };
1653
1654        // Send to the persist worker and wait for its ack.
1655        // LIVE (wait_for_flush=false): worker acks after staging; the ack carries
1656        //   the PRIOR flush result so a disk error surfaces on the next call.
1657        // BATCH (wait_for_flush=true): worker acks after flush, bounding
1658        //   in-flight batches to ~1.
1659        let (ack_tx, ack_rx) = sync_channel(1);
1660        self.persist_tx
1661            .send(PersistMessage::Block(BlockPersist {
1662                blocks: blocks_with_receipts,
1663                codes: code_updates,
1664                parent_state_root,
1665                child_state_root: last_state_root,
1666                account_updates,
1667                storage_updates,
1668                wait_for_flush: batch_mode,
1669                ack: ack_tx,
1670            }))
1671            .map_err(|e| StoreError::Custom(format!("failed to send block persist: {e}")))?;
1672        ack_rx
1673            .recv()
1674            .map_err(|e| StoreError::Custom(format!("block persist ack failed: {e}")))??;
1675
1676        Ok(())
1677    }
1678
1679    /// Opens (or creates) a store at `path` with the default [`StoreConfig`].
1680    ///
1681    /// Production callers that need to override storage tunables (e.g. the RocksDB
1682    /// block cache size from a CLI option) should use [`Store::new_with_config`].
1683    pub fn new(path: impl AsRef<Path>, engine_type: EngineType) -> Result<Self, StoreError> {
1684        Self::new_with_config(path, engine_type, StoreConfig::default())
1685    }
1686
1687    /// Opens (or creates) a store at `path`, applying the supplied [`StoreConfig`].
1688    pub fn new_with_config(
1689        path: impl AsRef<Path>,
1690        engine_type: EngineType,
1691        // `config` only feeds the RocksDB backend; without that feature it is unused.
1692        #[cfg_attr(not(feature = "rocksdb"), allow(unused_variables))] config: StoreConfig,
1693    ) -> Result<Self, StoreError> {
1694        let db_path = path.as_ref().to_path_buf();
1695
1696        if engine_type != EngineType::InMemory {
1697            let version = read_store_schema_version(&db_path)?;
1698
1699            match version {
1700                None if db_path.exists() && dir_contains_legacy_db(&db_path)? => {
1701                    // Pre-metadata DB — cannot migrate safely
1702                    return Err(StoreError::NotFoundDBVersion);
1703                }
1704                None => {
1705                    // No metadata and no recognizable database files. The directory
1706                    // may still hold unrelated files (e.g. a JWT secret placed in the
1707                    // datadir by tooling such as EthDocker, see issue #5680), so treat
1708                    // this as a fresh datadir and write the initial metadata instead
1709                    // of erroring out.
1710                    init_metadata_file(&db_path)?;
1711                }
1712                Some(v) if v < 1 => {
1713                    return Err(StoreError::MigrationFailed {
1714                        from: v,
1715                        to: STORE_SCHEMA_VERSION,
1716                        reason: format!("DB version v{v} is invalid (predates migrations)"),
1717                    });
1718                }
1719                Some(v) if v > STORE_SCHEMA_VERSION => {
1720                    return Err(StoreError::MigrationFailed {
1721                        from: v,
1722                        to: STORE_SCHEMA_VERSION,
1723                        reason: format!(
1724                            "DB version v{v} is more recent than the client expects (v{STORE_SCHEMA_VERSION}). Rolling back is not supported"
1725                        ),
1726                    });
1727                }
1728                #[cfg(feature = "rocksdb")]
1729                Some(v) if v < STORE_SCHEMA_VERSION => {
1730                    // Open backend, run migrations, then drop obsolete CFs.
1731                    // Cleanup must happen AFTER migrations so legacy CFs (e.g.
1732                    // `receipts`) are still readable during the migration.
1733                    let rocksdb = Arc::new(RocksDBBackend::open(
1734                        &path,
1735                        config.rocksdb_block_cache_size,
1736                    )?);
1737                    crate::migrations::run_pending_migrations(rocksdb.as_ref(), &db_path, v)?;
1738                    rocksdb.drop_obsolete_cfs(&path);
1739                    let backend: Arc<dyn crate::api::StorageBackend> = rocksdb;
1740                    return Self::from_backend(
1741                        backend,
1742                        db_path,
1743                        DB_COMMIT_THRESHOLD,
1744                        config.persist_channel_capacity,
1745                    );
1746                }
1747                Some(_) => {
1748                    // version == STORE_SCHEMA_VERSION, proceed normally.
1749                    // Without the `rocksdb` feature this also covers v < target,
1750                    // but that path is unreachable since InMemory is the only
1751                    // engine type and the outer guard excludes it.
1752                }
1753            }
1754        }
1755
1756        match engine_type {
1757            #[cfg(feature = "rocksdb")]
1758            EngineType::RocksDB => {
1759                let rocksdb = RocksDBBackend::open(&path, config.rocksdb_block_cache_size)?;
1760                rocksdb.drop_obsolete_cfs(&path);
1761                let backend: Arc<dyn StorageBackend> = Arc::new(rocksdb);
1762                Self::from_backend(
1763                    backend,
1764                    db_path,
1765                    DB_COMMIT_THRESHOLD,
1766                    config.persist_channel_capacity,
1767                )
1768            }
1769            EngineType::InMemory => {
1770                let backend = Arc::new(InMemoryBackend::open()?);
1771                Self::from_backend(
1772                    backend,
1773                    db_path,
1774                    IN_MEMORY_COMMIT_THRESHOLD,
1775                    config.persist_channel_capacity,
1776                )
1777            }
1778        }
1779    }
1780
1781    fn from_backend(
1782        backend: Arc<dyn StorageBackend>,
1783        db_path: PathBuf,
1784        commit_threshold: usize,
1785        persist_channel_capacity: usize,
1786    ) -> Result<Self, StoreError> {
1787        debug!("Initializing Store with {commit_threshold} in-memory diff-layers");
1788        let (fkv_tx, fkv_rx) = std::sync::mpsc::sync_channel(0);
1789        let persist_cap = persist_channel_capacity.max(1); // clamp: 0 would be a rendezvous channel
1790        let (persist_tx, persist_rx) = std::sync::mpsc::sync_channel(persist_cap);
1791
1792        let (last_written, initial_flushed_upto) = {
1793            let tx = backend.begin_read()?;
1794            let last_written = tx
1795                .get(MISC_VALUES, "last_written".as_bytes())?
1796                .unwrap_or_else(|| vec![0u8; 64]);
1797            let last_written = if last_written == [0xff] {
1798                vec![0xff; 64]
1799            } else {
1800                last_written
1801            };
1802            let initial_flushed_upto = match tx.get(MISC_VALUES, FLUSHED_UPTO_KEY)? {
1803                Some(bytes) => decode_flushed_upto(&bytes)?,
1804                None => 0,
1805            };
1806            (last_written, initial_flushed_upto)
1807        };
1808        let mut initial_buffer = BlockDataBuffer::new();
1809        initial_buffer.set_flushed_upto(initial_flushed_upto);
1810
1811        let mut background_threads = Vec::new();
1812        let mut store = Self {
1813            db_path,
1814            backend,
1815            chain_config: Default::default(),
1816            latest_block_header: Default::default(),
1817            trie_cache: Arc::new(RwLock::new(Arc::new(TrieLayerCache::new(commit_threshold)))),
1818            flatkeyvalue_control_tx: fkv_tx,
1819            block_data_buffer: Arc::new(RwLock::new(Arc::new(initial_buffer))),
1820            persist_tx,
1821            pending_trie_roots: Arc::new(PendingTrieRoots::default()),
1822            last_computed_flatkeyvalue: Arc::new(RwLock::new(last_written)),
1823            account_code_cache: Arc::new(Mutex::new(CodeCache::default())),
1824            code_metadata_cache: Arc::new(Mutex::new(rustc_hash::FxHashMap::default())),
1825            fcu_lock: Arc::new(tokio::sync::Mutex::new(())),
1826            background_threads: Default::default(),
1827        };
1828        let backend_clone = store.backend.clone();
1829        let last_computed_fkv = store.last_computed_flatkeyvalue.clone();
1830        background_threads.push(std::thread::spawn(move || {
1831            let rx = fkv_rx;
1832            // Wait for the first Continue to start generation
1833            loop {
1834                match rx.recv() {
1835                    Ok(FKVGeneratorControlMessage::Continue) => break,
1836                    Ok(FKVGeneratorControlMessage::Stop) => {}
1837                    Err(std::sync::mpsc::RecvError) => {
1838                        debug!("Closing FlatKeyValue generator.");
1839                        return;
1840                    }
1841                }
1842            }
1843
1844            let _ = flatkeyvalue_generator(&backend_clone, &last_computed_fkv, &rx)
1845                .inspect_err(|err| error!("Error while generating FlatKeyValue: {err}"));
1846        }));
1847        // The single persist worker: sole swapper of `block_data_buffer`, sole
1848        // builder of trie diff-layers. One DB transaction per `Block` message.
1849        let persist_backend = store.backend.clone();
1850        let persist_buffer = store.block_data_buffer.clone();
1851        let persist_trie_cache = store.trie_cache.clone();
1852        let persist_pending_roots = store.pending_trie_roots.clone();
1853        let persist_fkv_ctl = store.flatkeyvalue_control_tx.clone();
1854        background_threads.push(std::thread::spawn(move || {
1855            let rx = persist_rx;
1856            // Carries the prior flush result: the live path acks after staging,
1857            // so a disk failure surfaces on the next message's ack.
1858            let mut last_flush_result: Result<(), StoreError> = Ok(());
1859            loop {
1860                match rx.recv() {
1861                    Ok(PersistMessage::Block(bp)) => {
1862                        // Stage block data (sole swapper of the buffer; codes
1863                        // are batch-level and attributed to the first block).
1864                        let staged = mutate_block_buffer(&persist_buffer, move |b| {
1865                            let mut codes = Some(bp.codes);
1866                            for (block, receipts) in bp.blocks {
1867                                b.insert(block, receipts, codes.take().unwrap_or_default());
1868                            }
1869                        });
1870                        if let Err(e) = staged {
1871                            // Stage failure is terminal for this message.
1872                            // Clear the pending root so gated readers are not
1873                            // blocked forever (apply_trie_phase1, which normally
1874                            // does this, is skipped when we continue here).
1875                            persist_pending_roots.clear(bp.child_state_root);
1876                            let _ = bp.ack.send(Err(e));
1877                            continue;
1878                        }
1879                        // LIVE: ack after staging; carries prior flush result.
1880                        // NOTE: this acks block validity BEFORE apply_trie_phase1
1881                        // installs the trie layer below. A phase-1 failure (only
1882                        // reachable via lock poisoning, which is already fatal) is
1883                        // therefore deferred to the next block's ack via
1884                        // last_flush_result rather than attributed to this block;
1885                        // the pending root is still cleared unconditionally, so
1886                        // gated readers error rather than hang.
1887                        if !bp.wait_for_flush {
1888                            let _ = bp
1889                                .ack
1890                                .send(std::mem::replace(&mut last_flush_result, Ok(())));
1891                        }
1892                        // Build + install the trie layer; clear the read gate.
1893                        if let Err(err) = apply_trie_phase1(
1894                            &persist_trie_cache,
1895                            &persist_pending_roots,
1896                            bp.parent_state_root,
1897                            bp.child_state_root,
1898                            bp.account_updates,
1899                            bp.storage_updates,
1900                        ) {
1901                            error!("persist worker trie phase-1 failed: {err}");
1902                            if bp.wait_for_flush {
1903                                let _ = bp.ack.send(Err(err));
1904                            } else {
1905                                last_flush_result = Err(err);
1906                            }
1907                            continue;
1908                        }
1909                        // Flush block data + commit bottom trie layer when due.
1910                        let flushed = flush_block_data(persist_backend.as_ref(), &persist_buffer)
1911                            .inspect_err(|err| error!("flush_block_data failed: {err}"))
1912                            .and_then(|_| {
1913                                commit_trie_if_due(
1914                                    persist_backend.as_ref(),
1915                                    &persist_trie_cache,
1916                                    &persist_fkv_ctl,
1917                                    bp.parent_state_root,
1918                                    bp.wait_for_flush,
1919                                )
1920                            });
1921                        // BATCH: ack after flush (bounds in-flight batches to ~1),
1922                        // folding in any prior live-path error. LIVE: stash result.
1923                        if bp.wait_for_flush {
1924                            let prior = std::mem::replace(&mut last_flush_result, Ok(()));
1925                            let _ = bp.ack.send(prior.and(flushed));
1926                        } else {
1927                            last_flush_result = flushed;
1928                        }
1929                    }
1930                    Ok(PersistMessage::Ping(ack)) => {
1931                        // Idle handshake: reached only after all earlier Block
1932                        // messages are fully processed. Carry the pending flush
1933                        // result so a live-path failure is not silently dropped.
1934                        let _ = ack.send(std::mem::replace(&mut last_flush_result, Ok(())));
1935                    }
1936                    Ok(PersistMessage::Shutdown { ack }) => {
1937                        // Graceful shutdown: drain (already guaranteed by FIFO) and
1938                        // force-flush the not-yet-flushed block-data tail. The trie
1939                        // diff-layers stay in memory and are dropped on exit: the
1940                        // on-disk trie is a single-version path store, so committing
1941                        // the non-finalized tail would make a post-restart reorg
1942                        // unrecoverable. Those layers re-execute on the next start.
1943                        let result = flush_block_data(persist_backend.as_ref(), &persist_buffer);
1944                        let prior = std::mem::replace(&mut last_flush_result, Ok(()));
1945                        let _ = ack.send(prior.and(result));
1946                        // No more work will follow a shutdown request.
1947                        return;
1948                    }
1949                    Err(_) => return,
1950                }
1951            }
1952        }));
1953        store.background_threads = Arc::new(ThreadList {
1954            list: background_threads,
1955        });
1956        Ok(store)
1957    }
1958
1959    /// Opens (or creates) a store at `store_path` and seeds it from the
1960    /// given genesis file, using the default [`StoreConfig`].
1961    pub async fn new_from_genesis(
1962        store_path: &Path,
1963        engine_type: EngineType,
1964        genesis_path: &str,
1965    ) -> Result<Self, StoreError> {
1966        Self::new_from_genesis_with_config(
1967            store_path,
1968            engine_type,
1969            genesis_path,
1970            StoreConfig::default(),
1971        )
1972        .await
1973    }
1974
1975    /// Opens (or creates) a store at `store_path` from genesis, applying the
1976    /// supplied [`StoreConfig`].
1977    pub async fn new_from_genesis_with_config(
1978        store_path: &Path,
1979        engine_type: EngineType,
1980        genesis_path: &str,
1981        config: StoreConfig,
1982    ) -> Result<Self, StoreError> {
1983        let file = std::fs::File::open(genesis_path)
1984            .map_err(|error| StoreError::Custom(format!("Failed to open genesis file: {error}")))?;
1985        let reader = std::io::BufReader::new(file);
1986        let genesis: Genesis = serde_json::from_reader(reader)
1987            .map_err(|e| StoreError::Custom(format!("Failed to deserialize genesis file: {e}")))?;
1988        let mut store = Self::new_with_config(store_path, engine_type, config)?;
1989        store.add_initial_state(genesis).await?;
1990        Ok(store)
1991    }
1992
1993    pub async fn get_account_info(
1994        &self,
1995        block_number: BlockNumber,
1996        address: Address,
1997    ) -> Result<Option<AccountInfo>, StoreError> {
1998        match self.get_canonical_block_hash(block_number).await? {
1999            Some(block_hash) => self.get_account_info_by_hash(block_hash, address),
2000            None => Ok(None),
2001        }
2002    }
2003
2004    pub fn get_account_info_by_hash(
2005        &self,
2006        block_hash: BlockHash,
2007        address: Address,
2008    ) -> Result<Option<AccountInfo>, StoreError> {
2009        let Some(state_trie) = self.state_trie(block_hash)? else {
2010            return Ok(None);
2011        };
2012        let hashed_address = hash_address_fixed(&address);
2013
2014        let Some(encoded_state) = state_trie.get(hashed_address.as_bytes())? else {
2015            return Ok(None);
2016        };
2017
2018        let account_state = AccountState::decode(&encoded_state)?;
2019        Ok(Some(AccountInfo {
2020            code_hash: account_state.code_hash,
2021            balance: account_state.balance,
2022            nonce: account_state.nonce,
2023        }))
2024    }
2025
2026    pub fn get_account_state_by_acc_hash(
2027        &self,
2028        block_hash: BlockHash,
2029        account_hash: H256,
2030    ) -> Result<Option<AccountState>, StoreError> {
2031        let Some(state_trie) = self.state_trie(block_hash)? else {
2032            return Ok(None);
2033        };
2034        let Some(encoded_state) = state_trie.get(account_hash.as_bytes())? else {
2035            return Ok(None);
2036        };
2037        let account_state = AccountState::decode(&encoded_state)?;
2038        Ok(Some(account_state))
2039    }
2040
2041    pub async fn get_fork_id(&self) -> Result<ForkId, StoreError> {
2042        let chain_config = self.get_chain_config();
2043        let genesis_header = self
2044            .load_block_header(0)?
2045            .ok_or(StoreError::MissingEarliestBlockNumber)?;
2046        let block_header = self.latest_block_header.get();
2047
2048        Ok(ForkId::new(
2049            chain_config,
2050            genesis_header,
2051            block_header.timestamp,
2052            block_header.number,
2053        ))
2054    }
2055
2056    pub async fn get_code_by_account_address(
2057        &self,
2058        block_number: BlockNumber,
2059        address: Address,
2060    ) -> Result<Option<Code>, StoreError> {
2061        let Some(block_hash) = self.get_canonical_block_hash(block_number).await? else {
2062            return Ok(None);
2063        };
2064        let Some(state_trie) = self.state_trie(block_hash)? else {
2065            return Ok(None);
2066        };
2067        let hashed_address = hash_address_fixed(&address);
2068        let Some(encoded_state) = state_trie.get(hashed_address.as_bytes())? else {
2069            return Ok(None);
2070        };
2071        let account_state = AccountState::decode(&encoded_state)?;
2072        self.get_account_code(account_state.code_hash)
2073    }
2074
2075    pub async fn get_nonce_by_account_address(
2076        &self,
2077        block_number: BlockNumber,
2078        address: Address,
2079    ) -> Result<Option<u64>, StoreError> {
2080        let Some(block_hash) = self.get_canonical_block_hash(block_number).await? else {
2081            return Ok(None);
2082        };
2083        let Some(state_trie) = self.state_trie(block_hash)? else {
2084            return Ok(None);
2085        };
2086        let hashed_address = hash_address_fixed(&address);
2087        let Some(encoded_state) = state_trie.get(hashed_address.as_bytes())? else {
2088            return Ok(None);
2089        };
2090        let account_state = AccountState::decode(&encoded_state)?;
2091        Ok(Some(account_state.nonce))
2092    }
2093
2094    /// Applies account updates based on the block's latest storage state
2095    /// and returns the new state root after the updates have been applied.
2096    pub fn apply_account_updates_batch(
2097        &self,
2098        block_hash: BlockHash,
2099        account_updates: &[AccountUpdate],
2100    ) -> Result<Option<AccountUpdatesList>, StoreError> {
2101        let Some(mut state_trie) = self.state_trie(block_hash)? else {
2102            return Ok(None);
2103        };
2104
2105        Ok(Some(self.apply_account_updates_from_trie_batch(
2106            &mut state_trie,
2107            account_updates,
2108        )?))
2109    }
2110
2111    pub fn apply_account_updates_from_trie_batch<'a>(
2112        &self,
2113        state_trie: &mut Trie,
2114        account_updates: impl IntoIterator<Item = &'a AccountUpdate>,
2115    ) -> Result<AccountUpdatesList, StoreError> {
2116        let mut ret_storage_updates = Vec::new();
2117        let mut code_updates = Vec::new();
2118        let state_root = state_trie.hash_no_commit(&NativeCrypto);
2119        for update in account_updates {
2120            let hashed_address = hash_address_fixed(&update.address);
2121            if update.removed {
2122                // Remove account from trie
2123                state_trie.remove(hashed_address.as_bytes())?;
2124                continue;
2125            }
2126            // Add or update AccountState in the trie
2127            // Fetch current state or create a new state to be inserted
2128            let mut account_state = match state_trie.get(hashed_address.as_bytes())? {
2129                Some(encoded_state) => AccountState::decode(&encoded_state)?,
2130                None => AccountState::default(),
2131            };
2132            if update.removed_storage {
2133                account_state.storage_root = *EMPTY_TRIE_HASH;
2134            }
2135            if let Some(info) = &update.info {
2136                account_state.nonce = info.nonce;
2137                account_state.balance = info.balance;
2138                account_state.code_hash = info.code_hash;
2139                // Store updated code in DB
2140                if let Some(code) = &update.code {
2141                    code_updates.push((info.code_hash, code.clone()));
2142                }
2143            }
2144            // Store the added storage in the account's storage trie and compute its new root
2145            if !update.added_storage.is_empty() {
2146                let mut storage_trie =
2147                    self.open_storage_trie(hashed_address, state_root, account_state.storage_root)?;
2148                for (storage_key, storage_value) in &update.added_storage {
2149                    let hashed_key = hash_key(storage_key);
2150                    if storage_value.is_zero() {
2151                        storage_trie.remove(&hashed_key)?;
2152                    } else {
2153                        storage_trie.insert(hashed_key, storage_value.encode_to_vec())?;
2154                    }
2155                }
2156                let (storage_hash, storage_updates) =
2157                    storage_trie.collect_changes_since_last_hash(&NativeCrypto);
2158                account_state.storage_root = storage_hash;
2159                ret_storage_updates.push((hashed_address, storage_updates));
2160            }
2161            state_trie.insert(
2162                hashed_address.as_bytes().to_vec(),
2163                account_state.encode_to_vec(),
2164            )?;
2165        }
2166        let (state_trie_hash, state_updates) =
2167            state_trie.collect_changes_since_last_hash(&NativeCrypto);
2168
2169        Ok(AccountUpdatesList {
2170            state_trie_hash,
2171            state_updates,
2172            storage_updates: ret_storage_updates,
2173            code_updates,
2174        })
2175    }
2176
2177    /// Performs the same actions as apply_account_updates_from_trie
2178    ///  but also returns the used storage tries with witness recorded
2179    pub fn apply_account_updates_from_trie_with_witness(
2180        &self,
2181        mut state_trie: Trie,
2182        account_updates: &[AccountUpdate],
2183        mut storage_tries: StorageTries,
2184    ) -> Result<(StorageTries, AccountUpdatesList), StoreError> {
2185        let mut ret_storage_updates = Vec::new();
2186
2187        let mut code_updates = Vec::new();
2188
2189        let state_root = state_trie.hash_no_commit(&NativeCrypto);
2190
2191        for update in account_updates.iter() {
2192            let hashed_address = hash_address(&update.address);
2193
2194            if update.removed {
2195                // Remove account from trie
2196                state_trie.remove(&hashed_address)?;
2197
2198                continue;
2199            }
2200
2201            // Add or update AccountState in the trie
2202            // Fetch current state or create a new state to be inserted
2203            let mut account_state = match state_trie.get(&hashed_address)? {
2204                Some(encoded_state) => AccountState::decode(&encoded_state)?,
2205                None => AccountState::default(),
2206            };
2207
2208            if update.removed_storage {
2209                account_state.storage_root = *EMPTY_TRIE_HASH;
2210            }
2211
2212            if let Some(info) = &update.info {
2213                account_state.nonce = info.nonce;
2214
2215                account_state.balance = info.balance;
2216
2217                account_state.code_hash = info.code_hash;
2218
2219                // Store updated code in DB
2220                if let Some(code) = &update.code {
2221                    code_updates.push((info.code_hash, code.clone()));
2222                }
2223            }
2224
2225            // Store the added storage in the account's storage trie and compute its new root
2226            if !update.added_storage.is_empty() {
2227                let (_witness, storage_trie) = match storage_tries.entry(update.address) {
2228                    Entry::Occupied(value) => value.into_mut(),
2229                    Entry::Vacant(vacant) => {
2230                        let trie = self.open_storage_trie(
2231                            H256::from_slice(&hashed_address),
2232                            state_root,
2233                            account_state.storage_root,
2234                        )?;
2235                        vacant.insert(TrieLogger::open_trie(trie))
2236                    }
2237                };
2238
2239                for (storage_key, storage_value) in &update.added_storage {
2240                    let hashed_key = hash_key(storage_key);
2241
2242                    if storage_value.is_zero() {
2243                        storage_trie.remove(&hashed_key)?;
2244                    } else {
2245                        storage_trie.insert(hashed_key, storage_value.encode_to_vec())?;
2246                    }
2247                }
2248
2249                let (storage_hash, storage_updates) =
2250                    storage_trie.collect_changes_since_last_hash(&NativeCrypto);
2251
2252                account_state.storage_root = storage_hash;
2253
2254                ret_storage_updates.push((H256::from_slice(&hashed_address), storage_updates));
2255            }
2256
2257            state_trie.insert(hashed_address, account_state.encode_to_vec())?;
2258        }
2259
2260        let (state_trie_hash, state_updates) =
2261            state_trie.collect_changes_since_last_hash(&NativeCrypto);
2262
2263        let account_updates_list = AccountUpdatesList {
2264            state_trie_hash,
2265            state_updates,
2266            storage_updates: ret_storage_updates,
2267            code_updates,
2268        };
2269
2270        Ok((storage_tries, account_updates_list))
2271    }
2272
2273    /// Adds all genesis accounts and returns the genesis block's state_root
2274    pub async fn setup_genesis_state_trie(
2275        &self,
2276        genesis_accounts: BTreeMap<Address, GenesisAccount>,
2277    ) -> Result<H256, StoreError> {
2278        let mut storage_trie_nodes = vec![];
2279        let mut genesis_state_trie = self.open_direct_state_trie(*EMPTY_TRIE_HASH)?;
2280        for (address, account) in genesis_accounts {
2281            let hashed_address = hash_address(&address);
2282            let h256_hashed_address = H256::from_slice(&hashed_address);
2283
2284            // Store account code (as this won't be stored in the trie)
2285            let code = Code::from_bytecode(account.code, &NativeCrypto);
2286            let code_hash = code.hash;
2287            self.add_account_code(code).await?;
2288
2289            // Store the account's storage in a clean storage trie and compute its root
2290            let mut storage_trie =
2291                self.open_direct_storage_trie(h256_hashed_address, *EMPTY_TRIE_HASH)?;
2292            for (storage_key, storage_value) in account.storage {
2293                if !storage_value.is_zero() {
2294                    let hashed_key = hash_key(&H256(storage_key.to_big_endian()));
2295                    storage_trie.insert(hashed_key, storage_value.encode_to_vec())?;
2296                }
2297            }
2298
2299            let (storage_root, storage_nodes) =
2300                storage_trie.collect_changes_since_last_hash(&NativeCrypto);
2301
2302            storage_trie_nodes.extend(
2303                storage_nodes
2304                    .into_iter()
2305                    .map(|(path, n)| (apply_prefix(Some(h256_hashed_address), path).into_vec(), n)),
2306            );
2307
2308            // Add account to trie
2309            let account_state = AccountState {
2310                nonce: account.nonce,
2311                balance: account.balance,
2312                storage_root,
2313                code_hash,
2314            };
2315            genesis_state_trie.insert(hashed_address, account_state.encode_to_vec())?;
2316        }
2317
2318        let (state_root, account_trie_nodes) =
2319            genesis_state_trie.collect_changes_since_last_hash(&NativeCrypto);
2320        let account_trie_nodes = account_trie_nodes
2321            .into_iter()
2322            .map(|(path, n)| (apply_prefix(None, path).into_vec(), n))
2323            .collect::<Vec<_>>();
2324
2325        let mut tx = self.backend.begin_write()?;
2326        tx.put_batch(ACCOUNT_TRIE_NODES, account_trie_nodes)?;
2327        tx.put_batch(STORAGE_TRIE_NODES, storage_trie_nodes)?;
2328        tx.commit()?;
2329
2330        Ok(state_root)
2331    }
2332
2333    // Key format: block_number (8 bytes, big-endian) + block_hash (32 bytes)
2334    fn make_witness_key(block_number: u64, block_hash: &BlockHash) -> Vec<u8> {
2335        let mut composite_key = Vec::with_capacity(8 + 32);
2336        composite_key.extend_from_slice(&block_number.to_be_bytes());
2337        composite_key.extend_from_slice(block_hash.as_bytes());
2338        composite_key
2339    }
2340
2341    /// Stores a pre-serialized execution witness for a block.
2342    ///
2343    /// The witness is converted to RPC format (RpcExecutionWitness) before storage
2344    /// to avoid expensive `encode_subtrie` traversal on every read. This pre-computes
2345    /// the serialization at write time instead of read time.
2346    pub fn store_witness(
2347        &self,
2348        block_hash: BlockHash,
2349        block_number: u64,
2350        witness: ExecutionWitness,
2351    ) -> Result<(), StoreError> {
2352        // Convert to RPC format once at storage time
2353        let rpc_witness = RpcExecutionWitness::try_from(witness)?;
2354        let key = Self::make_witness_key(block_number, &block_hash);
2355        let value = serde_json::to_vec(&rpc_witness)?;
2356        self.write(EXECUTION_WITNESSES, key, value)?;
2357        // Clean up old witnesses (keep only last 128)
2358        self.cleanup_old_witnesses(block_number)
2359    }
2360
2361    fn cleanup_old_witnesses(&self, latest_block_number: u64) -> Result<(), StoreError> {
2362        // If we have less than 128 blocks, no cleanup needed
2363        if latest_block_number <= MAX_WITNESSES {
2364            return Ok(());
2365        }
2366
2367        let threshold = latest_block_number - MAX_WITNESSES;
2368
2369        if let Some(oldest_block_number) = self.get_oldest_witness_number()? {
2370            let prefix = oldest_block_number.to_be_bytes();
2371            let mut to_delete = Vec::new();
2372
2373            {
2374                let read_txn = self.backend.begin_read()?;
2375                let iter = read_txn.prefix_iterator(EXECUTION_WITNESSES, &prefix)?;
2376
2377                // We may have multiple witnesses for the same block number (forks)
2378                for item in iter {
2379                    let (key, _value) = item?;
2380                    let mut block_number_bytes = [0u8; 8];
2381                    block_number_bytes.copy_from_slice(&key[0..8]);
2382                    let block_number = u64::from_be_bytes(block_number_bytes);
2383                    if block_number > threshold {
2384                        break;
2385                    }
2386                    to_delete.push(key.to_vec());
2387                }
2388            }
2389
2390            for key in to_delete {
2391                self.delete(EXECUTION_WITNESSES, key)?;
2392            }
2393        };
2394
2395        self.update_oldest_witness_number(threshold + 1)?;
2396
2397        Ok(())
2398    }
2399
2400    fn update_oldest_witness_number(&self, oldest_block_number: u64) -> Result<(), StoreError> {
2401        self.write(
2402            MISC_VALUES,
2403            b"oldest_witness_block_number".to_vec(),
2404            oldest_block_number.to_le_bytes().to_vec(),
2405        )?;
2406        Ok(())
2407    }
2408
2409    fn get_oldest_witness_number(&self) -> Result<Option<u64>, StoreError> {
2410        let Some(value) = self.read(MISC_VALUES, b"oldest_witness_block_number".to_vec())? else {
2411            return Ok(None);
2412        };
2413
2414        let array: [u8; 8] = value.as_slice().try_into().map_err(|_| {
2415            StoreError::Custom("Invalid oldest witness block number bytes".to_string())
2416        })?;
2417        Ok(Some(u64::from_le_bytes(array)))
2418    }
2419
2420    /// Returns the raw JSON bytes of a cached witness for a block.
2421    ///
2422    /// This is the most efficient method for the RPC handler since it avoids
2423    /// deserialization and re-serialization. The bytes can be parsed directly
2424    /// as a JSON Value for the RPC response.
2425    pub fn get_witness_json_bytes(
2426        &self,
2427        block_number: u64,
2428        block_hash: BlockHash,
2429    ) -> Result<Option<Vec<u8>>, StoreError> {
2430        let key = Self::make_witness_key(block_number, &block_hash);
2431        self.read(EXECUTION_WITNESSES, key)
2432    }
2433
2434    /// Returns the deserialized RpcExecutionWitness for a block.
2435    ///
2436    /// Prefer `get_witness_json_bytes` when you need to return the witness
2437    /// as JSON (e.g., for RPC responses) to avoid re-serialization.
2438    pub fn get_witness_by_number_and_hash(
2439        &self,
2440        block_number: u64,
2441        block_hash: BlockHash,
2442    ) -> Result<Option<RpcExecutionWitness>, StoreError> {
2443        let key = Self::make_witness_key(block_number, &block_hash);
2444        match self.read(EXECUTION_WITNESSES, key)? {
2445            Some(value) => {
2446                let witness: RpcExecutionWitness = serde_json::from_slice(&value)?;
2447                Ok(Some(witness))
2448            }
2449            None => Ok(None),
2450        }
2451    }
2452
2453    /// Stores a block access list for a given block hash.
2454    pub fn store_block_access_list(
2455        &self,
2456        block_hash: BlockHash,
2457        bal: &BlockAccessList,
2458    ) -> Result<(), StoreError> {
2459        let key = block_hash.as_bytes().to_vec();
2460        let mut value = vec![];
2461        bal.encode(&mut value);
2462        self.write(BLOCK_ACCESS_LISTS, key, value)
2463    }
2464
2465    /// Returns the block access list for a given block hash, if stored.
2466    pub fn get_block_access_list(
2467        &self,
2468        block_hash: BlockHash,
2469    ) -> Result<Option<BlockAccessList>, StoreError> {
2470        let key = block_hash.as_bytes().to_vec();
2471        match self.read(BLOCK_ACCESS_LISTS, key)? {
2472            Some(value) => {
2473                let bal = BlockAccessList::decode(&value)
2474                    .map_err(|e| StoreError::Custom(format!("Failed to decode BAL: {e}")))?;
2475                Ok(Some(bal))
2476            }
2477            None => Ok(None),
2478        }
2479    }
2480
2481    pub async fn add_initial_state(&mut self, genesis: Genesis) -> Result<(), StoreError> {
2482        self.add_initial_state_inner(genesis, false).await
2483    }
2484
2485    /// Like [`Store::add_initial_state`], but trusts a pre-existing datadir's
2486    /// state instead of validating it against the provided genesis. If a genesis
2487    /// header is already stored, it is kept as-is rather than recomputing the
2488    /// genesis state root from `genesis.alloc` and rejecting on mismatch. The
2489    /// chain config from the genesis file is still applied either way.
2490    ///
2491    /// Intended for booting a datadir produced out-of-band (e.g. by a state
2492    /// generator that writes the state trie directly and emits a genesis file
2493    /// with an empty `alloc`), where the operator vouches for the stored state
2494    /// root. Has no effect on a fresh datadir: the genesis is built normally.
2495    pub async fn add_initial_state_skip_validation(
2496        &mut self,
2497        genesis: Genesis,
2498    ) -> Result<(), StoreError> {
2499        self.add_initial_state_inner(genesis, true).await
2500    }
2501
2502    async fn add_initial_state_inner(
2503        &mut self,
2504        genesis: Genesis,
2505        skip_genesis_validation: bool,
2506    ) -> Result<(), StoreError> {
2507        debug!("Storing initial state from genesis");
2508
2509        // Obtain genesis block
2510        let genesis_block = genesis.get_block();
2511        let genesis_block_number = genesis_block.header.number;
2512
2513        let genesis_hash = genesis_block.hash();
2514
2515        let stored_genesis_header = self.load_block_header(genesis_block_number)?;
2516
2517        // Always set the chain config from the genesis file. The in-memory
2518        // `chain_config` starts at `Default::default()` on every boot and is
2519        // not reloaded from the datadir, so skipping this would leave the store
2520        // with the wrong chainId and an empty fork schedule. Skip-validation
2521        // only waives the genesis state-root/header check; the `config` section
2522        // of the genesis file is still authoritative and must be applied.
2523        self.set_chain_config(&genesis.config).await?;
2524
2525        // The cache can't be empty. Clamp the head to the durable block: after a
2526        // crash, `LatestBlockNumber` can be ahead of `flushed_upto` (FCU writes the
2527        // head synchronously while block bodies are buffered), so loading the raw
2528        // latest header would brick boot when its body was never flushed.
2529        if let Some(latest) = self.load_latest_block_number().await? {
2530            self.anchor_to_durable_head(latest).await?;
2531        }
2532
2533        match stored_genesis_header {
2534            Some(header) if skip_genesis_validation => {
2535                info!(
2536                    stored_genesis = %header.hash(),
2537                    "Skipping genesis state validation; trusting the genesis header and state already stored in the datadir"
2538                );
2539                return Ok(());
2540            }
2541            Some(header) if header.hash() == genesis_hash => {
2542                info!("Received genesis file matching a previously stored one, nothing to do");
2543                return Ok(());
2544            }
2545            Some(_) => {
2546                error!(
2547                    "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."
2548                );
2549                return Err(StoreError::IncompatibleChainConfig);
2550            }
2551            None => {
2552                self.add_block_header(genesis_hash, genesis_block.header.clone())
2553                    .await?
2554            }
2555        }
2556        // Store genesis accounts
2557        // TODO: Should we use this root instead of computing it before the block hash check?
2558        let genesis_state_root = self.setup_genesis_state_trie(genesis.alloc).await?;
2559        debug_assert_eq!(genesis_state_root, genesis_block.header.state_root);
2560
2561        // Store genesis block
2562        info!(hash = %genesis_hash, "Storing genesis block");
2563
2564        self.add_block(genesis_block).await?;
2565        self.update_earliest_block_number(genesis_block_number)
2566            .await?;
2567        self.forkchoice_update(vec![], genesis_block_number, genesis_hash, None, None)
2568            .await?;
2569        Ok(())
2570    }
2571
2572    pub async fn load_initial_state(&self) -> Result<(), StoreError> {
2573        info!("Loading initial state from DB");
2574        let Some(latest) = self.load_latest_block_number().await? else {
2575            return Err(StoreError::MissingLatestBlockNumber);
2576        };
2577        // Use the same durable-head clamp as the node boot path so export and the
2578        // running node agree on the head. The persisted head is only rewritten when
2579        // it actually moved, so a plain export run does not mutate `CHAIN_DATA`.
2580        self.anchor_to_durable_head(latest).await?;
2581        Ok(())
2582    }
2583
2584    pub fn get_storage_at(
2585        &self,
2586        block_number: BlockNumber,
2587        address: Address,
2588        storage_key: H256,
2589    ) -> Result<Option<U256>, StoreError> {
2590        match self.get_block_header(block_number)? {
2591            Some(header) => self.get_storage_at_root(header.state_root, address, storage_key),
2592            None => Ok(None),
2593        }
2594    }
2595
2596    pub fn get_storage_at_root(
2597        &self,
2598        state_root: H256,
2599        address: Address,
2600        storage_key: H256,
2601    ) -> Result<Option<U256>, StoreError> {
2602        let account_hash = hash_address_fixed(&address);
2603
2604        // Pre-acquire shared resources once for both trie opens
2605        let read_view = self.backend.begin_read()?;
2606        let cache = self.gated_snapshot(state_root)?;
2607        let last_written = self.last_written()?;
2608        let use_fkv = Self::flatkeyvalue_computed_with_last_written(account_hash, &last_written);
2609
2610        let storage_root = if use_fkv {
2611            // We will use FKVs, we don't need the root
2612            *EMPTY_TRIE_HASH
2613        } else {
2614            let state_trie = self.open_state_trie_shared(
2615                state_root,
2616                read_view.clone(),
2617                cache.clone(),
2618                last_written.clone(),
2619            )?;
2620            let Some(encoded_account) = state_trie.get(account_hash.as_bytes())? else {
2621                return Ok(None);
2622            };
2623            let account = AccountState::decode(&encoded_account)?;
2624            account.storage_root
2625        };
2626        let storage_trie = self.open_storage_trie_shared(
2627            account_hash,
2628            state_root,
2629            storage_root,
2630            read_view,
2631            cache,
2632            last_written,
2633        )?;
2634
2635        let hashed_key = hash_key_fixed(&storage_key);
2636        storage_trie
2637            .get(&hashed_key)?
2638            .map(|rlp| U256::decode(&rlp).map_err(StoreError::RLPDecode))
2639            .transpose()
2640    }
2641
2642    /// Gets storage value when the account hash and storage root are already known.
2643    ///
2644    /// This skips the state-trie account lookup and account RLP decode done by
2645    /// [`Self::get_storage_at_root`], and directly opens the account storage trie.
2646    pub fn get_storage_at_root_with_known_storage_root(
2647        &self,
2648        state_root: H256,
2649        account_hash: H256,
2650        storage_root: H256,
2651        storage_key: H256,
2652    ) -> Result<Option<U256>, StoreError> {
2653        let read_view = self.backend.begin_read()?;
2654        let cache = self.gated_snapshot(state_root)?;
2655        let last_written = self.last_written()?;
2656        // When FKV is active the real storage root is in the flatkeyvalue store,
2657        // not in the account's RLP-encoded storage_root field. Use EMPTY_TRIE_HASH
2658        // so open_storage_trie_shared falls through to the FKV path.
2659        let storage_root =
2660            if Self::flatkeyvalue_computed_with_last_written(account_hash, &last_written) {
2661                *EMPTY_TRIE_HASH
2662            } else {
2663                storage_root
2664            };
2665        let storage_trie = self.open_storage_trie_shared(
2666            account_hash,
2667            state_root,
2668            storage_root,
2669            read_view,
2670            cache,
2671            last_written,
2672        )?;
2673
2674        let hashed_key = hash_key_fixed(&storage_key);
2675        storage_trie
2676            .get(&hashed_key)?
2677            .map(|rlp| U256::decode(&rlp).map_err(StoreError::RLPDecode))
2678            .transpose()
2679    }
2680
2681    pub fn get_chain_config(&self) -> ChainConfig {
2682        self.chain_config
2683    }
2684
2685    pub async fn get_latest_canonical_block_hash(&self) -> Result<Option<BlockHash>, StoreError> {
2686        Ok(Some(self.latest_block_header.get().hash()))
2687    }
2688
2689    /// Updates the canonical chain.
2690    /// Inserts new canonical blocks, removes blocks beyond the new head,
2691    /// and updates the head, safe, and finalized block pointers.
2692    /// All operations are performed in a single database transaction.
2693    pub async fn forkchoice_update(
2694        &self,
2695        new_canonical_blocks: Vec<(BlockNumber, BlockHash)>,
2696        head_number: BlockNumber,
2697        head_hash: BlockHash,
2698        safe: Option<BlockNumber>,
2699        finalized: Option<BlockNumber>,
2700    ) -> Result<(), StoreError> {
2701        // Serialize concurrent forkchoice updates. Without this, two callers
2702        // could interleave their `latest_block_header` cache updates with each
2703        // other's DB writes, leaving the cache inconsistent with the DB or
2704        // letting a later caller's write reorder relative to the cache update
2705        // order (see the TOCTOU discussion around canonical/latest drift).
2706        let _guard = self.fcu_lock.lock().await;
2707
2708        // Updates first the latest_block_header to avoid nonce inconsistencies #3927.
2709        // Snapshot the previous header so we can roll the cache back if the DB
2710        // write fails — otherwise the cache would point at a block the DB does
2711        // not consider canonical.
2712        let previous_head = self.latest_block_header.get();
2713        let new_head = self
2714            .get_block_header_by_hash(head_hash)?
2715            .ok_or_else(|| StoreError::MissingLatestBlockNumber)?;
2716        self.latest_block_header.update(new_head);
2717        if let Err(err) = self
2718            .forkchoice_update_inner(
2719                new_canonical_blocks,
2720                head_number,
2721                head_hash,
2722                safe,
2723                finalized,
2724            )
2725            .await
2726        {
2727            self.latest_block_header.update((*previous_head).clone());
2728            return Err(err);
2729        }
2730
2731        Ok(())
2732    }
2733
2734    /// Obtain the storage trie for the given block
2735    pub fn state_trie(&self, block_hash: BlockHash) -> Result<Option<Trie>, StoreError> {
2736        let Some(header) = self.get_block_header_by_hash(block_hash)? else {
2737            return Ok(None);
2738        };
2739        Ok(Some(self.open_state_trie(header.state_root)?))
2740    }
2741
2742    /// Obtain the storage trie for the given account on the given block
2743    pub fn storage_trie(
2744        &self,
2745        block_hash: BlockHash,
2746        address: Address,
2747    ) -> Result<Option<Trie>, StoreError> {
2748        let Some(header) = self.get_block_header_by_hash(block_hash)? else {
2749            return Ok(None);
2750        };
2751        // Fetch Account from state_trie
2752        let Some(state_trie) = self.state_trie(block_hash)? else {
2753            return Ok(None);
2754        };
2755        let hashed_address = hash_address_fixed(&address);
2756        let Some(encoded_account) = state_trie.get(hashed_address.as_bytes())? else {
2757            return Ok(None);
2758        };
2759        let account = AccountState::decode(&encoded_account)?;
2760        // Open storage_trie
2761        let storage_root = account.storage_root;
2762        Ok(Some(self.open_storage_trie(
2763            hashed_address,
2764            header.state_root,
2765            storage_root,
2766        )?))
2767    }
2768
2769    pub async fn get_account_state(
2770        &self,
2771        block_number: BlockNumber,
2772        address: Address,
2773    ) -> Result<Option<AccountState>, StoreError> {
2774        let Some(block_hash) = self.get_canonical_block_hash(block_number).await? else {
2775            return Ok(None);
2776        };
2777        let Some(state_trie) = self.state_trie(block_hash)? else {
2778            return Ok(None);
2779        };
2780        self.get_account_state_from_trie(&state_trie, address)
2781    }
2782
2783    pub fn get_account_state_by_root(
2784        &self,
2785        state_root: H256,
2786        address: Address,
2787    ) -> Result<Option<AccountState>, StoreError> {
2788        let state_trie = self.open_state_trie(state_root)?;
2789        self.get_account_state_from_trie(&state_trie, address)
2790    }
2791
2792    pub fn get_account_state_from_trie(
2793        &self,
2794        state_trie: &Trie,
2795        address: Address,
2796    ) -> Result<Option<AccountState>, StoreError> {
2797        let hashed_address = hash_address_fixed(&address);
2798        let Some(encoded_state) = state_trie.get(hashed_address.as_bytes())? else {
2799            return Ok(None);
2800        };
2801        Ok(Some(AccountState::decode(&encoded_state)?))
2802    }
2803
2804    /// Batch lookup of account states by address against a given state root.
2805    ///
2806    /// Fast path: for addresses whose hashed path falls within the FKV cursor
2807    /// (and which are not present in the in-memory diff-layer cache), values
2808    /// are fetched in a single `multi_get` on `ACCOUNT_FLATKEYVALUE`. Other
2809    /// addresses fall back to per-address trie walks.
2810    ///
2811    /// Results are returned in the same order as the input addresses.
2812    pub fn get_account_states_batch_by_root(
2813        &self,
2814        state_root: H256,
2815        addresses: &[Address],
2816    ) -> Result<Vec<Option<AccountState>>, StoreError> {
2817        if addresses.is_empty() {
2818            return Ok(Vec::new());
2819        }
2820
2821        let last_written = self.last_written()?;
2822        let trie_cache = self
2823            .trie_cache
2824            .read()
2825            .map_err(|_| StoreError::LockError)?
2826            .clone();
2827
2828        let mut results: Vec<Option<AccountState>> = vec![None; addresses.len()];
2829        // Per-address leaf paths (nibbles + leaf flag). Length 65.
2830        let leaf_paths: Vec<Vec<u8>> = addresses
2831            .iter()
2832            .map(|addr| {
2833                let hashed = hash_address_fixed(addr);
2834                Nibbles::from_bytes(hashed.as_bytes()).into_vec()
2835            })
2836            .collect();
2837
2838        let mut fkv_indices: Vec<usize> = Vec::new();
2839        let mut trie_indices: Vec<usize> = Vec::new();
2840
2841        // Match `BackendTrieDB::flatkeyvalue_computed` semantics: a path is
2842        // covered by FKV iff `last_written >= path` as raw nibble bytes. This
2843        // is the same check `Trie::get` uses; the related helper
2844        // `Store::flatkeyvalue_computed_with_last_written` slices `[0..64]`
2845        // and is intentionally more conservative — using that here would
2846        // unnecessarily fall back to the trie when the cursor sits inside an
2847        // account's storage sweep (the account leaf is already in FKV at that
2848        // point; see `flatkeyvalue_generator`).
2849        let fkv_cursor: &[u8] = last_written.as_slice();
2850        for (i, path) in leaf_paths.iter().enumerate() {
2851            if let Some(value) = trie_cache.get(state_root, path.as_slice()) {
2852                if !value.is_empty() {
2853                    results[i] = Some(AccountState::decode(&value)?);
2854                }
2855                continue;
2856            }
2857            if fkv_cursor >= path.as_slice() {
2858                fkv_indices.push(i);
2859            } else {
2860                trie_indices.push(i);
2861            }
2862        }
2863
2864        if !fkv_indices.is_empty() {
2865            let read_view = self.backend.begin_read()?;
2866            let keys: Vec<&[u8]> = fkv_indices
2867                .iter()
2868                .map(|&i| leaf_paths[i].as_slice())
2869                .collect();
2870            let raw = read_view.multi_get(ACCOUNT_FLATKEYVALUE, &keys);
2871            for (slot, res) in fkv_indices.iter().zip(raw.into_iter()) {
2872                let Some(encoded) = res? else { continue };
2873                if encoded.is_empty() {
2874                    continue;
2875                }
2876                results[*slot] = Some(AccountState::decode(&encoded)?);
2877            }
2878        }
2879
2880        if !trie_indices.is_empty() {
2881            // Fall back to the regular trie path for any addresses whose path
2882            // hasn't been swept by the FKV generator yet. Parallelized to
2883            // recover the per-address fan-out the pre-batch `par_iter` path
2884            // had, which matters during initial sync when most addresses
2885            // miss FKV.
2886            let state_trie = self.open_state_trie(state_root)?;
2887            let fetched: Result<Vec<(usize, Option<AccountState>)>, StoreError> = trie_indices
2888                .par_iter()
2889                .map(|&i| {
2890                    self.get_account_state_from_trie(&state_trie, addresses[i])
2891                        .map(|s| (i, s))
2892                })
2893                .collect();
2894            for (i, s) in fetched? {
2895                results[i] = s;
2896            }
2897        }
2898
2899        Ok(results)
2900    }
2901
2902    /// Constructs a merkle proof for the given account address against a given state.
2903    /// If storage_keys are provided, also constructs the storage proofs for those keys.
2904    ///
2905    /// Returns `None` if the state trie is missing, otherwise returns the proof.
2906    pub async fn get_account_proof(
2907        &self,
2908        state_root: H256,
2909        address: Address,
2910        storage_keys: &[H256],
2911    ) -> Result<Option<AccountProof>, StoreError> {
2912        // TODO: check state root
2913        // let Some(state_trie) = self.open_state_trie(state_trie)? else {
2914        //     return Ok(None);
2915        // };
2916        let state_trie = self.open_state_trie(state_root)?;
2917        let address_path = hash_address_fixed(&address);
2918        let proof = state_trie.get_proof(address_path.as_bytes())?;
2919        let account_opt = state_trie
2920            .get(address_path.as_bytes())?
2921            .map(|encoded_state| AccountState::decode(&encoded_state))
2922            .transpose()?;
2923
2924        let mut storage_proof = Vec::with_capacity(storage_keys.len());
2925
2926        if let Some(account) = &account_opt {
2927            let storage_trie =
2928                self.open_storage_trie(address_path, state_root, account.storage_root)?;
2929
2930            for key in storage_keys {
2931                let hashed_key = hash_key(key);
2932                let proof = storage_trie.get_proof(&hashed_key)?;
2933                let value = storage_trie
2934                    .get(&hashed_key)?
2935                    .map(|rlp| U256::decode(&rlp).map_err(StoreError::RLPDecode))
2936                    .transpose()?
2937                    .unwrap_or_default();
2938
2939                let slot_proof = StorageSlotProof {
2940                    proof,
2941                    key: *key,
2942                    value,
2943                };
2944                storage_proof.push(slot_proof);
2945            }
2946        } else {
2947            storage_proof.extend(storage_keys.iter().map(|key| StorageSlotProof {
2948                proof: Vec::new(),
2949                key: *key,
2950                value: U256::zero(),
2951            }));
2952        }
2953        let account = account_opt.unwrap_or_default();
2954        let account_proof = AccountProof {
2955            proof,
2956            account,
2957            storage_proof,
2958        };
2959        Ok(Some(account_proof))
2960    }
2961
2962    // Returns an iterator across all accounts in the state trie given by the state_root
2963    // Does not check that the state_root is valid
2964    pub fn iter_accounts_from(
2965        &self,
2966        state_root: H256,
2967        starting_address: H256,
2968    ) -> Result<impl Iterator<Item = (H256, AccountState)>, StoreError> {
2969        let mut iter = self.open_locked_state_trie(state_root)?.into_iter();
2970        iter.advance(starting_address.0.to_vec())?;
2971        Ok(iter.content().map_while(|(path, value)| {
2972            Some((H256::from_slice(&path), AccountState::decode(&value).ok()?))
2973        }))
2974    }
2975
2976    // Returns an iterator across all accounts in the state trie given by the state_root
2977    // Does not check that the state_root is valid
2978    pub fn iter_accounts(
2979        &self,
2980        state_root: H256,
2981    ) -> Result<impl Iterator<Item = (H256, AccountState)>, StoreError> {
2982        self.iter_accounts_from(state_root, H256::zero())
2983    }
2984
2985    // Returns an iterator across all accounts in the state trie given by the state_root
2986    // Does not check that the state_root is valid
2987    pub fn iter_storage_from(
2988        &self,
2989        state_root: H256,
2990        hashed_address: H256,
2991        starting_slot: H256,
2992    ) -> Result<Option<impl Iterator<Item = (H256, U256)>>, StoreError> {
2993        let state_trie = self.open_locked_state_trie(state_root)?;
2994        let Some(account_rlp) = state_trie.get(hashed_address.as_bytes())? else {
2995            return Ok(None);
2996        };
2997        let storage_root = AccountState::decode(&account_rlp)?.storage_root;
2998        let mut iter = self
2999            .open_locked_storage_trie(hashed_address, state_root, storage_root)?
3000            .into_iter();
3001        iter.advance(starting_slot.0.to_vec())?;
3002        Ok(Some(iter.content().map_while(|(path, value)| {
3003            Some((H256::from_slice(&path), U256::decode(&value).ok()?))
3004        })))
3005    }
3006
3007    // Returns an iterator across all accounts in the state trie given by the state_root
3008    // Does not check that the state_root is valid
3009    pub fn iter_storage(
3010        &self,
3011        state_root: H256,
3012        hashed_address: H256,
3013    ) -> Result<Option<impl Iterator<Item = (H256, U256)>>, StoreError> {
3014        self.iter_storage_from(state_root, hashed_address, H256::zero())
3015    }
3016
3017    pub fn get_account_range_proof(
3018        &self,
3019        state_root: H256,
3020        starting_hash: H256,
3021        last_hash: Option<H256>,
3022    ) -> Result<Vec<Vec<u8>>, StoreError> {
3023        let state_trie = self.open_state_trie(state_root)?;
3024        let mut proof = state_trie.get_proof(starting_hash.as_bytes())?;
3025        if let Some(last_hash) = last_hash {
3026            proof.extend_from_slice(&state_trie.get_proof(last_hash.as_bytes())?);
3027        }
3028        Ok(proof)
3029    }
3030
3031    pub fn get_storage_range_proof(
3032        &self,
3033        state_root: H256,
3034        hashed_address: H256,
3035        starting_hash: H256,
3036        last_hash: Option<H256>,
3037    ) -> Result<Option<Vec<Vec<u8>>>, StoreError> {
3038        let state_trie = self.open_state_trie(state_root)?;
3039        let Some(account_rlp) = state_trie.get(hashed_address.as_bytes())? else {
3040            return Ok(None);
3041        };
3042        let storage_root = AccountState::decode(&account_rlp)?.storage_root;
3043        let storage_trie = self.open_storage_trie(hashed_address, state_root, storage_root)?;
3044        let mut proof = storage_trie.get_proof(starting_hash.as_bytes())?;
3045        if let Some(last_hash) = last_hash {
3046            proof.extend_from_slice(&storage_trie.get_proof(last_hash.as_bytes())?);
3047        }
3048        Ok(Some(proof))
3049    }
3050
3051    /// 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
3052    /// (aka a hashed account address) and the following paths will be paths in the account's storage trie (aka hashed storage keys)
3053    /// If only one hash (account) is received, then the state trie node containing the account will be returned.
3054    /// If more than one hash is received, then the storage trie nodes where each storage key is stored will be returned
3055    /// For more information check out snap capability message [`GetTrieNodes`](https://github.com/ethereum/devp2p/blob/master/caps/snap.md#gettrienodes-0x06)
3056    /// 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
3057    pub fn get_trie_nodes(
3058        &self,
3059        state_root: H256,
3060        paths: Vec<Vec<u8>>,
3061        byte_limit: u64,
3062    ) -> Result<Vec<Vec<u8>>, StoreError> {
3063        let Some(account_path) = paths.first() else {
3064            return Ok(vec![]);
3065        };
3066        let state_trie = self.open_state_trie(state_root)?;
3067        // State Trie Nodes Request
3068        if paths.len() == 1 {
3069            // Fetch state trie node
3070            let node = state_trie.get_node(account_path)?;
3071            return Ok(vec![node]);
3072        }
3073        // Storage Trie Nodes Request
3074        let Some(account_state) = state_trie
3075            .get(account_path)?
3076            .map(|ref rlp| AccountState::decode(rlp))
3077            .transpose()?
3078        else {
3079            return Ok(vec![]);
3080        };
3081        // We can't access the storage trie without the account's address hash
3082        let Ok(hashed_address) = account_path.clone().try_into().map(H256) else {
3083            return Ok(vec![]);
3084        };
3085        let storage_trie =
3086            self.open_storage_trie(hashed_address, state_root, account_state.storage_root)?;
3087        // Fetch storage trie nodes
3088        let mut nodes = vec![];
3089        let mut bytes_used = 0;
3090        // The number of sub-paths is bounded upstream by the snap server's per-request lookup
3091        // cap (`MAX_SERVE_LOOKUPS`), which truncates the pathset before this call; here the
3092        // byte budget only bounds response size.
3093        for path in paths.iter().skip(1) {
3094            if bytes_used >= byte_limit {
3095                break;
3096            }
3097            let node = storage_trie.get_node(path)?;
3098            bytes_used += node.len() as u64;
3099            nodes.push(node);
3100        }
3101        Ok(nodes)
3102    }
3103
3104    // Methods exclusive for trie management during snap-syncing
3105
3106    /// Snapshot the trie layer cache for reading at `state_root`, blocking until
3107    /// that root's diff layer has been installed if it is still in-flight (see
3108    /// [`PendingTrieRoots`]). This is the read barrier for deferred layer builds:
3109    /// taking it at trie-open time guarantees the snapshot contains the layer, so
3110    /// a just-added block's state is never read as stale. Roots that are not
3111    /// pending (already installed, historical/committed, genesis) never block.
3112    fn gated_snapshot(&self, state_root: H256) -> Result<Arc<TrieLayerCache>, StoreError> {
3113        self.pending_trie_roots.wait_until_ready(state_root)?;
3114        Ok(self
3115            .trie_cache
3116            .read()
3117            .map_err(|_| StoreError::LockError)?
3118            .clone())
3119    }
3120
3121    /// Obtain a state trie from the given state root
3122    /// Doesn't check if the state root is valid
3123    /// Used for internal store operations
3124    pub fn open_state_trie(&self, state_root: H256) -> Result<Trie, StoreError> {
3125        let trie_db = TrieWrapper::new(
3126            state_root,
3127            self.gated_snapshot(state_root)?,
3128            Box::new(BackendTrieDB::new_for_accounts(
3129                self.backend.clone(),
3130                self.last_written()?,
3131            )?),
3132            None,
3133        );
3134        Ok(Trie::open(Box::new(trie_db), state_root))
3135    }
3136
3137    /// Obtain a state trie from the given state root
3138    /// Doesn't check if the state root is valid
3139    /// Used for internal store operations
3140    pub fn open_direct_state_trie(&self, state_root: H256) -> Result<Trie, StoreError> {
3141        Ok(Trie::open(
3142            Box::new(BackendTrieDB::new_for_accounts(
3143                self.backend.clone(),
3144                self.last_written()?,
3145            )?),
3146            state_root,
3147        ))
3148    }
3149
3150    /// Obtain a state trie locked for reads from the given state root
3151    /// Doesn't check if the state root is valid
3152    /// Used for internal store operations
3153    pub fn open_locked_state_trie(&self, state_root: H256) -> Result<Trie, StoreError> {
3154        let trie_db = TrieWrapper::new(
3155            state_root,
3156            self.gated_snapshot(state_root)?,
3157            Box::new(state_trie_locked_backend(
3158                self.backend.as_ref(),
3159                self.last_written()?,
3160            )?),
3161            None,
3162        );
3163        Ok(Trie::open(Box::new(trie_db), state_root))
3164    }
3165
3166    /// Obtain a storage trie from the given address and storage_root.
3167    /// Doesn't check if the account is stored
3168    pub fn open_storage_trie(
3169        &self,
3170        account_hash: H256,
3171        state_root: H256,
3172        storage_root: H256,
3173    ) -> Result<Trie, StoreError> {
3174        let trie_db = TrieWrapper::new(
3175            state_root,
3176            self.gated_snapshot(state_root)?,
3177            Box::new(BackendTrieDB::new_for_storages(
3178                self.backend.clone(),
3179                self.last_written()?,
3180            )?),
3181            Some(account_hash),
3182        );
3183        Ok(Trie::open(Box::new(trie_db), storage_root))
3184    }
3185
3186    /// Open a state trie using pre-acquired shared resources.
3187    /// Avoids redundant RwLock acquisitions when multiple tries are opened
3188    /// in the same operation (e.g., state trie + storage trie in get_storage_at_root).
3189    fn open_state_trie_shared(
3190        &self,
3191        state_root: H256,
3192        read_view: Arc<dyn StorageReadView>,
3193        cache: Arc<TrieLayerCache>,
3194        last_written: Vec<u8>,
3195    ) -> Result<Trie, StoreError> {
3196        let trie_db = TrieWrapper::new(
3197            state_root,
3198            cache,
3199            Box::new(BackendTrieDB::new_for_accounts_with_view(
3200                self.backend.clone(),
3201                read_view,
3202                last_written,
3203            )?),
3204            None,
3205        );
3206        Ok(Trie::open(Box::new(trie_db), state_root))
3207    }
3208
3209    /// Open a storage trie using pre-acquired shared resources.
3210    fn open_storage_trie_shared(
3211        &self,
3212        account_hash: H256,
3213        state_root: H256,
3214        storage_root: H256,
3215        read_view: Arc<dyn StorageReadView>,
3216        cache: Arc<TrieLayerCache>,
3217        last_written: Vec<u8>,
3218    ) -> Result<Trie, StoreError> {
3219        let trie_db = TrieWrapper::new(
3220            state_root,
3221            cache,
3222            Box::new(BackendTrieDB::new_for_storages_with_view(
3223                self.backend.clone(),
3224                read_view,
3225                last_written,
3226            )?),
3227            Some(account_hash),
3228        );
3229        Ok(Trie::open(Box::new(trie_db), storage_root))
3230    }
3231
3232    /// Obtain a storage trie from the given address and storage_root.
3233    /// Doesn't check if the account is stored
3234    pub fn open_direct_storage_trie(
3235        &self,
3236        account_hash: H256,
3237        storage_root: H256,
3238    ) -> Result<Trie, StoreError> {
3239        Ok(Trie::open(
3240            Box::new(BackendTrieDB::new_for_account_storage(
3241                self.backend.clone(),
3242                account_hash,
3243                self.last_written()?,
3244            )?),
3245            storage_root,
3246        ))
3247    }
3248
3249    /// Obtain a read-locked storage trie from the given address and storage_root.
3250    /// Doesn't check if the account is stored
3251    pub fn open_locked_storage_trie(
3252        &self,
3253        account_hash: H256,
3254        state_root: H256,
3255        storage_root: H256,
3256    ) -> Result<Trie, StoreError> {
3257        let trie_db = TrieWrapper::new(
3258            state_root,
3259            self.gated_snapshot(state_root)?,
3260            Box::new(state_trie_locked_backend(
3261                self.backend.as_ref(),
3262                self.last_written()?,
3263            )?),
3264            Some(account_hash),
3265        );
3266        Ok(Trie::open(Box::new(trie_db), storage_root))
3267    }
3268
3269    pub fn has_state_root(&self, state_root: H256) -> Result<bool, StoreError> {
3270        // Empty state trie is always available
3271        if state_root == *EMPTY_TRIE_HASH {
3272            return Ok(true);
3273        }
3274        let trie = self.open_state_trie(state_root)?;
3275        // NOTE: here we hash the root because the trie doesn't check the state root is correct
3276        let Some(root) = trie.db().get(Nibbles::default())? else {
3277            return Ok(false);
3278        };
3279        let root_hash = ethrex_trie::Node::decode(&root)?
3280            .compute_hash(&NativeCrypto)
3281            .finalize(&NativeCrypto);
3282        Ok(state_root == root_hash)
3283    }
3284
3285    /// Takes a block hash and returns an iterator to its ancestors. Block headers are returned
3286    /// in reverse order, starting from the given block and going up to the genesis block.
3287    pub fn ancestors(&self, block_hash: BlockHash) -> AncestorIterator {
3288        AncestorIterator {
3289            store: self.clone(),
3290            next_hash: block_hash,
3291        }
3292    }
3293
3294    /// Checks if a given block belongs to the current canonical chain. Returns false if the block is not known
3295    pub fn is_canonical_sync(&self, block_hash: BlockHash) -> Result<bool, StoreError> {
3296        let Some(block_number) = self.get_block_number_sync(block_hash)? else {
3297            return Ok(false);
3298        };
3299        Ok(self
3300            .get_canonical_block_hash_sync(block_number)?
3301            .is_some_and(|h| h == block_hash))
3302    }
3303
3304    pub fn generate_flatkeyvalue(&self) -> Result<(), StoreError> {
3305        self.flatkeyvalue_control_tx
3306            .send(FKVGeneratorControlMessage::Continue)
3307            .map_err(|_| StoreError::Custom("FlatKeyValue thread disconnected.".to_string()))
3308    }
3309
3310    pub fn create_checkpoint(&self, path: impl AsRef<Path>) -> Result<(), StoreError> {
3311        self.backend.create_checkpoint(path.as_ref())?;
3312        init_metadata_file(path.as_ref())?;
3313        Ok(())
3314    }
3315
3316    pub fn get_store_directory(&self) -> Result<PathBuf, StoreError> {
3317        Ok(self.db_path.clone())
3318    }
3319
3320    /// Loads the latest block number stored in the database, bypassing the latest block number cache
3321    async fn load_latest_block_number(&self) -> Result<Option<BlockNumber>, StoreError> {
3322        let key = chain_data_key(ChainDataIndex::LatestBlockNumber);
3323        self.read_async(CHAIN_DATA, key)
3324            .await?
3325            .map(|bytes| -> Result<BlockNumber, StoreError> {
3326                let array: [u8; 8] = bytes
3327                    .try_into()
3328                    .map_err(|_| StoreError::Custom("Invalid BlockNumber bytes".to_string()))?;
3329                Ok(BlockNumber::from_le_bytes(array))
3330            })
3331            .transpose()
3332    }
3333
3334    fn load_canonical_block_hash(
3335        &self,
3336        block_number: BlockNumber,
3337    ) -> Result<Option<BlockHash>, StoreError> {
3338        let txn = self.backend.begin_read()?;
3339        txn.get(
3340            CANONICAL_BLOCK_HASHES,
3341            block_number.to_le_bytes().as_slice(),
3342        )?
3343        .map(|bytes| H256::decode(bytes.as_slice()))
3344        .transpose()
3345        .map_err(StoreError::from)
3346    }
3347
3348    fn load_block_header(
3349        &self,
3350        block_number: BlockNumber,
3351    ) -> Result<Option<BlockHeader>, StoreError> {
3352        let Some(block_hash) = self.load_canonical_block_hash(block_number)? else {
3353            return Ok(None);
3354        };
3355        self.load_block_header_by_hash(block_hash)
3356    }
3357
3358    /// Load a block header, bypassing the latest header cache
3359    fn load_block_header_by_hash(
3360        &self,
3361        block_hash: BlockHash,
3362    ) -> Result<Option<BlockHeader>, StoreError> {
3363        let txn = self.backend.begin_read()?;
3364        let hash_key = block_hash.encode_to_vec();
3365        let header_value = txn.get(HEADERS, hash_key.as_slice())?;
3366        let mut header = header_value
3367            .map(|bytes| BlockHeaderRLP::from_bytes(bytes).to())
3368            .transpose()
3369            .map_err(StoreError::from)?;
3370        header.as_mut().inspect(|h| {
3371            // Set the hash so we avoid recomputing it later
3372            let _ = h.hash.set(block_hash);
3373        });
3374        Ok(header)
3375    }
3376
3377    pub fn last_written(&self) -> Result<Vec<u8>, StoreError> {
3378        let last_computed_flatkeyvalue = self
3379            .last_computed_flatkeyvalue
3380            .read()
3381            .map_err(|_| StoreError::LockError)?;
3382        Ok(last_computed_flatkeyvalue.clone())
3383    }
3384
3385    fn flatkeyvalue_computed_with_last_written(account: H256, last_written: &[u8]) -> bool {
3386        let account_nibbles = Nibbles::from_bytes(account.as_bytes());
3387        &last_written[0..64] > account_nibbles.as_ref()
3388    }
3389
3390    /// Returns the highest block number durably flushed to disk, or `0` when
3391    /// the marker is absent. Use [`Self::read_flushed_upto_opt`] when you need
3392    /// to distinguish "absent marker" (legacy DB, everything is durable) from
3393    /// "marker present and equal to 0".
3394    pub fn read_flushed_upto(&self) -> Result<BlockNumber, StoreError> {
3395        Ok(self.read_flushed_upto_opt()?.unwrap_or(0))
3396    }
3397
3398    /// Returns `None` when the marker has never been written — a legacy or fresh
3399    /// DB where everything is durable and the head must not be clamped to 0.
3400    fn read_flushed_upto_opt(&self) -> Result<Option<BlockNumber>, StoreError> {
3401        let tx = self.backend.begin_read()?;
3402        match tx.get(MISC_VALUES, FLUSHED_UPTO_KEY)? {
3403            Some(bytes) => Ok(Some(decode_flushed_upto(&bytes)?)),
3404            None => Ok(None),
3405        }
3406    }
3407
3408    /// Insert a block into the in-memory buffer without writing to disk.
3409    /// For testing only — gates production code off.
3410    #[cfg(any(test, feature = "testing"))]
3411    pub fn buffer_block_for_test(&self, block: &Block) {
3412        mutate_block_buffer(&self.block_data_buffer, |b| {
3413            b.insert(block.clone(), vec![], vec![])
3414        })
3415        .expect("block_data_buffer lock poisoned");
3416    }
3417
3418    /// Synchronously flush the block data buffer to disk.
3419    /// For testing only — gates production code off.
3420    #[cfg(any(test, feature = "testing"))]
3421    pub fn flush_block_data_for_test(&self) -> Result<(), StoreError> {
3422        flush_block_data(self.backend.as_ref(), &self.block_data_buffer)
3423    }
3424
3425    /// Read a raw trie node straight from the on-disk account/storage trie-node
3426    /// table by its committed key. For testing only — lets a reopen assert which
3427    /// trie diff-layers a shutdown flush did (or did not) commit to disk.
3428    #[cfg(any(test, feature = "testing"))]
3429    pub fn get_trie_node_for_test(
3430        &self,
3431        is_account: bool,
3432        key: &[u8],
3433    ) -> Result<Option<Vec<u8>>, StoreError> {
3434        let table = if is_account {
3435            ACCOUNT_TRIE_NODES
3436        } else {
3437            STORAGE_TRIE_NODES
3438        };
3439        self.backend.begin_read()?.get(table, key)
3440    }
3441
3442    /// Insert a block plus associated codes into the in-memory buffer without
3443    /// writing to disk.  For testing only — proves the buffer overlay resolves
3444    /// code that has not been persisted yet.
3445    #[cfg(any(test, feature = "testing"))]
3446    pub fn buffer_block_with_codes_for_test(&self, block: &Block, codes: Vec<(H256, Code)>) {
3447        mutate_block_buffer(&self.block_data_buffer, |b| {
3448            b.insert(block.clone(), vec![], codes)
3449        })
3450        .expect("block_data_buffer lock poisoned");
3451    }
3452
3453    /// Mark a state root as in-flight (build pending) without doing a build.
3454    /// For testing only — simulates the window where the persist worker has not
3455    /// yet installed the layer, so reads at this root must block in
3456    /// `gated_snapshot`.
3457    #[cfg(any(test, feature = "testing"))]
3458    pub fn register_pending_root_for_test(&self, root: H256) -> Result<(), StoreError> {
3459        self.pending_trie_roots.register(root)
3460    }
3461
3462    /// Clear an in-flight state root (simulates the worker having installed the
3463    /// layer), unblocking readers waiting in `gated_snapshot`. For testing only.
3464    #[cfg(any(test, feature = "testing"))]
3465    pub fn clear_pending_root_for_test(&self, root: H256) {
3466        self.pending_trie_roots.clear(root)
3467    }
3468
3469    /// Boot-time recovery: clamp `latest_block_header` to the durable head.
3470    ///
3471    /// Durable head = `min(flushed_upto, latest)` when the marker is present
3472    /// (buffered blocks past `flushed_upto` may be lost after a crash; the CL
3473    /// re-sends them via `newPayload`). When the marker is absent the DB
3474    /// predates deferred persistence and everything is on disk — use `latest`
3475    /// as-is, never rewind to 0. On first boot the marker is seeded so a later
3476    /// crash clamps against it rather than an absent (→ 0) marker.
3477    ///
3478    /// The marker tracks the max flushed *block number*, not which hash is
3479    /// canonical at that height. A tip reorg inside the flush window — `Na` at
3480    /// height N is flushed (marker = N), then `newPayload(Nb)` buffers a sibling
3481    /// and FCU durably repoints `canonical[N]` to the still-unflushed `Nb` — can
3482    /// leave `canonical[head]` resolving to a header that never reached disk if
3483    /// we crash before `Nb` flushes. So we walk `head` down to the highest height
3484    /// whose canonical hash actually resolves on disk rather than bricking with
3485    /// `MissingLatestBlockNumber`. A legacy DB (no marker) is exempt: everything
3486    /// there is durable, so a missing header is real corruption and must surface.
3487    async fn anchor_to_durable_head(&self, latest: BlockNumber) -> Result<(), StoreError> {
3488        let marker = self.read_flushed_upto_opt()?;
3489        let start = match marker {
3490            Some(flushed) => flushed.min(latest),
3491            None => latest,
3492        };
3493
3494        let mut head = start;
3495        let latest_block_header = loop {
3496            match self.load_block_header(head)? {
3497                Some(header) => break header,
3498                // Legacy/fresh DB: everything is supposed to be durable, so a
3499                // missing header is real corruption — surface it, don't rewind.
3500                None if marker.is_none() => return Err(StoreError::MissingLatestBlockNumber),
3501                None if head == 0 => return Err(StoreError::MissingLatestBlockNumber),
3502                None => {
3503                    warn!(
3504                        "durable head {head}: canonical hash has no on-disk header \
3505                         (reorg inside flush window); rewinding"
3506                    );
3507                    head -= 1;
3508                }
3509            }
3510        };
3511        self.latest_block_header.update(latest_block_header);
3512
3513        // Re-anchor the persisted head when we moved below `latest`, and (re)write
3514        // the marker to the resolved head: an absent marker is seeded to the
3515        // durable baseline, and a walked-down head lowers the marker so a later
3516        // crash clamps against a hash known to resolve.
3517        let reanchor = head != latest;
3518        let rewrite_marker = marker != Some(head);
3519        if reanchor || rewrite_marker {
3520            let mut tx = self.backend.begin_write()?;
3521            if reanchor {
3522                // Re-anchor the persisted head so `get_latest_block_number` and
3523                // every downstream consumer agree with the clamped head.
3524                let latest_key = chain_data_key(ChainDataIndex::LatestBlockNumber);
3525                tx.put(CHAIN_DATA, &latest_key, &head.to_le_bytes())?;
3526            }
3527            if rewrite_marker {
3528                write_flushed_upto(tx.as_mut(), head)?;
3529            }
3530            tx.commit()?;
3531        }
3532        Ok(())
3533    }
3534}
3535
3536/// Writes the `flushed_upto` block number into an open write batch.
3537///
3538/// The caller is responsible for committing `tx` afterward.
3539pub fn write_flushed_upto(
3540    tx: &mut dyn StorageWriteBatch,
3541    n: BlockNumber,
3542) -> Result<(), StoreError> {
3543    tx.put(MISC_VALUES, FLUSHED_UPTO_KEY, &n.to_le_bytes())
3544}
3545
3546/// Decode an 8-byte little-endian `flushed_upto` marker value.
3547///
3548/// Returns an error for a present-but-malformed value so on-disk corruption is
3549/// surfaced loudly rather than silently resetting the durable marker. Single
3550/// source of truth for both `from_backend` and [`Store::read_flushed_upto`].
3551fn decode_flushed_upto(bytes: &[u8]) -> Result<BlockNumber, StoreError> {
3552    let arr: [u8; 8] = bytes
3553        .try_into()
3554        .map_err(|_| StoreError::Custom("Invalid flushed_upto bytes".to_string()))?;
3555    Ok(BlockNumber::from_le_bytes(arr))
3556}
3557
3558/// RCU-swap the block-data buffer. The persist worker is the sole caller in
3559/// production (no lost-update race); test helpers also call this on one thread.
3560fn mutate_block_buffer(
3561    buffer: &Arc<RwLock<Arc<BlockDataBuffer>>>,
3562    f: impl FnOnce(&mut BlockDataBuffer),
3563) -> Result<(), StoreError> {
3564    let mut new_buf = (*buffer.read().map_err(|_| StoreError::LockError)?.clone()).clone();
3565    f(&mut new_buf);
3566    *buffer.write().map_err(|_| StoreError::LockError)? = Arc::new(new_buf);
3567    Ok(())
3568}
3569
3570/// Default for [`StoreConfig::persist_channel_capacity`].
3571const DEFAULT_PERSIST_CHANNEL_CAPACITY: usize = 2;
3572
3573/// One unit of work for the persist worker: stage block(s), build the trie
3574/// diff-layer, flush to disk. `wait_for_flush` selects the ack point: `false`
3575/// (live) acks after staging carrying the prior flush result; `true` (batch)
3576/// acks after flush.
3577struct BlockPersist {
3578    blocks: Vec<(Block, Vec<Receipt>)>,
3579    codes: Vec<(H256, Code)>,
3580    parent_state_root: H256,
3581    child_state_root: H256,
3582    account_updates: TrieNodesUpdate,
3583    storage_updates: Vec<(H256, TrieNodesUpdate)>,
3584    wait_for_flush: bool,
3585    ack: std::sync::mpsc::SyncSender<Result<(), StoreError>>,
3586}
3587
3588/// Messages for the persist worker. `Ping(ack)` is the idle handshake for
3589/// [`Store::wait_for_persistence_idle`]: the FIFO worker handles it only after
3590/// all earlier `Block` messages are fully processed.
3591enum PersistMessage {
3592    Block(BlockPersist),
3593    Ping(std::sync::mpsc::SyncSender<Result<(), StoreError>>),
3594    /// Graceful-shutdown handshake. Handled only after every earlier `Block`
3595    /// (FIFO), so it both drains in-flight work and force-flushes the block-data
3596    /// buffer to disk. The trie diff-layers are deliberately left in memory (see
3597    /// [`Store::shutdown`]). The worker acks and exits.
3598    Shutdown {
3599        ack: std::sync::mpsc::SyncSender<Result<(), StoreError>>,
3600    },
3601}
3602
3603/// Write one block's header, body, number, and tx locations into an open batch.
3604/// Shared by [`Store::add_blocks`] (sync import) and [`flush_block_data`]
3605/// (deferred flush) so the on-disk encoding stays in lockstep. Receipts and codes
3606/// are written by callers that need them (only `flush_block_data` does).
3607fn write_block_data(
3608    tx: &mut dyn StorageWriteBatch,
3609    number: BlockNumber,
3610    hash: BlockHash,
3611    header: &BlockHeader,
3612    body: &BlockBody,
3613) -> Result<(), StoreError> {
3614    let hash_key = hash.encode_to_vec();
3615    tx.put(
3616        HEADERS,
3617        &hash_key,
3618        BlockHeaderRLP::from(header.clone()).bytes(),
3619    )?;
3620    tx.put(
3621        BODIES,
3622        &hash_key,
3623        BlockBodyRLP::from_bytes(body.encode_to_vec()).bytes(),
3624    )?;
3625    tx.put(BLOCK_NUMBERS, &hash_key, &number.to_le_bytes())?;
3626    for (index, transaction) in body.transactions.iter().enumerate() {
3627        tx.merge(
3628            TRANSACTION_LOCATIONS,
3629            transaction.hash(&NativeCrypto).as_bytes(),
3630            &encode_tx_location_operand(number, hash, index as u64),
3631        )?;
3632    }
3633    Ok(())
3634}
3635
3636/// Write all unflushed blocks to disk in one tx, advance `flushed_upto`, then
3637/// evict. Eviction is gap-safe: blocks stay buffered until the commit succeeds.
3638fn flush_block_data(
3639    backend: &dyn StorageBackend,
3640    buffer: &Arc<RwLock<Arc<BlockDataBuffer>>>,
3641) -> Result<(), StoreError> {
3642    let snapshot = buffer.read().map_err(|_| StoreError::LockError)?.clone();
3643    let to_flush = snapshot.flushable();
3644    if to_flush.is_empty() {
3645        return Ok(());
3646    }
3647    let hashes: Vec<_> = to_flush.iter().map(|b| b.header.hash()).collect();
3648    let codes = snapshot.codes_for(&hashes);
3649    let mut max_number = snapshot.flushed_upto();
3650
3651    let mut tx = backend.begin_write()?;
3652    for b in &to_flush {
3653        let hash = b.header.hash();
3654        write_block_data(tx.as_mut(), b.number, hash, &b.header, &b.body)?;
3655        for (index, receipt) in b.receipts.iter().enumerate() {
3656            tx.put(
3657                RECEIPTS_V2,
3658                &receipt_key(&hash, index as u64),
3659                &receipt.encode_to_vec(),
3660            )?;
3661        }
3662        max_number = max_number.max(b.number);
3663    }
3664    for (code_hash, code) in codes {
3665        let buf = encode_code(&code);
3666        tx.put(ACCOUNT_CODES, code_hash.as_ref(), &buf)?;
3667        tx.put(
3668            ACCOUNT_CODE_METADATA,
3669            code_hash.as_ref(),
3670            &(code.len() as u64).to_be_bytes(),
3671        )?;
3672    }
3673    write_flushed_upto(tx.as_mut(), max_number)?;
3674    tx.commit()?;
3675
3676    // Phase 3: evict only after the commit succeeded (gap safety).
3677    mutate_block_buffer(buffer, |b| b.evict_flushed(max_number))
3678}
3679
3680type TrieNodesUpdate = Vec<(Nibbles, Vec<u8>)>;
3681
3682/// Tracks state roots whose trie diff-layer is in-flight (building but not yet
3683/// installed in `trie_cache`). `apply_updates` registers a root *before*
3684/// returning; the worker clears it *after* swapping the layer in. This ordering
3685/// is mandatory: a reader opening a trie at a pending root blocks until the
3686/// layer is installed, preventing stale on-disk reads.
3687#[derive(Debug, Default)]
3688struct PendingTrieRoots {
3689    /// Fast-path: when zero, nothing is in flight and readers skip the lock.
3690    count: AtomicUsize,
3691    roots: Mutex<HashSet<H256>>,
3692    ready: Condvar,
3693}
3694
3695impl PendingTrieRoots {
3696    /// Mark `root` as in-flight. MUST be called before the build is handed to
3697    /// the worker (so the worker's `clear` always finds it) and before the head
3698    /// can advance to `root` (so any reader that can reference it sees it pending).
3699    fn register(&self, root: H256) -> Result<(), StoreError> {
3700        let mut roots = self.roots.lock().map_err(|_| StoreError::LockError)?;
3701        if roots.insert(root) {
3702            self.count.fetch_add(1, Ordering::Release);
3703        }
3704        Ok(())
3705    }
3706
3707    /// Mark `root` as installed and wake any waiting readers. MUST be called only
3708    /// after the layer is swapped into `trie_cache`, so a woken reader sees it.
3709    /// Best-effort: a poisoned lock means a reader's `wait_until_ready` also errors,
3710    /// so no reader deadlocks.
3711    fn clear(&self, root: H256) {
3712        let Ok(mut roots) = self.roots.lock() else {
3713            return;
3714        };
3715        if roots.remove(&root) {
3716            self.count.fetch_sub(1, Ordering::Release);
3717            self.ready.notify_all();
3718        }
3719    }
3720
3721    /// Block until `root` is no longer in-flight (its layer is installed). Returns
3722    /// immediately on the fast path when nothing is pending.
3723    fn wait_until_ready(&self, root: H256) -> Result<(), StoreError> {
3724        if self.count.load(Ordering::Acquire) == 0 {
3725            return Ok(());
3726        }
3727        let mut roots = self.roots.lock().map_err(|_| StoreError::LockError)?;
3728        while roots.contains(&root) {
3729            roots = self.ready.wait(roots).map_err(|_| StoreError::LockError)?;
3730        }
3731        Ok(())
3732    }
3733}
3734
3735/// Build the trie diff-layer, RCU-swap it into `trie_cache`, then clear the
3736/// pending root. Swap MUST precede the clear so a woken reader sees the layer.
3737/// On swap failure the root is still cleared so gated readers error, not deadlock.
3738fn apply_trie_phase1(
3739    trie_cache: &Arc<RwLock<Arc<TrieLayerCache>>>,
3740    pending_roots: &PendingTrieRoots,
3741    parent_state_root: H256,
3742    child_state_root: H256,
3743    account_updates: TrieNodesUpdate,
3744    storage_updates: Vec<(H256, TrieNodesUpdate)>,
3745) -> Result<(), StoreError> {
3746    let build: Result<(), StoreError> = (|| {
3747        let new_layer = storage_updates
3748            .into_iter()
3749            .flat_map(|(account_hash, nodes)| {
3750                nodes
3751                    .into_iter()
3752                    .map(move |(path, node)| (apply_prefix(Some(account_hash), path), node))
3753            })
3754            .chain(account_updates)
3755            .collect();
3756        let trie = trie_cache
3757            .read()
3758            .map_err(|_| StoreError::LockError)?
3759            .clone();
3760        let mut trie_mut = (*trie).clone();
3761        trie_mut.put_batch(parent_state_root, child_state_root, new_layer);
3762        *trie_cache.write().map_err(|_| StoreError::LockError)? = Arc::new(trie_mut);
3763        Ok(())
3764    })();
3765    // Always clear the pending root, whether or not the swap succeeded: on success
3766    // readers see the installed layer; on failure (poisoning) the lock is poisoned
3767    // so gated readers error rather than read stale, and we must not leave them
3768    // blocked forever.
3769    pending_roots.clear(child_state_root);
3770    build
3771}
3772
3773/// When the diff-layer chain is deep enough, flush the bottom layer to disk and
3774/// RCU-evict it. `is_batch` selects `BATCH_COMMIT_THRESHOLD` (full sync) over
3775/// the default per-block threshold. No-ops when nothing is committable.
3776fn commit_trie_if_due(
3777    backend: &dyn StorageBackend,
3778    trie_cache: &Arc<RwLock<Arc<TrieLayerCache>>>,
3779    fkv_ctl: &SyncSender<FKVGeneratorControlMessage>,
3780    parent_state_root: H256,
3781    is_batch: bool,
3782) -> Result<(), StoreError> {
3783    let trie = trie_cache
3784        .read()
3785        .map_err(|_| StoreError::LockError)?
3786        .clone();
3787    // Phase 2: update disk layer.
3788    let commitable = if is_batch {
3789        trie.get_commitable_with_threshold(parent_state_root, BATCH_COMMIT_THRESHOLD)
3790    } else {
3791        trie.get_commitable(parent_state_root)
3792    };
3793    let Some(root) = commitable else {
3794        // Nothing to commit to disk, move on.
3795        return Ok(());
3796    };
3797    commit_trie_layers(backend, trie_cache, fkv_ctl, &trie, root)
3798}
3799
3800/// Writes the layer at `root` and all of its ancestors to disk in one tx, then
3801/// RCU-evicts them from the cache. Used by the per-block "commit when due" path
3802/// ([`commit_trie_if_due`]). `trie` is the caller's snapshot of the cache;
3803/// `root` must be one of its layer keys.
3804fn commit_trie_layers(
3805    backend: &dyn StorageBackend,
3806    trie_cache: &Arc<RwLock<Arc<TrieLayerCache>>>,
3807    fkv_ctl: &SyncSender<FKVGeneratorControlMessage>,
3808    trie: &Arc<TrieLayerCache>,
3809    root: H256,
3810) -> Result<(), StoreError> {
3811    // Stop the flat-key-value generator thread, as the underlying trie is about to change.
3812    // Ignore the error, if the channel is closed it means there is no worker to notify.
3813    let _ = fkv_ctl.send(FKVGeneratorControlMessage::Stop);
3814
3815    // RCU to remove the bottom layer: update step needs to happen after disk layer is updated.
3816    let mut trie_mut = (**trie).clone();
3817
3818    let last_written = backend
3819        .begin_read()?
3820        .get(MISC_VALUES, "last_written".as_bytes())?
3821        .unwrap_or_default();
3822
3823    let mut write_tx = backend.begin_write()?;
3824
3825    // Before encoding, accounts have only the account address as their path, while storage keys have
3826    // the account address (32 bytes) + storage path (up to 32 bytes).
3827
3828    // Commit removes the bottom layer and returns it, this is the mutation step.
3829    let nodes = trie_mut.commit(root).unwrap_or_default();
3830    let mut result = Ok(());
3831    for (key, value) in nodes {
3832        let is_leaf = key.len() == 65 || key.len() == 131;
3833        let is_account = key.len() <= 65;
3834
3835        if is_leaf && key > last_written {
3836            continue;
3837        }
3838        let table = if is_leaf {
3839            if is_account {
3840                &ACCOUNT_FLATKEYVALUE
3841            } else {
3842                &STORAGE_FLATKEYVALUE
3843            }
3844        } else if is_account {
3845            &ACCOUNT_TRIE_NODES
3846        } else {
3847            &STORAGE_TRIE_NODES
3848        };
3849        if value.is_empty() {
3850            result = write_tx.delete(table, &key);
3851        } else {
3852            result = write_tx.put(table, &key, &value);
3853        }
3854        if result.is_err() {
3855            break;
3856        }
3857    }
3858    if result.is_ok() {
3859        result = write_tx.commit();
3860    }
3861    // We want to send this message even if there was an error during the batch write
3862    let _ = fkv_ctl.send(FKVGeneratorControlMessage::Continue);
3863    result?;
3864    // Phase 3: update diff layers with the removal of bottom layer.
3865    *trie_cache.write().map_err(|_| StoreError::LockError)? = Arc::new(trie_mut);
3866    Ok(())
3867}
3868
3869// NOTE: we don't receive `Store` here to avoid cyclic dependencies
3870// with the other end of `control_rx`
3871fn flatkeyvalue_generator(
3872    backend: &Arc<dyn StorageBackend>,
3873    last_computed_fkv: &RwLock<Vec<u8>>,
3874    control_rx: &std::sync::mpsc::Receiver<FKVGeneratorControlMessage>,
3875) -> Result<(), StoreError> {
3876    info!("Generation of FlatKeyValue started.");
3877    let initial_last_written = backend
3878        .begin_read()?
3879        .get(MISC_VALUES, "last_written".as_bytes())?
3880        .unwrap_or_default();
3881
3882    if initial_last_written.is_empty() {
3883        // First time generating the FKV. Remove all FKV entries just in case
3884        backend.clear_table(ACCOUNT_FLATKEYVALUE)?;
3885        backend.clear_table(STORAGE_FLATKEYVALUE)?;
3886    } else if initial_last_written == [0xff] {
3887        // FKV was already generated
3888        info!("FlatKeyValue already generated. Skipping.");
3889        return Ok(());
3890    }
3891
3892    loop {
3893        // Acquire a fresh read view per iteration so updates performed while the
3894        // generator is paused are visible after a Continue signal.
3895        let read_tx = backend.begin_read()?;
3896        let root = read_tx
3897            .get(ACCOUNT_TRIE_NODES, &[])?
3898            .ok_or(StoreError::MissingLatestBlockNumber)?;
3899        let root: Node = ethrex_trie::Node::decode(&root)?;
3900        let state_root = root.compute_hash(&NativeCrypto).finalize(&NativeCrypto);
3901
3902        let last_written = read_tx
3903            .get(MISC_VALUES, "last_written".as_bytes())?
3904            .unwrap_or_default();
3905        let last_written_account = last_written
3906            .get(0..64)
3907            .map(|v| Nibbles::from_hex(v.to_vec()))
3908            .unwrap_or_default();
3909        let mut last_written_storage = last_written
3910            .get(66..130)
3911            .map(|v| Nibbles::from_hex(v.to_vec()))
3912            .unwrap_or_default();
3913
3914        debug!("Starting FlatKeyValue loop pivot={last_written:?} SR={state_root:x}");
3915
3916        let mut ctr = 0;
3917        let mut write_txn = backend.begin_write()?;
3918        let mut iter = Trie::open(
3919            Box::new(BackendTrieDB::new_for_accounts_with_view(
3920                backend.clone(),
3921                read_tx.clone(),
3922                last_written.clone(),
3923            )?),
3924            state_root,
3925        )
3926        .into_iter();
3927        if last_written_account > Nibbles::default() {
3928            iter.advance(last_written_account.to_bytes())?;
3929        }
3930        let res = iter.try_for_each(|(path, node)| -> Result<(), StoreError> {
3931            let Node::Leaf(node) = node else {
3932                return Ok(());
3933            };
3934            let account_state = AccountState::decode(&node.value)?;
3935            let account_hash = H256::from_slice(&path.to_bytes());
3936            write_txn.put(MISC_VALUES, "last_written".as_bytes(), path.as_ref())?;
3937            write_txn.put(ACCOUNT_FLATKEYVALUE, path.as_ref(), &node.value)?;
3938            ctr += 1;
3939            if ctr > 10_000 {
3940                write_txn.commit()?;
3941                write_txn = backend.begin_write()?;
3942                *last_computed_fkv
3943                    .write()
3944                    .map_err(|_| StoreError::LockError)? = path.as_ref().to_vec();
3945                ctr = 0;
3946            }
3947
3948            let mut iter_inner = Trie::open(
3949                Box::new(BackendTrieDB::new_for_account_storage_with_view(
3950                    backend.clone(),
3951                    read_tx.clone(),
3952                    account_hash,
3953                    path.as_ref().to_vec(),
3954                )?),
3955                account_state.storage_root,
3956            )
3957            .into_iter();
3958            if last_written_storage > Nibbles::default() {
3959                iter_inner.advance(last_written_storage.to_bytes())?;
3960                last_written_storage = Nibbles::default();
3961            }
3962            iter_inner.try_for_each(|(path, node)| -> Result<(), StoreError> {
3963                let Node::Leaf(node) = node else {
3964                    return Ok(());
3965                };
3966                let key = apply_prefix(Some(account_hash), path);
3967                write_txn.put(MISC_VALUES, "last_written".as_bytes(), key.as_ref())?;
3968                write_txn.put(STORAGE_FLATKEYVALUE, key.as_ref(), &node.value)?;
3969                ctr += 1;
3970                if ctr > 10_000 {
3971                    write_txn.commit()?;
3972                    write_txn = backend.begin_write()?;
3973                    *last_computed_fkv
3974                        .write()
3975                        .map_err(|_| StoreError::LockError)? = key.into_vec();
3976                    ctr = 0;
3977                }
3978                fkv_check_for_stop_msg(control_rx)?;
3979                Ok(())
3980            })?;
3981            fkv_check_for_stop_msg(control_rx)?;
3982            Ok(())
3983        });
3984        match res {
3985            Err(StoreError::PivotChanged) => {
3986                match control_rx.recv() {
3987                    Ok(FKVGeneratorControlMessage::Continue) => {}
3988                    Ok(FKVGeneratorControlMessage::Stop) => {
3989                        return Err(StoreError::Custom("Unexpected Stop message".to_string()));
3990                    }
3991                    // If the channel was closed, we stop generation prematurely
3992                    Err(std::sync::mpsc::RecvError) => {
3993                        info!("Store closed, stopping FlatKeyValue generation.");
3994                        return Ok(());
3995                    }
3996                }
3997            }
3998            Err(err) => return Err(err),
3999            Ok(()) => {
4000                write_txn.put(MISC_VALUES, "last_written".as_bytes(), &[0xff])?;
4001                write_txn.commit()?;
4002                *last_computed_fkv
4003                    .write()
4004                    .map_err(|_| StoreError::LockError)? = vec![0xff; 131];
4005                info!("FlatKeyValue generation finished.");
4006                return Ok(());
4007            }
4008        };
4009    }
4010}
4011
4012fn fkv_check_for_stop_msg(
4013    control_rx: &std::sync::mpsc::Receiver<FKVGeneratorControlMessage>,
4014) -> Result<(), StoreError> {
4015    match control_rx.try_recv() {
4016        Ok(FKVGeneratorControlMessage::Stop) | Err(TryRecvError::Disconnected) => {
4017            return Err(StoreError::PivotChanged);
4018        }
4019        Ok(FKVGeneratorControlMessage::Continue) => {
4020            return Err(StoreError::Custom(
4021                "Unexpected Continue message".to_string(),
4022            ));
4023        }
4024        Err(TryRecvError::Empty) => {}
4025    }
4026    Ok(())
4027}
4028
4029fn state_trie_locked_backend(
4030    backend: &dyn StorageBackend,
4031    last_written: Vec<u8>,
4032) -> Result<BackendTrieDBLocked, StoreError> {
4033    // No address prefix for state trie
4034    BackendTrieDBLocked::new(backend, last_written)
4035}
4036
4037pub struct AccountProof {
4038    pub proof: Vec<NodeRLP>,
4039    pub account: AccountState,
4040    pub storage_proof: Vec<StorageSlotProof>,
4041}
4042
4043pub struct StorageSlotProof {
4044    pub proof: Vec<NodeRLP>,
4045    pub key: H256,
4046    pub value: U256,
4047}
4048
4049pub struct AncestorIterator {
4050    store: Store,
4051    next_hash: BlockHash,
4052}
4053
4054impl Iterator for AncestorIterator {
4055    type Item = Result<(BlockHash, BlockHeader), StoreError>;
4056
4057    fn next(&mut self) -> Option<Self::Item> {
4058        let next_hash = self.next_hash;
4059        // Buffer-aware: a not-yet-flushed ancestor (e.g. on a side branch during
4060        // a reorg) must be visible here, or a BLOCKHASH opcode resolving through
4061        // this walk would wrongly reject a valid block.
4062        match self.store.get_block_header_by_hash(next_hash) {
4063            Ok(Some(header)) => {
4064                let ret_hash = self.next_hash;
4065                self.next_hash = header.parent_hash;
4066                Some(Ok((ret_hash, header)))
4067            }
4068            Ok(None) => None,
4069            Err(e) => Some(Err(e)),
4070        }
4071    }
4072}
4073
4074pub fn hash_address(address: &Address) -> Vec<u8> {
4075    keccak_hash(address.to_fixed_bytes()).to_vec()
4076}
4077
4078fn hash_address_fixed(address: &Address) -> H256 {
4079    keccak(address.to_fixed_bytes())
4080}
4081
4082pub fn hash_key(key: &H256) -> Vec<u8> {
4083    keccak_hash(key.to_fixed_bytes()).to_vec()
4084}
4085
4086pub fn hash_key_fixed(key: &H256) -> [u8; 32] {
4087    keccak_hash(key.to_fixed_bytes())
4088}
4089
4090fn chain_data_key(index: ChainDataIndex) -> Vec<u8> {
4091    (index as u8).encode_to_vec()
4092}
4093
4094fn snap_state_key(index: SnapStateIndex) -> Vec<u8> {
4095    (index as u8).encode_to_vec()
4096}
4097
4098/// Builds a fixed-width RECEIPTS key: block_hash (32B) || index (8B BE).
4099pub fn receipt_key(block_hash: &BlockHash, index: u64) -> Vec<u8> {
4100    let mut key = Vec::with_capacity(40);
4101    key.extend_from_slice(block_hash.as_bytes());
4102    key.extend_from_slice(&index.to_be_bytes());
4103    key
4104}
4105
4106fn encode_code(code: &Code) -> Vec<u8> {
4107    let mut buf =
4108        Vec::with_capacity(6 + code.len() + std::mem::size_of_val::<[u32]>(&code.jump_targets));
4109    code.code().encode(&mut buf);
4110    // `Arc<[u32]>` (the in-memory share) has no `RLPEncode` impl; encode through an
4111    // owned `Vec` on this cold DB-write path (code is persisted once per hash).
4112    code.jump_targets.to_vec().encode(&mut buf);
4113    buf
4114}
4115
4116#[derive(Debug, Default, Clone)]
4117struct LatestBlockHeaderCache {
4118    current: Arc<Mutex<Arc<BlockHeader>>>,
4119}
4120
4121impl LatestBlockHeaderCache {
4122    pub fn get(&self) -> Arc<BlockHeader> {
4123        self.current.lock().expect("poisoned mutex").clone()
4124    }
4125
4126    pub fn update(&self, header: BlockHeader) {
4127        let new = Arc::new(header);
4128        *self.current.lock().expect("poisoned mutex") = new;
4129    }
4130}
4131
4132#[derive(Debug, Serialize, Deserialize)]
4133pub struct StoreMetadata {
4134    pub schema_version: u64,
4135}
4136
4137impl StoreMetadata {
4138    pub fn new(schema_version: u64) -> Self {
4139        Self { schema_version }
4140    }
4141}
4142
4143/// Reads the schema version from the metadata file, if it exists.
4144///
4145/// Returns `Some(version)` when metadata.json is present and valid,
4146/// or `None` when the file does not exist.
4147fn read_store_schema_version(path: &Path) -> Result<Option<u64>, StoreError> {
4148    let metadata_path = path.join(STORE_METADATA_FILENAME);
4149    if !metadata_path.exists() {
4150        return Ok(None);
4151    }
4152    if !metadata_path.is_file() {
4153        return Err(StoreError::Custom(
4154            "store schema path exists but is not a file".to_string(),
4155        ));
4156    }
4157    let file_contents = std::fs::read_to_string(metadata_path)?;
4158    let metadata: StoreMetadata = serde_json::from_str(&file_contents)?;
4159    Ok(Some(metadata.schema_version))
4160}
4161
4162fn init_metadata_file(parent_path: &Path) -> Result<(), StoreError> {
4163    std::fs::create_dir_all(parent_path)?;
4164
4165    let metadata_path = parent_path.join(STORE_METADATA_FILENAME);
4166    let metadata = StoreMetadata::new(STORE_SCHEMA_VERSION);
4167    let serialized_metadata = serde_json::to_string_pretty(&metadata)?;
4168    let mut new_file = std::fs::File::create_new(metadata_path)?;
4169    new_file.write_all(serialized_metadata.as_bytes())?;
4170    Ok(())
4171}
4172
4173/// Returns `true` if `path` contains a *legacy* database — one written before
4174/// the metadata file existed, so it has no `metadata.json` to identify it.
4175/// Detected by RocksDB's own marker files, as opposed to unrelated files that
4176/// merely share the datadir. Only meaningful once metadata has been confirmed
4177/// absent; otherwise prefer `has_valid_db`, which keys off the metadata file.
4178///
4179/// Previously the caller treated *any* non-empty directory as such a legacy
4180/// database, which made startup fail when unrelated files lived alongside the DB
4181/// — e.g. EthDocker writes the JWT secret into the datadir (issue #5680). We
4182/// instead look for RocksDB's marker files, so a datadir that only contains such
4183/// unrelated files is correctly treated as fresh.
4184fn dir_contains_legacy_db(path: &Path) -> Result<bool, StoreError> {
4185    // `CURRENT` has a fixed name and is written by every RocksDB instance, so
4186    // check for it directly instead of scanning a datadir that may hold many
4187    // unrelated files.
4188    if path.join("CURRENT").is_file() {
4189        return Ok(true);
4190    }
4191    // The manifest has a numeric suffix (`MANIFEST-<n>`), so it can only be
4192    // found by scanning. Restrict to plain files: a directory that happens to
4193    // share the name is not a database marker.
4194    for entry in std::fs::read_dir(path)? {
4195        let entry = entry?;
4196        if !entry.file_type()?.is_file() {
4197            continue;
4198        }
4199        if entry.file_name().to_string_lossy().starts_with("MANIFEST-") {
4200            return Ok(true);
4201        }
4202    }
4203    Ok(false)
4204}
4205
4206/// Checks whether a valid (or migratable) database exists at the given path
4207/// by looking for a metadata.json file with a schema version between 1 and
4208/// `STORE_SCHEMA_VERSION` (inclusive).
4209pub fn has_valid_db(path: &Path) -> bool {
4210    let metadata_path = path.join(STORE_METADATA_FILENAME);
4211    if !metadata_path.is_file() {
4212        return false;
4213    }
4214    let Ok(contents) = std::fs::read_to_string(&metadata_path) else {
4215        return false;
4216    };
4217    let Ok(metadata) = serde_json::from_str::<StoreMetadata>(&contents) else {
4218        return false;
4219    };
4220    metadata.schema_version >= 1 && metadata.schema_version <= STORE_SCHEMA_VERSION
4221}
4222
4223/// Reads the chain ID from an existing database without performing a full
4224/// store initialization. Returns `None` if the database doesn't exist or
4225/// the chain config can't be read. Always returns `None` when compiled
4226/// without the `rocksdb` feature.
4227///
4228/// Each failure mode logs a warning so callers (and operators) can diagnose
4229/// why an existing database was not usable — previously every error was
4230/// silently swallowed by `.ok()?`.
4231pub fn read_chain_id_from_db(path: &Path) -> Option<u64> {
4232    if !has_valid_db(path) {
4233        return None;
4234    }
4235    #[cfg(feature = "rocksdb")]
4236    {
4237        // The cache size is irrelevant for this one-shot chain-id read (the LRU
4238        // is sized as a ceiling, not pre-allocated), so we use the default.
4239        let backend = match RocksDBBackend::open(path, DEFAULT_ROCKSDB_BLOCK_CACHE_SIZE_BYTES) {
4240            Ok(backend) => backend,
4241            Err(e) => {
4242                warn!("Failed to open RocksDB at {path:?} to read chain ID: {e}");
4243                return None;
4244            }
4245        };
4246        let read = match backend.begin_read() {
4247            Ok(read) => read,
4248            Err(e) => {
4249                warn!("Failed to begin read transaction at {path:?}: {e}");
4250                return None;
4251            }
4252        };
4253        let key = chain_data_key(ChainDataIndex::ChainConfig);
4254        let bytes = match read.get(CHAIN_DATA, &key) {
4255            Ok(Some(bytes)) => bytes,
4256            Ok(None) => {
4257                warn!("Chain config entry not found in database at {path:?}");
4258                return None;
4259            }
4260            Err(e) => {
4261                warn!("Failed to read chain config from database at {path:?}: {e}");
4262                return None;
4263            }
4264        };
4265        // Only extract chain_id here: the stored `ChainConfig` JSON may include
4266        // fields whose serialization changed across releases (e.g. pre-v10 wrote
4267        // `terminal_total_difficulty` as a plain number, v10 expects hex string).
4268        // Deserializing the full struct would reject otherwise-migratable v9 data.
4269        #[derive(serde::Deserialize)]
4270        #[serde(rename_all = "camelCase")]
4271        struct ChainIdOnly {
4272            chain_id: u64,
4273        }
4274        match serde_json::from_slice::<ChainIdOnly>(&bytes) {
4275            Ok(partial) => Some(partial.chain_id),
4276            Err(e) => {
4277                warn!("Failed to deserialize chain ID from database at {path:?}: {e}");
4278                None
4279            }
4280        }
4281    }
4282    #[cfg(not(feature = "rocksdb"))]
4283    {
4284        let _ = path;
4285        None
4286    }
4287}
4288
4289#[cfg(test)]
4290mod merge_tests {
4291    use super::*;
4292
4293    fn h256(b: u8) -> H256 {
4294        H256::from_low_u64_be(b as u64)
4295    }
4296
4297    fn op(bn: BlockNumber, bh: H256, idx: Index) -> Vec<u8> {
4298        encode_tx_location_operand(bn, bh, idx)
4299    }
4300
4301    fn decode(v: &[u8]) -> Vec<(BlockNumber, BlockHash, Index)> {
4302        <Vec<(BlockNumber, BlockHash, Index)>>::decode(v).unwrap()
4303    }
4304
4305    #[test]
4306    fn single_operand_on_empty_base() {
4307        let out = tx_locations_merge(None, vec![op(100, h256(0x10), 0)]).unwrap();
4308        assert_eq!(decode(&out), vec![(100, h256(0x10), 0)]);
4309    }
4310
4311    #[test]
4312    fn operand_appended_to_existing_base() {
4313        let base = vec![(100u64, h256(0x10), 0u64)].encode_to_vec();
4314        let out = tx_locations_merge(Some(&base), vec![op(101, h256(0x11), 5)]).unwrap();
4315        let mut got = decode(&out);
4316        got.sort();
4317        let mut want = vec![(100, h256(0x10), 0), (101, h256(0x11), 5)];
4318        want.sort();
4319        assert_eq!(got, want);
4320    }
4321
4322    #[test]
4323    fn multiple_operands_combined() {
4324        let out = tx_locations_merge(
4325            None,
4326            vec![
4327                op(100, h256(0x10), 0),
4328                op(100, h256(0x11), 1),
4329                op(101, h256(0x12), 2),
4330            ],
4331        )
4332        .unwrap();
4333        assert_eq!(decode(&out).len(), 3);
4334    }
4335
4336    #[test]
4337    fn same_block_hash_is_deduped() {
4338        // Two operands with the same block_hash: the later one replaces the earlier.
4339        let out =
4340            tx_locations_merge(None, vec![op(100, h256(0x10), 0), op(100, h256(0x10), 7)]).unwrap();
4341        assert_eq!(decode(&out), vec![(100, h256(0x10), 7)]);
4342    }
4343
4344    #[test]
4345    fn malformed_operand_aborts_merge() {
4346        // Fail loud: a malformed operand must abort the merge (return None), not
4347        // silently drop it and commit a partial result.
4348        let out = tx_locations_merge(None, vec![vec![0xff, 0xff], op(100, h256(0x10), 0)]);
4349        assert!(out.is_none(), "merge must abort on a malformed operand");
4350    }
4351
4352    #[test]
4353    fn malformed_base_value_aborts_merge() {
4354        let out = tx_locations_merge(Some(&[0xff, 0xff]), vec![op(100, h256(0x10), 0)]);
4355        assert!(out.is_none(), "merge must abort on a corrupt base value");
4356    }
4357
4358    /// Regression for the associative-merge format bug: a PartialMerge result
4359    /// must be re-mergeable as an operand. RocksDB folds operands together
4360    /// without a base value during compaction, then feeds that result back into
4361    /// a later merge. If the operand format differed from the output format,
4362    /// the re-fed result would fail to decode and entries would be dropped
4363    /// (observed as 1664 silent drops during a compaction pass on mainnet).
4364    #[test]
4365    fn partial_merge_result_is_a_valid_operand() {
4366        // Step 1: PartialMerge — combine operands with NO base value.
4367        let partial =
4368            tx_locations_merge(None, vec![op(100, h256(0x10), 0), op(101, h256(0x11), 1)]).unwrap();
4369
4370        // Step 2: the partial result is now itself an operand in a later merge,
4371        // on top of an existing base value. This is the path that used to drop
4372        // entries.
4373        let base = vec![(99u64, h256(0x09), 9u64)].encode_to_vec();
4374        let out = tx_locations_merge(Some(&base), vec![partial]).unwrap();
4375
4376        let mut got = decode(&out);
4377        got.sort();
4378        let mut want = vec![
4379            (99, h256(0x09), 9),
4380            (100, h256(0x10), 0),
4381            (101, h256(0x11), 1),
4382        ];
4383        want.sort();
4384        assert_eq!(
4385            got, want,
4386            "no entries may be lost when re-merging a partial result"
4387        );
4388    }
4389
4390    /// Operand and stored-value encodings must be byte-identical types, so a
4391    /// freshly-encoded operand round-trips through the value decoder.
4392    #[test]
4393    fn operand_encoding_matches_value_encoding() {
4394        let operand = op(100, h256(0x10), 3);
4395        // Decoding the operand as the stored Vec type must succeed.
4396        assert_eq!(decode(&operand), vec![(100, h256(0x10), 3)]);
4397    }
4398
4399    /// Chained PartialMerges (operand-only folds applied repeatedly) stay valid.
4400    #[test]
4401    fn chained_partial_merges() {
4402        let p1 = tx_locations_merge(None, vec![op(1, h256(0x01), 0)]).unwrap();
4403        let p2 = tx_locations_merge(None, vec![p1, op(2, h256(0x02), 0)]).unwrap();
4404        let p3 = tx_locations_merge(None, vec![p2, op(3, h256(0x03), 0)]).unwrap();
4405        let out = tx_locations_merge(None, vec![p3]).unwrap();
4406        assert_eq!(decode(&out).len(), 3);
4407    }
4408}
4409
4410#[cfg(test)]
4411mod datadir_tests {
4412    use super::*;
4413    use std::fs;
4414
4415    #[test]
4416    fn empty_dir_has_no_existing_db() {
4417        let dir = tempfile::tempdir().unwrap();
4418        assert!(!dir_contains_legacy_db(dir.path()).unwrap());
4419    }
4420
4421    #[test]
4422    fn dir_with_only_unrelated_files_has_no_existing_db() {
4423        // Regression for #5680: a JWT secret (or any unrelated file) in the
4424        // datadir must not be mistaken for an existing database.
4425        let dir = tempfile::tempdir().unwrap();
4426        fs::write(dir.path().join("jwt.hex"), "0xdeadbeef").unwrap();
4427        fs::write(dir.path().join("LOG"), "noise").unwrap();
4428        assert!(!dir_contains_legacy_db(dir.path()).unwrap());
4429    }
4430
4431    #[test]
4432    fn dir_with_rocksdb_markers_has_existing_db() {
4433        // A `CURRENT` file (and, separately, a `MANIFEST-*` file) marks a real DB.
4434        let dir = tempfile::tempdir().unwrap();
4435        fs::write(dir.path().join("CURRENT"), "MANIFEST-000001\n").unwrap();
4436        assert!(dir_contains_legacy_db(dir.path()).unwrap());
4437
4438        let dir2 = tempfile::tempdir().unwrap();
4439        fs::write(dir2.path().join("MANIFEST-000007"), "x").unwrap();
4440        assert!(dir_contains_legacy_db(dir2.path()).unwrap());
4441    }
4442
4443    #[test]
4444    fn dir_with_marker_named_subdirectories_has_no_existing_db() {
4445        // A *directory* named like a marker file must not be mistaken for a DB;
4446        // RocksDB only ever writes these as plain files.
4447        let dir = tempfile::tempdir().unwrap();
4448        fs::create_dir(dir.path().join("CURRENT")).unwrap();
4449        fs::create_dir(dir.path().join("MANIFEST-000001")).unwrap();
4450        assert!(!dir_contains_legacy_db(dir.path()).unwrap());
4451    }
4452}