Skip to main content

ethrex_storage/
store.rs

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