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