#[cfg(feature = "rocksdb")]
use crate::backend::rocksdb::RocksDBBackend;
use crate::{
STORE_METADATA_FILENAME, STORE_SCHEMA_VERSION,
api::{
StorageBackend, StorageReadView, StorageWriteBatch,
tables::{
ACCOUNT_CODE_METADATA, ACCOUNT_CODES, ACCOUNT_FLATKEYVALUE, ACCOUNT_TRIE_NODES,
BAD_BLOCKS, BLOCK_ACCESS_LISTS, BLOCK_NUMBERS, BODIES, CANONICAL_BLOCK_HASHES,
CHAIN_DATA, EXECUTION_WITNESSES, FULLSYNC_HEADERS, HEADERS, INVALID_CHAINS,
MISC_VALUES, PENDING_BLOCKS, RECEIPTS_V2, SNAP_STATE, STATE_HISTORY,
STORAGE_FLATKEYVALUE, STORAGE_TRIE_NODES, TRANSACTION_LOCATIONS,
},
},
apply_prefix,
backend::in_memory::InMemoryBackend,
block_data_buffer::BlockDataBuffer,
error::StoreError,
journal::{FlatDiff, JournalEntry},
layering::{Overlay, OverlayCf, TrieLayerCache, TrieWrapper},
rlp::{BlockBodyRLP, BlockHeaderRLP, BlockRLP},
trie::{BackendTrieDB, BackendTrieDBLocked, classify_trie_key},
utils::{ChainDataIndex, SnapStateIndex},
};
use ethrex_common::{
Address, H256, U256,
types::{
AccountInfo, AccountState, AccountUpdate, Block, BlockBody, BlockHash, BlockHeader,
BlockNumber, ChainConfig, Code, CodeMetadata, ForkId, Genesis, GenesisAccount, Index,
Receipt, Transaction,
block_access_list::BlockAccessList,
block_execution_witness::{ExecutionWitness, RpcExecutionWitness},
},
utils::keccak,
};
use ethrex_crypto::{NativeCrypto, keccak::keccak_hash};
use ethrex_rlp::{
decode::{RLPDecode, decode_bytes, decode_rlp_item},
encode::RLPEncode,
};
use ethrex_trie::{EMPTY_TRIE_HASH, Nibbles, Trie, TrieLogger, TrieNode, TrieWitness};
use ethrex_trie::{Node, NodeRLP};
use indexmap::IndexMap;
use lru::LruCache;
use rayon::prelude::*;
use rustc_hash::FxBuildHasher;
use serde::{Deserialize, Serialize};
use std::{
collections::{BTreeMap, HashMap, HashSet, hash_map::Entry},
fmt::Debug,
hash::BuildHasher,
io::Write,
num::NonZeroUsize,
path::{Path, PathBuf},
sync::{
Arc, Condvar, Mutex, RwLock,
atomic::{AtomicUsize, Ordering},
mpsc::{SyncSender, TryRecvError, sync_channel},
},
thread::JoinHandle,
};
use tracing::{debug, error, info, warn};
pub const MAX_WITNESSES: u64 = 128;
pub const DB_COMMIT_THRESHOLD: usize = 128;
const IN_MEMORY_COMMIT_THRESHOLD: usize = 10000;
pub const BATCH_COMMIT_THRESHOLD: usize = 4;
const KEYS_PER_SHARD: usize = 256;
const MAX_SHARDS: usize = 64;
fn shard_count(n: usize) -> usize {
n.div_ceil(KEYS_PER_SHARD).clamp(1, MAX_SHARDS)
}
pub const MAX_ROCKSDB_BLOCK_CACHE_SIZE_BYTES: usize = 12 * 1024 * 1024 * 1024;
pub const MIN_ROCKSDB_BLOCK_CACHE_SIZE_BYTES: usize = 512 * 1024 * 1024;
pub const ROCKSDB_BLOCK_CACHE_MEMORY_PERCENT: usize = 40;
pub fn default_rocksdb_block_cache_size() -> usize {
let physical = physical_memory_bytes();
let cgroup = cgroup_memory_limit_bytes();
let limit = [physical, cgroup].into_iter().flatten().min();
let size = rocksdb_block_cache_size_for(limit);
let source = match (physical, cgroup) {
(None, None) => "undetected, falling back to the ceiling",
(Some(_), None) => "physical memory",
(None, Some(_)) => "cgroup limit",
(Some(p), Some(c)) => {
if c <= p {
"cgroup limit"
} else {
"physical memory"
}
}
};
info!(
detected_limit_bytes = ?limit,
source,
cache_bytes = size,
"sized RocksDB block cache"
);
size
}
pub fn rocksdb_block_cache_size_for(memory_limit: Option<usize>) -> usize {
let Some(limit) = memory_limit else {
return MAX_ROCKSDB_BLOCK_CACHE_SIZE_BYTES;
};
(limit.saturating_mul(ROCKSDB_BLOCK_CACHE_MEMORY_PERCENT) / 100).clamp(
MIN_ROCKSDB_BLOCK_CACHE_SIZE_BYTES,
MAX_ROCKSDB_BLOCK_CACHE_SIZE_BYTES,
)
}
fn physical_memory_bytes() -> Option<usize> {
let meminfo = std::fs::read_to_string("/proc/meminfo").ok()?;
let kib: usize = meminfo
.lines()
.find_map(|line| line.strip_prefix("MemTotal:"))?
.split_whitespace()
.next()?
.parse()
.ok()?;
kib.checked_mul(1024)
}
fn cgroup_memory_limit_bytes() -> Option<usize> {
let cgroups = std::fs::read_to_string("/proc/self/cgroup").ok()?;
let v2 = cgroups
.lines()
.find_map(|line| line.strip_prefix("0::"))
.and_then(|relative| {
cgroup_limit_up_to_root(Path::new("/sys/fs/cgroup"), relative, "memory.max")
});
v2.or_else(|| {
let relative = cgroups.lines().find_map(|line| {
let mut fields = line.splitn(3, ':');
let controllers = fields.nth(1)?;
let path = fields.next()?;
controllers
.split(',')
.any(|controller| controller == "memory")
.then_some(path)
})?;
cgroup_limit_up_to_root(
Path::new("/sys/fs/cgroup/memory"),
relative,
"memory.limit_in_bytes",
)
})
}
fn cgroup_limit_up_to_root(mount: &Path, relative: &str, file: &str) -> Option<usize> {
let mut dir = mount.join(relative.trim_start_matches('/'));
let mut limit: Option<usize> = None;
loop {
if let Some(value) = std::fs::read_to_string(dir.join(file))
.ok()
.and_then(|contents| contents.trim().parse::<usize>().ok())
{
limit = Some(limit.map_or(value, |current| current.min(value)));
}
if dir == mount || !dir.pop() {
return limit;
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct StoreConfig {
pub rocksdb_block_cache_size: usize,
pub code_cache_size: Option<u64>,
pub persist_channel_capacity: usize,
}
impl StoreConfig {
pub fn with_rocksdb_block_cache_size(rocksdb_block_cache_size: usize) -> Self {
Self {
rocksdb_block_cache_size,
code_cache_size: None,
persist_channel_capacity: DEFAULT_PERSIST_CHANNEL_CAPACITY,
}
}
}
impl Default for StoreConfig {
fn default() -> Self {
Self::with_rocksdb_block_cache_size(default_rocksdb_block_cache_size())
}
}
#[derive(Debug, PartialEq)]
enum FKVGeneratorControlMessage {
Stop,
Continue,
}
const CODE_CACHE_MAX_SIZE: u64 = 2 * 1024 * 1024 * 1024;
const CODE_METADATA_CACHE_MAX_ENTRIES: usize = (16 * 1024 * 1024) / 64;
const FLUSHED_UPTO_KEY: &[u8] = b"bodies_flushed_upto";
const BAD_BLOCKS_KEY: &[u8] = b"bad_blocks";
const MAX_BAD_BLOCKS: usize = 16;
const CODE_CACHE_ENTRY_OVERHEAD: u64 = 160;
struct CodeCache {
entries: IndexMap<H256, Code, FxBuildHasher>,
cache_size: u64,
max_size: u64,
rng: u64,
}
impl std::fmt::Debug for CodeCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CodeCache")
.field("len", &self.entries.len())
.field("cache_size", &self.cache_size)
.field("max_size", &self.max_size)
.finish_non_exhaustive()
}
}
impl Default for CodeCache {
fn default() -> Self {
Self {
entries: IndexMap::default(),
cache_size: 0,
max_size: CODE_CACHE_MAX_SIZE,
rng: std::collections::hash_map::RandomState::new().hash_one(0u64) | 1,
}
}
}
impl CodeCache {
fn entry_size(code: &Code) -> u64 {
(code.size() as u64).saturating_add(CODE_CACHE_ENTRY_OVERHEAD)
}
fn get(&self, code_hash: &H256) -> Result<Option<Code>, StoreError> {
Ok(self.entries.get(code_hash).cloned())
}
fn contains(&self, code_hash: &H256) -> bool {
self.entries.contains_key(code_hash)
}
fn insert(&mut self, code: &Code) -> Result<(), StoreError> {
if self.entries.contains_key(&code.hash) {
return Ok(());
}
let size = Self::entry_size(code);
if size > self.max_size {
return Ok(());
}
while self.cache_size + size > self.max_size && !self.entries.is_empty() {
let victim = (self.next_random() % self.entries.len() as u64) as usize;
if let Some((_, evicted)) = self.entries.swap_remove_index(victim) {
self.cache_size -= Self::entry_size(&evicted);
}
}
self.cache_size += size;
self.entries.insert(code.hash, code.clone());
let cache_len = self.entries.len();
let current_size = self.cache_size;
debug!(
"[ACCOUNT CODE CACHE] cache elements (): {cache_len}, total size: {current_size} bytes"
);
Ok(())
}
fn next_random(&mut self) -> u64 {
self.rng ^= self.rng >> 12;
self.rng ^= self.rng << 25;
self.rng ^= self.rng >> 27;
self.rng.wrapping_mul(0x2545_F491_4F6C_DD1D)
}
}
#[derive(Debug, Clone)]
pub struct Store {
db_path: PathBuf,
backend: Arc<dyn StorageBackend>,
chain_config: ChainConfig,
trie_cache: Arc<RwLock<Arc<TrieLayerCache>>>,
flatkeyvalue_control_tx: std::sync::mpsc::SyncSender<FKVGeneratorControlMessage>,
block_data_buffer: Arc<RwLock<Arc<BlockDataBuffer>>>,
persist_tx: std::sync::mpsc::SyncSender<PersistMessage>,
pending_trie_roots: Arc<PendingTrieRoots>,
latest_block_header: LatestBlockHeaderCache,
last_computed_flatkeyvalue: Arc<RwLock<Vec<u8>>>,
account_code_cache: Arc<Mutex<CodeCache>>,
code_metadata_cache: Arc<Mutex<LruCache<H256, CodeMetadata, FxBuildHasher>>>,
fcu_lock: Arc<tokio::sync::Mutex<()>>,
safe_commit_root: Arc<RwLock<H256>>,
journal_pruning_paused: Arc<std::sync::atomic::AtomicBool>,
background_threads: Arc<ThreadList>,
}
#[derive(Debug, Default)]
struct ThreadList {
list: Vec<JoinHandle<()>>,
}
impl Drop for ThreadList {
fn drop(&mut self) {
for handle in self.list.drain(..) {
let _ = handle.join();
}
}
}
pub type StorageTrieNodes = Vec<(H256, Vec<(Nibbles, Vec<u8>)>)>;
type StorageTries = HashMap<Address, (TrieWitness, Trie)>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EngineType {
InMemory,
#[cfg(feature = "rocksdb")]
RocksDB,
}
pub struct UpdateBatch {
pub account_updates: Vec<TrieNode>,
pub storage_updates: Vec<(H256, Vec<TrieNode>)>,
pub blocks: Vec<Block>,
pub receipts: Vec<(H256, Vec<Receipt>)>,
pub code_updates: Vec<(H256, Code)>,
pub commit_depth: Option<usize>,
pub wait_for_flush: bool,
}
pub struct BackfilledBlock {
pub header: BlockHeader,
pub body: BlockBody,
pub receipts: Vec<Receipt>,
pub index_transactions: bool,
}
pub type StorageUpdates = Vec<(H256, Vec<(Nibbles, Vec<u8>)>)>;
pub struct AccountUpdatesList {
pub state_trie_hash: H256,
pub state_updates: Vec<(Nibbles, Vec<u8>)>,
pub storage_updates: StorageUpdates,
pub code_updates: Vec<(H256, Code)>,
}
pub(crate) fn encode_tx_location_operand(
block_number: BlockNumber,
block_hash: BlockHash,
index: Index,
) -> Vec<u8> {
vec![(block_number, block_hash, index)].encode_to_vec()
}
pub fn tx_locations_merge(
existing: Option<&[u8]>,
operands: impl IntoIterator<Item = impl AsRef<[u8]>>,
) -> Option<Vec<u8>> {
fn fold_chunk(
list: &mut Vec<(BlockNumber, BlockHash, Index)>,
bytes: &[u8],
what: &str,
) -> bool {
match <Vec<(BlockNumber, BlockHash, Index)>>::decode(bytes) {
Ok(entries) => {
for (bn, bh, idx) in entries {
list.retain(|(_, existing_bh, _)| *existing_bh != bh);
list.push((bn, bh, idx));
}
true
}
Err(e) => {
error!(
"tx_locations_merge: failed to decode {what} ({} bytes): {e}; \
aborting merge to avoid silent data loss",
bytes.len()
);
false
}
}
}
let mut list: Vec<(BlockNumber, BlockHash, Index)> = Vec::new();
if let Some(bytes) = existing
&& !fold_chunk(&mut list, bytes, "existing value")
{
return None;
}
for op in operands {
if !fold_chunk(&mut list, op.as_ref(), "operand") {
return None;
}
}
Some(list.encode_to_vec())
}
impl Store {
pub async fn wait_for_persistence_idle(&self) -> Result<(), StoreError> {
let tx = self.persist_tx.clone();
tokio::task::spawn_blocking(move || {
let (ack_tx, ack_rx) = sync_channel::<Result<(), StoreError>>(1);
tx.send(PersistMessage::Ping(ack_tx))
.map_err(|e| StoreError::Custom(format!("wait_for_persistence_idle send: {e}")))?;
ack_rx
.recv()
.map_err(|e| StoreError::Custom(format!("wait_for_persistence_idle ack: {e}")))?
})
.await
.map_err(|e| StoreError::Custom(format!("wait_for_persistence_idle join: {e}")))?
}
pub async fn shutdown(&self) -> Result<(), StoreError> {
let tx = self.persist_tx.clone();
let backend = self.backend.clone();
tokio::task::spawn_blocking(move || {
let (ack_tx, ack_rx) = sync_channel::<Result<(), StoreError>>(1);
tx.send(PersistMessage::Shutdown { ack: ack_tx })
.map_err(|e| StoreError::Custom(format!("shutdown send: {e}")))?;
ack_rx
.recv()
.map_err(|e| StoreError::Custom(format!("shutdown ack: {e}")))??;
backend.flush()
})
.await
.map_err(|e| StoreError::Custom(format!("shutdown join: {e}")))?
}
pub async fn add_block(&self, block: Block) -> Result<(), StoreError> {
self.add_blocks(vec![block]).await
}
pub async fn add_blocks(&self, blocks: Vec<Block>) -> Result<(), StoreError> {
let db = self.backend.clone();
tokio::task::spawn_blocking(move || {
let mut tx = db.begin_write()?;
for block in blocks {
write_block_data(
tx.as_mut(),
block.header.number,
block.hash(),
&block.header,
&block.body,
)?;
}
tx.commit()
})
.await
.map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
}
pub async fn add_block_header(
&self,
block_hash: BlockHash,
block_header: BlockHeader,
) -> Result<(), StoreError> {
let hash_key = block_hash.encode_to_vec();
let header_value = BlockHeaderRLP::from(block_header).into_vec();
self.write_async(HEADERS, hash_key, header_value).await
}
pub async fn add_block_headers(
&self,
block_headers: Vec<BlockHeader>,
) -> Result<(), StoreError> {
let mut txn = self.backend.begin_write()?;
for header in block_headers {
let block_hash = header.hash();
let block_number = header.number;
let hash_key = block_hash.encode_to_vec();
let header_value = BlockHeaderRLP::from(header).into_vec();
txn.put(HEADERS, &hash_key, &header_value)?;
let number_key = block_number.to_le_bytes().to_vec();
txn.put(BLOCK_NUMBERS, &hash_key, &number_key)?;
}
txn.commit()?;
Ok(())
}
pub fn get_block_header(
&self,
block_number: BlockNumber,
) -> Result<Option<BlockHeader>, StoreError> {
let latest = self.latest_block_header.get();
if block_number == latest.number {
return Ok(Some((*latest).clone()));
}
let Some(block_hash) = self.get_canonical_block_hash_sync(block_number)? else {
return Ok(None);
};
self.get_block_header_by_hash(block_hash)
}
pub async fn add_block_body(
&self,
block_hash: BlockHash,
block_body: BlockBody,
) -> Result<(), StoreError> {
let hash_key = block_hash.encode_to_vec();
let body_value = BlockBodyRLP::from(block_body).into_vec();
self.write_async(BODIES, hash_key, body_value).await
}
pub async fn add_backfilled_blocks(
&self,
blocks: Vec<BackfilledBlock>,
new_earliest: BlockNumber,
) -> Result<(), StoreError> {
let backend = self.backend.clone();
tokio::task::spawn_blocking(move || {
let mut txn = backend.begin_write()?;
for block in &blocks {
let hash = block.header.hash();
let hash_key = hash.encode_to_vec();
txn.put(
BODIES,
&hash_key,
&BlockBodyRLP::from(block.body.clone()).into_vec(),
)?;
for (index, receipt) in block.receipts.iter().enumerate() {
txn.put(
RECEIPTS_V2,
&receipt_key(&hash, index as u64),
&receipt.encode_storage(),
)?;
}
if block.index_transactions {
for (index, transaction) in block.body.transactions.iter().enumerate() {
txn.merge(
TRANSACTION_LOCATIONS,
transaction.hash(&NativeCrypto).as_bytes(),
&encode_tx_location_operand(block.header.number, hash, index as u64),
)?;
}
}
}
txn.put(
CHAIN_DATA,
&chain_data_key(ChainDataIndex::EarliestBlockNumber),
&new_earliest.to_le_bytes(),
)?;
txn.commit()
})
.await
.map_err(|e| StoreError::Custom(format!("Backfill write task panicked: {e}")))?
}
pub fn rocksdb_stats(&self) -> Option<crate::api::RocksDbStats> {
self.backend.db_stats()
}
pub async fn get_block_body(
&self,
block_number: BlockNumber,
) -> Result<Option<BlockBody>, StoreError> {
let Some(block_hash) = self.get_canonical_block_hash_sync(block_number)? else {
return Ok(None);
};
self.get_block_body_by_hash(block_hash).await
}
pub async fn remove_block(&self, block_number: BlockNumber) -> Result<(), StoreError> {
let Some(hash) = self.get_canonical_block_hash_sync(block_number)? else {
return Ok(());
};
let backend = self.backend.clone();
tokio::task::spawn_blocking(move || {
let hash_key = hash.encode_to_vec();
let mut txn = backend.begin_write()?;
txn.delete(
CANONICAL_BLOCK_HASHES,
block_number.to_le_bytes().as_slice(),
)?;
txn.delete(BODIES, &hash_key)?;
txn.delete(HEADERS, &hash_key)?;
txn.delete(BLOCK_NUMBERS, &hash_key)?;
txn.commit()
})
.await
.map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
}
pub async fn get_block_bodies(
&self,
from: BlockNumber,
to: BlockNumber,
) -> Result<Vec<Option<BlockBody>>, StoreError> {
let buffer = self.buffer()?;
let backend = self.backend.clone();
tokio::task::spawn_blocking(move || {
let numbers: Vec<BlockNumber> = (from..=to).collect();
let mut block_bodies = Vec::new();
let txn = backend.begin_read()?;
for number in numbers {
let Some(hash) = txn
.get(CANONICAL_BLOCK_HASHES, number.to_le_bytes().as_slice())?
.map(|bytes| H256::decode(bytes.as_slice()))
.transpose()?
else {
block_bodies.push(None);
continue;
};
if let Some(body) = buffer.get_body(&hash) {
block_bodies.push(Some(body));
continue;
}
let hash_key = hash.encode_to_vec();
let block_body_opt = txn
.get(BODIES, &hash_key)?
.map(|bytes| BlockBodyRLP::from_bytes(bytes).to())
.transpose()
.map_err(StoreError::from)?;
block_bodies.push(block_body_opt);
}
Ok(block_bodies)
})
.await
.map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
}
pub async fn get_block_bodies_by_hash(
&self,
hashes: Vec<BlockHash>,
) -> Result<Vec<BlockBody>, StoreError> {
let buffer = self.buffer()?;
let backend = self.backend.clone();
tokio::task::spawn_blocking(move || {
let txn = backend.begin_read()?;
let mut block_bodies = Vec::new();
for hash in hashes {
if let Some(body) = buffer.get_body(&hash) {
block_bodies.push(body);
continue;
}
let hash_key = hash.encode_to_vec();
let Some(block_body) = txn
.get(BODIES, &hash_key)?
.map(|bytes| BlockBodyRLP::from_bytes(bytes).to())
.transpose()
.map_err(StoreError::from)?
else {
return Err(StoreError::Custom(format!(
"Block body not found for hash: {hash}"
)));
};
block_bodies.push(block_body);
}
Ok(block_bodies)
})
.await
.map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
}
pub async fn get_block_body_by_hash(
&self,
block_hash: BlockHash,
) -> Result<Option<BlockBody>, StoreError> {
if let Some(b) = self.buffer()?.get_body(&block_hash) {
return Ok(Some(b));
}
self.read_async(BODIES, block_hash.encode_to_vec())
.await?
.map(|bytes| BlockBodyRLP::from_bytes(bytes).to())
.transpose()
.map_err(StoreError::from)
}
pub fn get_block_header_by_hash(
&self,
block_hash: BlockHash,
) -> Result<Option<BlockHeader>, StoreError> {
let latest = self.latest_block_header.get();
if block_hash == latest.hash() {
return Ok(Some((*latest).clone()));
}
if let Some(h) = self.buffer()?.get_header(&block_hash) {
return Ok(Some(h));
}
self.load_block_header_by_hash(block_hash)
}
pub fn add_pending_block(&self, block: Block) -> Result<(), StoreError> {
let block_hash = block.hash();
let block_value = BlockRLP::from(block).into_vec();
self.write(PENDING_BLOCKS, block_hash.as_bytes().to_vec(), block_value)
}
pub async fn get_pending_block(
&self,
block_hash: BlockHash,
) -> Result<Option<Block>, StoreError> {
self.read_async(PENDING_BLOCKS, block_hash.as_bytes().to_vec())
.await?
.map(|bytes| BlockRLP::from_bytes(bytes).to())
.transpose()
.map_err(StoreError::from)
}
pub async fn add_block_number(
&self,
block_hash: BlockHash,
block_number: BlockNumber,
) -> Result<(), StoreError> {
let number_value = block_number.to_le_bytes().to_vec();
self.write_async(BLOCK_NUMBERS, block_hash.encode_to_vec(), number_value)
.await
}
pub async fn get_block_number(
&self,
block_hash: BlockHash,
) -> Result<Option<BlockNumber>, StoreError> {
if let Some(n) = self.buffer()?.get_number(&block_hash) {
return Ok(Some(n));
}
self.read_async(BLOCK_NUMBERS, block_hash.encode_to_vec())
.await?
.map(decode_block_number)
.transpose()
}
pub async fn add_transaction_location(
&self,
transaction_hash: H256,
block_number: BlockNumber,
block_hash: BlockHash,
index: Index,
) -> Result<(), StoreError> {
self.add_transaction_locations(vec![(transaction_hash, block_number, block_hash, index)])
.await
}
pub async fn add_transaction_locations(
&self,
locations: Vec<(H256, BlockNumber, BlockHash, Index)>,
) -> Result<(), StoreError> {
let db = self.backend.clone();
tokio::task::spawn_blocking(move || {
let mut tx = db.begin_write()?;
for (tx_hash, block_number, block_hash, index) in locations {
tx.merge(
TRANSACTION_LOCATIONS,
tx_hash.as_bytes(),
&encode_tx_location_operand(block_number, block_hash, index),
)?;
}
tx.commit()
})
.await
.map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
}
pub async fn get_transaction_location(
&self,
transaction_hash: H256,
) -> Result<Option<(BlockNumber, BlockHash, Index)>, StoreError> {
let buffered = self.buffer()?.get_tx_locations(&transaction_hash);
let db = self.backend.clone();
tokio::task::spawn_blocking(move || {
let tx = db.begin_read()?;
let mut locations = buffered;
if let Some(bytes) = tx.get(TRANSACTION_LOCATIONS, transaction_hash.as_bytes())? {
locations.extend(<Vec<(BlockNumber, BlockHash, Index)>>::decode(&bytes)?);
}
for (block_number, block_hash, index) in locations {
let canonical_hash = tx
.get(
CANONICAL_BLOCK_HASHES,
block_number.to_le_bytes().as_slice(),
)?
.map(|bytes| H256::decode(bytes.as_slice()))
.transpose()?;
if canonical_hash == Some(block_hash) {
return Ok(Some((block_number, block_hash, index)));
}
}
Ok(None)
})
.await
.map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
}
pub async fn add_receipt(
&self,
block_hash: BlockHash,
index: Index,
receipt: Receipt,
) -> Result<(), StoreError> {
let key = receipt_key(&block_hash, index);
let value = receipt.encode_storage();
self.write_async(RECEIPTS_V2, key, value).await
}
pub async fn add_receipts(
&self,
block_hash: BlockHash,
receipts: Vec<Receipt>,
) -> Result<(), StoreError> {
let batch_items: Vec<_> = receipts
.into_iter()
.enumerate()
.map(|(index, receipt)| {
let key = receipt_key(&block_hash, index as u64);
let value = receipt.encode_storage();
(key, value)
})
.collect();
self.write_batch_async(RECEIPTS_V2, batch_items).await
}
pub async fn get_receipt(
&self,
block_number: BlockNumber,
index: Index,
) -> Result<Option<Receipt>, StoreError> {
let Some(block_hash) = self.get_canonical_block_hash(block_number).await? else {
return Ok(None);
};
self.get_receipt_by_block_hash(block_hash, index).await
}
async fn get_receipt_by_block_hash(
&self,
block_hash: BlockHash,
index: Index,
) -> Result<Option<Receipt>, StoreError> {
if let Some(r) = self.buffer()?.get_receipt(&block_hash, index) {
return Ok(Some(r));
}
let key = receipt_key(&block_hash, index);
self.read_async(RECEIPTS_V2, key)
.await?
.map(|bytes| Receipt::decode_storage(bytes.as_slice()))
.transpose()
.map_err(StoreError::from)
}
pub fn code_cache_budget_bytes(&self) -> u64 {
self.account_code_cache
.lock()
.map(|cache| cache.max_size)
.unwrap_or(0)
}
pub fn get_account_code(&self, code_hash: H256) -> Result<Option<Code>, StoreError> {
if let Some(code) = self.buffer()?.get_code(&code_hash) {
return Ok(Some(code));
}
if let Some(code) = self
.account_code_cache
.lock()
.map_err(|_| StoreError::LockError)?
.get(&code_hash)?
{
return Ok(Some(code));
}
let Some(bytes) = self
.backend
.begin_read()?
.get(ACCOUNT_CODES, code_hash.as_bytes())?
else {
return Ok(None);
};
let (bytecode_slice, jumpdests) = decode_bytes(&bytes)?;
let (jumpdests, was_legacy) = decode_jumpdests(bytecode_slice, jumpdests)?;
let code = Code::from_parts_unchecked(code_hash, bytecode_slice, jumpdests);
if was_legacy && let Ok(handle) = tokio::runtime::Handle::try_current() {
let buf = encode_code(&code);
let hash_key = code_hash.0.to_vec();
let backend = self.backend.clone();
handle.spawn(async move {
if let Err(e) = async {
let mut tx = backend.begin_write()?;
tx.put(ACCOUNT_CODES, &hash_key, &buf)?;
tx.commit()
}
.await
{
tracing::warn!("Failed to rewrite legacy jumpdests during backfill: {e}");
}
});
}
self.account_code_cache
.lock()
.map_err(|_| StoreError::LockError)?
.insert(&code)?;
Ok(Some(code))
}
pub fn get_account_codes_batch(
&self,
code_hashes: &[H256],
) -> Result<Vec<Option<Code>>, StoreError> {
let mut out: Vec<Option<Code>> = vec![None; code_hashes.len()];
let mut pending: HashMap<H256, Vec<usize>> = HashMap::new();
{
let buffer = self.buffer()?;
let cache = self
.account_code_cache
.lock()
.map_err(|_| StoreError::LockError)?;
for (i, hash) in code_hashes.iter().enumerate() {
if let Some(code) = buffer.get_code(hash) {
out[i] = Some(code);
} else if let Some(code) = cache.get(hash)? {
out[i] = Some(code);
} else {
pending.entry(*hash).or_default().push(i);
}
}
}
if pending.is_empty() {
return Ok(out);
}
let mut missing: Vec<H256> = pending.keys().copied().collect();
missing.sort_unstable();
const KEYS_PER_SHARD: usize = 256;
const MIN_SHARD_CAP: usize = 64;
let parallelism = std::thread::available_parallelism().map_or(8, |p| p.get());
let max_shards = parallelism.saturating_mul(2).max(MIN_SHARD_CAP);
let shards = missing.len().div_ceil(KEYS_PER_SHARD).min(max_shards);
let read_view = self.backend.begin_read()?;
let decode = |hash: &H256, value: Option<Vec<u8>>| -> Result<Option<Code>, StoreError> {
let Some(bytes) = value else { return Ok(None) };
let (bytecode_slice, jumpdests) = decode_bytes(&bytes)?;
let (jumpdests, _was_legacy) = decode_jumpdests(bytecode_slice, jumpdests)?;
Ok(Some(Code::from_parts_unchecked(
*hash,
bytecode_slice,
jumpdests,
)))
};
let decoded: Vec<Result<Option<Code>, StoreError>> = if shards > parallelism {
let chunk = missing.len().div_ceil(shards);
let rv = read_view.as_ref();
let read_shard = |hashes: &[H256]| -> Vec<Result<Option<Code>, StoreError>> {
let keys: Vec<&[u8]> = hashes.iter().map(|h| h.as_bytes()).collect();
rv.multi_get(ACCOUNT_CODES, &keys)
.into_iter()
.zip(hashes.iter())
.map(|(value, hash)| decode(hash, value?))
.collect()
};
std::thread::scope(|scope| {
let handles: Vec<_> = missing
.chunks(chunk)
.map(|ck| (ck, scope.spawn(move || read_shard(ck))))
.collect();
handles
.into_iter()
.flat_map(|(shard, handle)| {
handle.join().unwrap_or_else(|_| {
shard
.iter()
.map(|_| {
Err(StoreError::Custom(
"account code shard panicked".to_string(),
))
})
.collect()
})
})
.collect()
})
} else {
let rv = read_view.as_ref();
missing
.par_iter()
.map(|hash| decode(hash, rv.get(ACCOUNT_CODES, hash.as_bytes())?))
.collect()
};
let mut fetched: Vec<Code> = Vec::new();
for (hash, code) in missing.iter().zip(decoded.into_iter()) {
let Some(code) = code? else { continue };
for &i in pending.get(hash).into_iter().flatten() {
out[i] = Some(code.clone());
}
fetched.push(code);
}
if !fetched.is_empty() {
let mut cache = self
.account_code_cache
.lock()
.map_err(|_| StoreError::LockError)?;
for code in &fetched {
cache.insert(code)?;
}
}
Ok(out)
}
pub fn code_exists(&self, code_hash: H256) -> Result<bool, StoreError> {
if self.buffer()?.get_code(&code_hash).is_some() {
return Ok(true);
}
if self
.account_code_cache
.lock()
.map_err(|_| StoreError::LockError)?
.contains(&code_hash)
{
return Ok(true);
}
Ok(self
.backend
.begin_read()?
.get(ACCOUNT_CODES, code_hash.as_bytes())?
.is_some())
}
pub fn get_code_metadata(&self, code_hash: H256) -> Result<Option<CodeMetadata>, StoreError> {
use ethrex_common::constants::EMPTY_KECCAK_HASH;
if code_hash == *EMPTY_KECCAK_HASH {
return Ok(Some(CodeMetadata { length: 0 }));
}
if let Some(metadata) = self
.code_metadata_cache
.lock()
.map_err(|_| StoreError::LockError)?
.get(&code_hash)
.copied()
{
return Ok(Some(metadata));
}
let metadata = if let Some(bytes) = self
.backend
.begin_read()?
.get(ACCOUNT_CODE_METADATA, code_hash.as_bytes())?
{
let length =
u64::from_be_bytes(bytes.try_into().map_err(|_| {
StoreError::Custom("Invalid metadata length encoding".to_string())
})?);
CodeMetadata { length }
} else {
let Some(code) = self.get_account_code(code_hash)? else {
return Ok(None);
};
let metadata = CodeMetadata {
length: code.len() as u64,
};
if let Ok(handle) = tokio::runtime::Handle::try_current() {
let metadata_buf = metadata.length.to_be_bytes().to_vec();
let hash_key = code_hash.0.to_vec();
let backend = self.backend.clone();
handle.spawn(async move {
if let Err(e) = async {
let mut tx = backend.begin_write()?;
tx.put(ACCOUNT_CODE_METADATA, &hash_key, &metadata_buf)?;
tx.commit()
}
.await
{
tracing::warn!("Failed to write code metadata during backfill: {}", e);
}
});
}
metadata
};
self.code_metadata_cache
.lock()
.map_err(|_| StoreError::LockError)?
.put(code_hash, metadata);
Ok(Some(metadata))
}
pub async fn add_account_code(&self, code: Code) -> Result<(), StoreError> {
let hash_key = code.hash.0.to_vec();
let buf = encode_code(&code);
let metadata_buf = (code.len() as u64).to_be_bytes();
let backend = self.backend.clone();
tokio::task::spawn_blocking(move || {
let mut tx = backend.begin_write()?;
tx.put(ACCOUNT_CODES, &hash_key, &buf)?;
tx.put(ACCOUNT_CODE_METADATA, &hash_key, &metadata_buf)?;
tx.commit()
})
.await
.map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
}
pub async fn clear_snap_state(&self) -> Result<(), StoreError> {
let db = self.backend.clone();
tokio::task::spawn_blocking(move || db.clear_table(SNAP_STATE))
.await
.map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
}
pub async fn get_transaction_by_hash(
&self,
transaction_hash: H256,
) -> Result<Option<Transaction>, StoreError> {
let (_block_number, block_hash, index) =
match self.get_transaction_location(transaction_hash).await? {
Some(location) => location,
None => return Ok(None),
};
self.get_transaction_by_location(block_hash, index).await
}
pub async fn get_transaction_by_location(
&self,
block_hash: H256,
index: u64,
) -> Result<Option<Transaction>, StoreError> {
let block_body = match self.get_block_body_by_hash(block_hash).await? {
Some(body) => body,
None => return Ok(None),
};
let index: usize = index.try_into()?;
Ok(block_body.transactions.get(index).cloned())
}
pub async fn get_block_by_hash(
&self,
block_hash: BlockHash,
) -> Result<Option<Block>, StoreError> {
let header = match self.get_block_header_by_hash(block_hash)? {
Some(header) => header,
None => return Ok(None),
};
let body = match self.get_block_body_by_hash(block_hash).await? {
Some(body) => body,
None => return Ok(None),
};
Ok(Some(Block::new(header, body)))
}
pub async fn get_block_by_number(
&self,
block_number: BlockNumber,
) -> Result<Option<Block>, StoreError> {
let Some(block_hash) = self.get_canonical_block_hash(block_number).await? else {
return Ok(None);
};
self.get_block_by_hash(block_hash).await
}
pub async fn get_canonical_block_hash(
&self,
block_number: BlockNumber,
) -> Result<Option<BlockHash>, StoreError> {
let last = self.latest_block_header.get();
if last.number == block_number {
return Ok(Some(last.hash()));
}
let backend = self.backend.clone();
tokio::task::spawn_blocking(move || {
backend
.begin_read()?
.get(
CANONICAL_BLOCK_HASHES,
block_number.to_le_bytes().as_slice(),
)?
.map(|bytes| H256::decode(bytes.as_slice()))
.transpose()
.map_err(StoreError::from)
})
.await
.map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
}
pub async fn set_chain_config(&mut self, chain_config: &ChainConfig) -> Result<(), StoreError> {
self.chain_config = *chain_config;
let key = chain_data_key(ChainDataIndex::ChainConfig);
let value = serde_json::to_string(chain_config)
.map_err(|_| StoreError::Custom("Failed to serialize chain config".to_string()))?
.into_bytes();
self.write_async(CHAIN_DATA, key, value).await
}
pub async fn update_earliest_block_number(
&self,
block_number: BlockNumber,
) -> Result<(), StoreError> {
let key = chain_data_key(ChainDataIndex::EarliestBlockNumber);
let value = block_number.to_le_bytes().to_vec();
self.write_async(CHAIN_DATA, key, value).await
}
async fn read_chain_data_block_number(
&self,
index: ChainDataIndex,
) -> Result<Option<BlockNumber>, StoreError> {
self.read_async(CHAIN_DATA, chain_data_key(index))
.await?
.map(decode_block_number)
.transpose()
}
pub async fn get_earliest_block_number(&self) -> Result<BlockNumber, StoreError> {
self.read_chain_data_block_number(ChainDataIndex::EarliestBlockNumber)
.await?
.ok_or(StoreError::MissingEarliestBlockNumber)
}
pub async fn get_finalized_block_number(&self) -> Result<Option<BlockNumber>, StoreError> {
self.read_chain_data_block_number(ChainDataIndex::FinalizedBlockNumber)
.await
}
pub async fn get_safe_block_number(&self) -> Result<Option<BlockNumber>, StoreError> {
self.read_chain_data_block_number(ChainDataIndex::SafeBlockNumber)
.await
}
pub fn get_latest_block_number(&self) -> Result<BlockNumber, StoreError> {
Ok(self.latest_block_header.get().number)
}
pub async fn update_pending_block_number(
&self,
block_number: BlockNumber,
) -> Result<(), StoreError> {
let key = chain_data_key(ChainDataIndex::PendingBlockNumber);
let value = block_number.to_le_bytes().to_vec();
self.write_async(CHAIN_DATA, key, value).await
}
pub async fn get_pending_block_number(&self) -> Result<Option<BlockNumber>, StoreError> {
self.read_chain_data_block_number(ChainDataIndex::PendingBlockNumber)
.await
}
async fn forkchoice_update_inner(
&self,
new_canonical_blocks: Vec<(BlockNumber, BlockHash)>,
head_number: BlockNumber,
head_hash: BlockHash,
safe: Option<BlockNumber>,
finalized: Option<BlockNumber>,
) -> Result<(), StoreError> {
let latest = self.load_latest_block_number().await?.unwrap_or(0);
let db = self.backend.clone();
let journal_pruning_paused = self.journal_pruning_paused.clone();
tokio::task::spawn_blocking(move || {
let mut txn = db.begin_write()?;
for (block_number, block_hash) in new_canonical_blocks {
let head_key = block_number.to_le_bytes();
let head_value = block_hash.encode_to_vec();
txn.put(CANONICAL_BLOCK_HASHES, &head_key, &head_value)?;
}
for number in (head_number + 1)..=(latest) {
txn.delete(CANONICAL_BLOCK_HASHES, number.to_le_bytes().as_slice())?;
}
let head_key = head_number.to_le_bytes();
let head_value = head_hash.encode_to_vec();
txn.put(CANONICAL_BLOCK_HASHES, &head_key, &head_value)?;
let latest_key = chain_data_key(ChainDataIndex::LatestBlockNumber);
txn.put(CHAIN_DATA, &latest_key, &head_number.to_le_bytes())?;
if let Some(safe) = safe {
let safe_key = chain_data_key(ChainDataIndex::SafeBlockNumber);
txn.put(CHAIN_DATA, &safe_key, &safe.to_le_bytes())?;
}
if let Some(finalized) = finalized {
let finalized_key = chain_data_key(ChainDataIndex::FinalizedBlockNumber);
let prev_finalized = match db.begin_read()?.get(CHAIN_DATA, &finalized_key)? {
Some(bytes) => {
let arr: [u8; 8] = bytes.as_slice().try_into().map_err(|_| {
StoreError::Custom(format!(
"FinalizedBlockNumber has unexpected length {} (want 8)",
bytes.len()
))
})?;
BlockNumber::from_le_bytes(arr)
}
None => 0,
};
txn.put(CHAIN_DATA, &finalized_key, &finalized.to_le_bytes())?;
if finalized > prev_finalized
&& !journal_pruning_paused.load(std::sync::atomic::Ordering::Acquire)
{
let start = 0u64.to_be_bytes();
let end = finalized.saturating_add(1).to_be_bytes();
txn.delete_range(STATE_HISTORY, &start, &end)?;
}
}
txn.commit()
})
.await
.map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
}
pub async fn get_receipts_for_block(
&self,
block_hash: &BlockHash,
) -> Result<Vec<Receipt>, StoreError> {
self.get_receipts_for_block_from_index(block_hash, 0, None)
.await
}
pub async fn get_receipts_for_block_from_index(
&self,
block_hash: &BlockHash,
start_index: u64,
max_count: Option<usize>,
) -> Result<Vec<Receipt>, StoreError> {
if let Some(all) = self.buffer()?.get_receipts(block_hash) {
let start = start_index as usize;
let slice = all.into_iter().skip(start);
return Ok(match max_count {
Some(max) => slice.take(max).collect(),
None => slice.collect(),
});
}
let backend = self.backend.clone();
let block_hash = *block_hash;
tokio::task::spawn_blocking(move || {
let txn = backend.begin_read()?;
let prefix = block_hash.as_bytes().to_vec();
let mut seek_key = prefix.clone();
seek_key.extend_from_slice(&start_index.to_be_bytes());
let iter = txn.prefix_iterator(RECEIPTS_V2, &seek_key)?;
let mut receipts = Vec::new();
for result in iter {
let (k, v) = result?;
if !k.starts_with(&prefix) {
break;
}
if k.len() != 40 {
continue;
}
receipts.push(Receipt::decode_storage(v.as_ref())?);
if let Some(max) = max_count
&& receipts.len() >= max
{
break;
}
}
Ok(receipts)
})
.await
.map_err(|e| StoreError::Custom(format!("Task panicked: {e}")))?
}
pub async fn set_header_download_checkpoint(
&self,
block_hash: BlockHash,
) -> Result<(), StoreError> {
let key = snap_state_key(SnapStateIndex::HeaderDownloadCheckpoint);
let value = block_hash.encode_to_vec();
self.write_async(SNAP_STATE, key, value).await
}
pub async fn get_header_download_checkpoint(&self) -> Result<Option<BlockHash>, StoreError> {
let key = snap_state_key(SnapStateIndex::HeaderDownloadCheckpoint);
self.backend
.begin_read()?
.get(SNAP_STATE, &key)?
.map(|bytes| H256::decode(bytes.as_slice()))
.transpose()
.map_err(StoreError::from)
}
pub async fn set_latest_valid_ancestor(
&self,
bad_block: BlockHash,
latest_valid: BlockHash,
) -> Result<(), StoreError> {
let value = latest_valid.encode_to_vec();
self.write_async(INVALID_CHAINS, bad_block.as_bytes().to_vec(), value)
.await
}
pub async fn get_latest_valid_ancestor(
&self,
block: BlockHash,
) -> Result<Option<BlockHash>, StoreError> {
self.read_async(INVALID_CHAINS, block.as_bytes().to_vec())
.await?
.map(|bytes| H256::decode(bytes.as_slice()))
.transpose()
.map_err(StoreError::from)
}
pub async fn add_bad_block(&self, block: Block) -> Result<(), StoreError> {
let mut bad_blocks = self.get_bad_blocks().await?;
let block_number = block.header.number;
let block_hash = block.hash();
if bad_blocks
.iter()
.any(|b| b.header.number == block_number && b.hash() == block_hash)
{
return Ok(());
}
bad_blocks.push(block);
bad_blocks.sort_by(|a, b| b.header.number.cmp(&a.header.number));
bad_blocks.truncate(MAX_BAD_BLOCKS);
self.write_async(
BAD_BLOCKS,
BAD_BLOCKS_KEY.to_vec(),
bad_blocks.encode_to_vec(),
)
.await
}
pub async fn get_bad_blocks(&self) -> Result<Vec<Block>, StoreError> {
match self.read_async(BAD_BLOCKS, BAD_BLOCKS_KEY.to_vec()).await? {
Some(bytes) => Vec::<Block>::decode(&bytes).map_err(StoreError::from),
None => Ok(Vec::new()),
}
}
pub fn get_block_number_sync(
&self,
block_hash: BlockHash,
) -> Result<Option<BlockNumber>, StoreError> {
if let Some(n) = self.buffer()?.get_number(&block_hash) {
return Ok(Some(n));
}
let txn = self.backend.begin_read()?;
txn.get(BLOCK_NUMBERS, &block_hash.encode_to_vec())?
.map(decode_block_number)
.transpose()
}
pub fn get_canonical_block_hash_sync(
&self,
block_number: BlockNumber,
) -> Result<Option<BlockHash>, StoreError> {
let last = self.latest_block_header.get();
if last.number == block_number {
return Ok(Some(last.hash()));
}
let txn = self.backend.begin_read()?;
txn.get(
CANONICAL_BLOCK_HASHES,
block_number.to_le_bytes().as_slice(),
)?
.map(|bytes| H256::decode(bytes.as_slice()))
.transpose()
.map_err(StoreError::from)
}
pub async fn write_storage_trie_nodes_batch(
&self,
storage_trie_nodes: StorageUpdates,
) -> Result<(), StoreError> {
let mut txn = self.backend.begin_write()?;
tokio::task::spawn_blocking(move || {
for (address_hash, nodes) in storage_trie_nodes {
for (node_path, node_data) in nodes {
let key = apply_prefix(Some(address_hash), node_path);
if node_data.is_empty() {
txn.delete(STORAGE_TRIE_NODES, key.as_ref())?;
} else {
txn.put(STORAGE_TRIE_NODES, key.as_ref(), &node_data)?;
}
}
}
txn.commit()
})
.await
.map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
}
pub async fn write_account_code_batch(
&self,
account_codes: Vec<(H256, Code)>,
) -> Result<(), StoreError> {
let mut code_batch_items = Vec::new();
let mut metadata_batch_items = Vec::new();
for (code_hash, code) in account_codes {
let buf = encode_code(&code);
let metadata_buf = (code.len() as u64).to_be_bytes().to_vec();
code_batch_items.push((code_hash.as_bytes().to_vec(), buf));
metadata_batch_items.push((code_hash.as_bytes().to_vec(), metadata_buf));
}
self.write_batch_async(ACCOUNT_CODES, code_batch_items)
.await?;
self.write_batch_async(ACCOUNT_CODE_METADATA, metadata_batch_items)
.await
}
fn buffer(&self) -> Result<Arc<BlockDataBuffer>, StoreError> {
Ok(self
.block_data_buffer
.read()
.map_err(|_| StoreError::LockError)?
.clone())
}
pub fn write(
&self,
table: &'static str,
key: Vec<u8>,
value: Vec<u8>,
) -> Result<(), StoreError> {
let backend = self.backend.clone();
let mut txn = backend.begin_write()?;
txn.put(table, &key, &value)?;
txn.commit()
}
async fn write_async(
&self,
table: &'static str,
key: Vec<u8>,
value: Vec<u8>,
) -> Result<(), StoreError> {
let backend = self.backend.clone();
tokio::task::spawn_blocking(move || {
let mut txn = backend.begin_write()?;
txn.put(table, &key, &value)?;
txn.commit()
})
.await
.map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
}
pub async fn read_async(
&self,
table: &'static str,
key: Vec<u8>,
) -> Result<Option<Vec<u8>>, StoreError> {
let backend = self.backend.clone();
tokio::task::spawn_blocking(move || {
let txn = backend.begin_read()?;
txn.get(table, &key)
})
.await
.map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
}
pub fn read(&self, table: &'static str, key: Vec<u8>) -> Result<Option<Vec<u8>>, StoreError> {
let backend = self.backend.clone();
let txn = backend.begin_read()?;
txn.get(table, &key)
}
pub async fn write_batch_async(
&self,
table: &'static str,
batch_ops: Vec<(Vec<u8>, Vec<u8>)>,
) -> Result<(), StoreError> {
let backend = self.backend.clone();
tokio::task::spawn_blocking(move || {
let mut txn = backend.begin_write()?;
txn.put_batch(table, batch_ops)?;
txn.commit()
})
.await
.map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))?
}
pub fn write_batch(
&self,
table: &'static str,
batch_ops: Vec<(Vec<u8>, Vec<u8>)>,
) -> Result<(), StoreError> {
let backend = self.backend.clone();
let mut txn = backend.begin_write()?;
txn.put_batch(table, batch_ops)?;
txn.commit()
}
pub async fn add_fullsync_batch(&self, headers: Vec<BlockHeader>) -> Result<(), StoreError> {
self.write_batch_async(
FULLSYNC_HEADERS,
headers
.into_iter()
.map(|header| (header.number.to_le_bytes().to_vec(), header.encode_to_vec()))
.collect(),
)
.await
}
pub async fn read_fullsync_batch(
&self,
start: BlockNumber,
limit: u64,
) -> Result<Vec<Option<BlockHeader>>, StoreError> {
let mut res = vec![];
let read_tx = self.backend.begin_read()?;
for key in start..start + limit {
let header_opt = read_tx
.get(FULLSYNC_HEADERS, &key.to_le_bytes())?
.map(|header| BlockHeader::decode(&header))
.transpose()?;
res.push(header_opt);
}
Ok(res)
}
pub async fn clear_fullsync_headers(&self) -> Result<(), StoreError> {
self.backend.clear_table(FULLSYNC_HEADERS)
}
pub fn delete(&self, table: &'static str, key: Vec<u8>) -> Result<(), StoreError> {
let mut txn = self.backend.begin_write()?;
txn.delete(table, &key)?;
txn.commit()
}
pub fn store_block_updates(&self, update_batch: UpdateBatch) -> Result<(), StoreError> {
self.apply_updates(update_batch)
}
fn batch_state_roots(
&self,
update_batch: &UpdateBatch,
) -> Result<(H256, H256, BlockNumber, H256), StoreError> {
let parent_state_root = self
.get_block_header_by_hash(
update_batch
.blocks
.first()
.ok_or(StoreError::UpdateBatchNoBlocks)?
.header
.parent_hash,
)?
.map(|header| header.state_root)
.unwrap_or_default();
let last_block = update_batch
.blocks
.last()
.ok_or(StoreError::UpdateBatchNoBlocks)?;
let last_state_root = last_block.header.state_root;
let last_block_number = last_block.header.number;
let last_block_hash = last_block.hash();
Ok((
parent_state_root,
last_state_root,
last_block_number,
last_block_hash,
))
}
fn apply_updates(&self, update_batch: UpdateBatch) -> Result<(), StoreError> {
let (parent_state_root, last_state_root, last_block_number, last_block_hash) =
self.batch_state_roots(&update_batch)?;
let UpdateBatch {
account_updates,
storage_updates,
blocks,
receipts,
code_updates,
commit_depth,
wait_for_flush,
} = update_batch;
self.pending_trie_roots.register(last_state_root)?;
let blocks_with_receipts: Vec<(Block, Vec<Receipt>)> = if blocks.len() == 1 {
let block = blocks.into_iter().next().expect("len == 1");
let hash = block.hash();
let r = receipts
.into_iter()
.find(|(h, _)| *h == hash)
.map(|(_, r)| r)
.unwrap_or_default();
vec![(block, r)]
} else {
let mut receipts_by_hash: std::collections::HashMap<BlockHash, Vec<Receipt>> =
receipts.into_iter().collect();
blocks
.into_iter()
.map(|b| {
let r = receipts_by_hash.remove(&b.hash()).unwrap_or_default();
(b, r)
})
.collect()
};
let (ack_tx, ack_rx) = sync_channel(1);
self.persist_tx
.send(PersistMessage::Block(Box::new(BlockPersist {
blocks: blocks_with_receipts,
codes: code_updates,
parent_state_root,
child_state_root: last_state_root,
account_updates,
storage_updates,
commit_depth,
wait_for_flush,
block_number: last_block_number,
block_hash: last_block_hash,
ack: ack_tx,
})))
.map_err(|e| StoreError::Custom(format!("failed to send block persist: {e}")))?;
ack_rx
.recv()
.map_err(|e| StoreError::Custom(format!("block persist ack failed: {e}")))??;
Ok(())
}
pub fn new(path: impl AsRef<Path>, engine_type: EngineType) -> Result<Self, StoreError> {
Self::new_with_config(path, engine_type, StoreConfig::default())
}
pub fn new_with_config(
path: impl AsRef<Path>,
engine_type: EngineType,
config: StoreConfig,
) -> Result<Self, StoreError> {
let code_cache_size = config.code_cache_size.unwrap_or(CODE_CACHE_MAX_SIZE);
let store = Self::open_with_config(path, engine_type, config)?;
if let Ok(mut cache) = store.account_code_cache.lock() {
cache.max_size = code_cache_size;
}
info!(cache_bytes = code_cache_size, "sized bytecode cache");
Ok(store)
}
fn open_with_config(
path: impl AsRef<Path>,
engine_type: EngineType,
#[cfg_attr(not(feature = "rocksdb"), allow(unused_variables))] config: StoreConfig,
) -> Result<Self, StoreError> {
let db_path = path.as_ref().to_path_buf();
if engine_type != EngineType::InMemory {
let version = read_store_schema_version(&db_path)?;
match version {
None if db_path.exists() && dir_contains_legacy_db(&db_path)? => {
return Err(StoreError::NotFoundDBVersion);
}
None => {
init_metadata_file(&db_path)?;
}
Some(v) if v < 1 => {
return Err(StoreError::IncompatibleDBVersion {
found: v,
expected: STORE_SCHEMA_VERSION,
});
}
Some(v) if v > STORE_SCHEMA_VERSION => {
return Err(StoreError::IncompatibleDBVersion {
found: v,
expected: STORE_SCHEMA_VERSION,
});
}
#[cfg(feature = "rocksdb")]
Some(v) if v < STORE_SCHEMA_VERSION => {
let rocksdb = Arc::new(RocksDBBackend::open(
&path,
config.rocksdb_block_cache_size,
)?);
crate::migrations::run_pending_migrations(rocksdb.as_ref(), &db_path, v)?;
rocksdb.drop_obsolete_cfs(&path);
let backend: Arc<dyn crate::api::StorageBackend> = rocksdb;
return Self::from_backend(
backend,
db_path,
DB_COMMIT_THRESHOLD,
config.persist_channel_capacity,
);
}
Some(_) => {
}
}
}
match engine_type {
#[cfg(feature = "rocksdb")]
EngineType::RocksDB => {
let rocksdb = RocksDBBackend::open(&path, config.rocksdb_block_cache_size)?;
rocksdb.drop_obsolete_cfs(&path);
let backend: Arc<dyn StorageBackend> = Arc::new(rocksdb);
Self::from_backend(
backend,
db_path,
DB_COMMIT_THRESHOLD,
config.persist_channel_capacity,
)
}
EngineType::InMemory => {
let backend = Arc::new(InMemoryBackend::open()?);
Self::from_backend(
backend,
db_path,
IN_MEMORY_COMMIT_THRESHOLD,
config.persist_channel_capacity,
)
}
}
}
fn from_backend(
backend: Arc<dyn StorageBackend>,
db_path: PathBuf,
commit_threshold: usize,
persist_channel_capacity: usize,
) -> Result<Self, StoreError> {
debug!("Initializing Store with {commit_threshold} in-memory diff-layers");
let (fkv_tx, fkv_rx) = std::sync::mpsc::sync_channel(0);
let persist_cap = persist_channel_capacity.max(1); let (persist_tx, persist_rx) = std::sync::mpsc::sync_channel(persist_cap);
let (last_written, initial_flushed_upto) = {
let tx = backend.begin_read()?;
let last_written = tx
.get(MISC_VALUES, "last_written".as_bytes())?
.unwrap_or_else(|| vec![0u8; 64]);
let last_written = if last_written == [0xff] {
vec![0xff; 64]
} else {
last_written
};
let initial_flushed_upto = match tx.get(MISC_VALUES, FLUSHED_UPTO_KEY)? {
Some(bytes) => decode_flushed_upto(&bytes)?,
None => 0,
};
(last_written, initial_flushed_upto)
};
let mut initial_buffer = BlockDataBuffer::new();
initial_buffer.set_flushed_upto(initial_flushed_upto);
let mut background_threads = Vec::new();
let safe_commit_root = Arc::new(RwLock::new(H256::zero()));
let mut store = Self {
db_path,
backend,
chain_config: Default::default(),
latest_block_header: Default::default(),
trie_cache: Arc::new(RwLock::new(Arc::new(TrieLayerCache::new_with_safe_commit(
commit_threshold,
safe_commit_root.clone(),
)))),
flatkeyvalue_control_tx: fkv_tx,
block_data_buffer: Arc::new(RwLock::new(Arc::new(initial_buffer))),
persist_tx,
pending_trie_roots: Arc::new(PendingTrieRoots::default()),
last_computed_flatkeyvalue: Arc::new(RwLock::new(last_written)),
account_code_cache: Arc::new(Mutex::new(CodeCache::default())),
code_metadata_cache: Arc::new(Mutex::new(LruCache::with_hasher(
NonZeroUsize::new(CODE_METADATA_CACHE_MAX_ENTRIES).unwrap_or(NonZeroUsize::MIN),
FxBuildHasher,
))),
fcu_lock: Arc::new(tokio::sync::Mutex::new(())),
safe_commit_root,
journal_pruning_paused: Arc::new(std::sync::atomic::AtomicBool::new(false)),
background_threads: Default::default(),
};
let backend_clone = store.backend.clone();
let last_computed_fkv = store.last_computed_flatkeyvalue.clone();
background_threads.push(std::thread::spawn(move || {
let rx = fkv_rx;
loop {
match rx.recv() {
Ok(FKVGeneratorControlMessage::Continue) => break,
Ok(FKVGeneratorControlMessage::Stop) => {}
Err(std::sync::mpsc::RecvError) => {
debug!("Closing FlatKeyValue generator.");
return;
}
}
}
let _ = flatkeyvalue_generator(&backend_clone, &last_computed_fkv, &rx)
.inspect_err(|err| error!("Error while generating FlatKeyValue: {err}"));
}));
let persist_backend = store.backend.clone();
let persist_buffer = store.block_data_buffer.clone();
let persist_trie_cache = store.trie_cache.clone();
let persist_pending_roots = store.pending_trie_roots.clone();
let persist_fkv_ctl = store.flatkeyvalue_control_tx.clone();
background_threads.push(std::thread::spawn(move || {
let rx = persist_rx;
let mut last_flush_result: Result<(), StoreError> = Ok(());
loop {
match rx.recv() {
Ok(PersistMessage::Block(bp)) => {
let bp = *bp;
let staged = mutate_block_buffer(&persist_buffer, move |b| {
let mut codes = Some(bp.codes);
for (block, receipts) in bp.blocks {
b.insert(block, receipts, codes.take().unwrap_or_default());
}
});
if let Err(e) = staged {
persist_pending_roots.clear(bp.child_state_root);
let _ = bp.ack.send(Err(e));
continue;
}
if !bp.wait_for_flush {
let _ = bp
.ack
.send(std::mem::replace(&mut last_flush_result, Ok(())));
}
if let Err(err) = apply_trie_phase1(
&persist_trie_cache,
&persist_pending_roots,
bp.parent_state_root,
bp.child_state_root,
bp.block_number,
bp.block_hash,
bp.account_updates,
bp.storage_updates,
) {
error!("persist worker trie phase-1 failed: {err}");
if bp.wait_for_flush {
let _ = bp.ack.send(Err(err));
} else {
last_flush_result = Err(err);
}
continue;
}
let flushed = flush_block_data(persist_backend.as_ref(), &persist_buffer)
.inspect_err(|err| error!("flush_block_data failed: {err}"))
.and_then(|_| {
commit_trie_if_due(
persist_backend.as_ref(),
&persist_trie_cache,
&persist_fkv_ctl,
bp.parent_state_root,
bp.commit_depth,
bp.wait_for_flush,
)
});
if bp.wait_for_flush {
let prior = std::mem::replace(&mut last_flush_result, Ok(()));
let _ = bp.ack.send(prior.and(flushed));
} else {
last_flush_result = flushed;
}
}
Ok(PersistMessage::Commit(root)) => match persist_trie_cache.read() {
Ok(guard) => {
let trie = guard.clone();
drop(guard);
let _ = commit_to_disk(
persist_backend.as_ref(),
&persist_fkv_ctl,
&persist_trie_cache,
&trie,
root,
false,
)
.inspect_err(|err| error!("commit_to_disk failed: {err}"));
}
Err(_) => error!("trie cache lock poisoned during commit"),
},
Ok(PersistMessage::Ping(ack)) => {
let _ = ack.send(std::mem::replace(&mut last_flush_result, Ok(())));
}
Ok(PersistMessage::Shutdown { ack }) => {
let result = flush_block_data(persist_backend.as_ref(), &persist_buffer);
let prior = std::mem::replace(&mut last_flush_result, Ok(()));
let _ = ack.send(prior.and(result));
return;
}
Err(_) => return,
}
}
}));
store.background_threads = Arc::new(ThreadList {
list: background_threads,
});
Ok(store)
}
pub async fn new_from_genesis(
store_path: &Path,
engine_type: EngineType,
genesis_path: &str,
) -> Result<Self, StoreError> {
Self::new_from_genesis_with_config(
store_path,
engine_type,
genesis_path,
StoreConfig::default(),
)
.await
}
pub async fn new_from_genesis_with_config(
store_path: &Path,
engine_type: EngineType,
genesis_path: &str,
config: StoreConfig,
) -> Result<Self, StoreError> {
let file = std::fs::File::open(genesis_path)
.map_err(|error| StoreError::Custom(format!("Failed to open genesis file: {error}")))?;
let reader = std::io::BufReader::new(file);
let genesis: Genesis = serde_json::from_reader(reader)
.map_err(|e| StoreError::Custom(format!("Failed to deserialize genesis file: {e}")))?;
let mut store = Self::new_with_config(store_path, engine_type, config)?;
store.add_initial_state(genesis).await?;
Ok(store)
}
pub async fn get_account_info(
&self,
block_number: BlockNumber,
address: Address,
) -> Result<Option<AccountInfo>, StoreError> {
match self.get_canonical_block_hash(block_number).await? {
Some(block_hash) => self.get_account_info_by_hash(block_hash, address),
None => Ok(None),
}
}
pub fn get_account_info_by_hash(
&self,
block_hash: BlockHash,
address: Address,
) -> Result<Option<AccountInfo>, StoreError> {
let Some(state_trie) = self.state_trie(block_hash)? else {
return Ok(None);
};
let hashed_address = hash_address_fixed(&address);
let Some(encoded_state) = state_trie.get(hashed_address.as_bytes())? else {
return Ok(None);
};
let account_state = AccountState::decode(&encoded_state)?;
Ok(Some(AccountInfo {
code_hash: account_state.code_hash,
balance: account_state.balance,
nonce: account_state.nonce,
}))
}
pub fn get_account_state_by_acc_hash(
&self,
block_hash: BlockHash,
account_hash: H256,
) -> Result<Option<AccountState>, StoreError> {
let Some(state_trie) = self.state_trie(block_hash)? else {
return Ok(None);
};
let Some(encoded_state) = state_trie.get(account_hash.as_bytes())? else {
return Ok(None);
};
let account_state = AccountState::decode(&encoded_state)?;
Ok(Some(account_state))
}
pub async fn get_fork_id(&self) -> Result<ForkId, StoreError> {
let chain_config = self.get_chain_config();
let genesis_header = self
.load_block_header(0)?
.ok_or(StoreError::MissingEarliestBlockNumber)?;
let block_header = self.latest_block_header.get();
Ok(ForkId::new(
chain_config,
genesis_header,
block_header.timestamp,
block_header.number,
))
}
pub async fn get_code_by_account_address(
&self,
block_number: BlockNumber,
address: Address,
) -> Result<Option<Code>, StoreError> {
let Some(block_hash) = self.get_canonical_block_hash(block_number).await? else {
return Ok(None);
};
let Some(state_trie) = self.state_trie(block_hash)? else {
return Ok(None);
};
let hashed_address = hash_address_fixed(&address);
let Some(encoded_state) = state_trie.get(hashed_address.as_bytes())? else {
return Ok(None);
};
let account_state = AccountState::decode(&encoded_state)?;
self.get_account_code(account_state.code_hash)
}
pub async fn get_nonce_by_account_address(
&self,
block_number: BlockNumber,
address: Address,
) -> Result<Option<u64>, StoreError> {
let Some(block_hash) = self.get_canonical_block_hash(block_number).await? else {
return Ok(None);
};
let Some(state_trie) = self.state_trie(block_hash)? else {
return Ok(None);
};
let hashed_address = hash_address_fixed(&address);
let Some(encoded_state) = state_trie.get(hashed_address.as_bytes())? else {
return Ok(None);
};
let account_state = AccountState::decode(&encoded_state)?;
Ok(Some(account_state.nonce))
}
pub fn apply_account_updates_batch(
&self,
block_hash: BlockHash,
account_updates: &[AccountUpdate],
) -> Result<Option<AccountUpdatesList>, StoreError> {
let Some(mut state_trie) = self.state_trie(block_hash)? else {
return Ok(None);
};
Ok(Some(self.apply_account_updates_from_trie_batch(
&mut state_trie,
account_updates,
)?))
}
pub fn apply_account_updates_from_trie_batch<'a>(
&self,
state_trie: &mut Trie,
account_updates: impl IntoIterator<Item = &'a AccountUpdate>,
) -> Result<AccountUpdatesList, StoreError> {
let mut ret_storage_updates = Vec::new();
let mut code_updates = Vec::new();
let state_root = state_trie.hash_no_commit(&NativeCrypto);
for update in account_updates {
let hashed_address = hash_address_fixed(&update.address);
if update.removed {
state_trie.remove(hashed_address.as_bytes())?;
continue;
}
let mut account_state = match state_trie.get(hashed_address.as_bytes())? {
Some(encoded_state) => AccountState::decode(&encoded_state)?,
None => AccountState::default(),
};
if update.removed_storage {
account_state.storage_root = EMPTY_TRIE_HASH;
}
if let Some(info) = &update.info {
account_state.nonce = info.nonce;
account_state.balance = info.balance;
account_state.code_hash = info.code_hash;
if let Some(code) = &update.code {
code_updates.push((info.code_hash, code.clone()));
}
}
if !update.added_storage.is_empty() {
let mut storage_trie =
self.open_storage_trie(hashed_address, state_root, account_state.storage_root)?;
for (storage_key, storage_value) in &update.added_storage {
let hashed_key = hash_key(storage_key);
if storage_value.is_zero() {
storage_trie.remove(&hashed_key)?;
} else {
storage_trie.insert(hashed_key, storage_value.encode_to_vec())?;
}
}
let (storage_hash, storage_updates) =
storage_trie.collect_changes_since_last_hash(&NativeCrypto);
account_state.storage_root = storage_hash;
ret_storage_updates.push((hashed_address, storage_updates));
}
state_trie.insert(
hashed_address.as_bytes().to_vec(),
account_state.encode_to_vec(),
)?;
}
let (state_trie_hash, state_updates) =
state_trie.collect_changes_since_last_hash(&NativeCrypto);
Ok(AccountUpdatesList {
state_trie_hash,
state_updates,
storage_updates: ret_storage_updates,
code_updates,
})
}
pub fn apply_account_updates_from_trie_with_witness(
&self,
mut state_trie: Trie,
account_updates: &[AccountUpdate],
mut storage_tries: StorageTries,
) -> Result<(StorageTries, AccountUpdatesList), StoreError> {
let mut ret_storage_updates = Vec::new();
let mut code_updates = Vec::new();
let state_root = state_trie.hash_no_commit(&NativeCrypto);
for update in account_updates.iter() {
let hashed_address = hash_address(&update.address);
if update.removed {
state_trie.remove(&hashed_address)?;
continue;
}
let mut account_state = match state_trie.get(&hashed_address)? {
Some(encoded_state) => AccountState::decode(&encoded_state)?,
None => AccountState::default(),
};
if update.removed_storage {
account_state.storage_root = EMPTY_TRIE_HASH;
}
if let Some(info) = &update.info {
account_state.nonce = info.nonce;
account_state.balance = info.balance;
account_state.code_hash = info.code_hash;
if let Some(code) = &update.code {
code_updates.push((info.code_hash, code.clone()));
}
}
if !update.added_storage.is_empty() {
let (_witness, storage_trie) = match storage_tries.entry(update.address) {
Entry::Occupied(value) => value.into_mut(),
Entry::Vacant(vacant) => {
let trie = self.open_storage_trie(
H256::from_slice(&hashed_address),
state_root,
account_state.storage_root,
)?;
vacant.insert(TrieLogger::open_trie(trie))
}
};
for (storage_key, storage_value) in &update.added_storage {
let hashed_key = hash_key(storage_key);
if storage_value.is_zero() {
storage_trie.remove(&hashed_key)?;
} else {
storage_trie.insert(hashed_key, storage_value.encode_to_vec())?;
}
}
let (storage_hash, storage_updates) =
storage_trie.collect_changes_since_last_hash(&NativeCrypto);
account_state.storage_root = storage_hash;
ret_storage_updates.push((H256::from_slice(&hashed_address), storage_updates));
}
state_trie.insert(hashed_address, account_state.encode_to_vec())?;
}
let (state_trie_hash, state_updates) =
state_trie.collect_changes_since_last_hash(&NativeCrypto);
let account_updates_list = AccountUpdatesList {
state_trie_hash,
state_updates,
storage_updates: ret_storage_updates,
code_updates,
};
Ok((storage_tries, account_updates_list))
}
pub async fn setup_genesis_state_trie(
&self,
genesis_accounts: BTreeMap<Address, GenesisAccount>,
) -> Result<H256, StoreError> {
let mut storage_trie_nodes = vec![];
let mut genesis_state_trie = self.open_direct_state_trie(EMPTY_TRIE_HASH)?;
for (address, account) in genesis_accounts {
let hashed_address = hash_address(&address);
let h256_hashed_address = H256::from_slice(&hashed_address);
let code = Code::from_bytecode(account.code, &NativeCrypto);
let code_hash = code.hash;
self.add_account_code(code).await?;
let mut storage_trie =
self.open_direct_storage_trie(h256_hashed_address, EMPTY_TRIE_HASH)?;
for (storage_key, storage_value) in account.storage {
if !storage_value.is_zero() {
let hashed_key = hash_key(&H256(storage_key.to_big_endian()));
storage_trie.insert(hashed_key, storage_value.encode_to_vec())?;
}
}
let (storage_root, storage_nodes) =
storage_trie.collect_changes_since_last_hash(&NativeCrypto);
storage_trie_nodes.extend(
storage_nodes
.into_iter()
.map(|(path, n)| (apply_prefix(Some(h256_hashed_address), path).into_vec(), n)),
);
let account_state = AccountState {
nonce: account.nonce,
balance: account.balance,
storage_root,
code_hash,
};
genesis_state_trie.insert(hashed_address, account_state.encode_to_vec())?;
}
let (state_root, account_trie_nodes) =
genesis_state_trie.collect_changes_since_last_hash(&NativeCrypto);
let account_trie_nodes = account_trie_nodes
.into_iter()
.map(|(path, n)| (apply_prefix(None, path).into_vec(), n))
.collect::<Vec<_>>();
let mut tx = self.backend.begin_write()?;
tx.put_batch(ACCOUNT_TRIE_NODES, account_trie_nodes)?;
tx.put_batch(STORAGE_TRIE_NODES, storage_trie_nodes)?;
tx.commit()?;
Ok(state_root)
}
fn make_witness_key(block_number: u64, block_hash: &BlockHash) -> Vec<u8> {
let mut composite_key = Vec::with_capacity(8 + 32);
composite_key.extend_from_slice(&block_number.to_be_bytes());
composite_key.extend_from_slice(block_hash.as_bytes());
composite_key
}
pub fn store_witness(
&self,
block_hash: BlockHash,
block_number: u64,
witness: ExecutionWitness,
) -> Result<(), StoreError> {
let rpc_witness = RpcExecutionWitness::try_from(witness)?;
let key = Self::make_witness_key(block_number, &block_hash);
let value = serde_json::to_vec(&rpc_witness)?;
self.write(EXECUTION_WITNESSES, key, value)?;
self.cleanup_old_witnesses(block_number)
}
fn cleanup_old_witnesses(&self, latest_block_number: u64) -> Result<(), StoreError> {
if latest_block_number <= MAX_WITNESSES {
return Ok(());
}
let threshold = latest_block_number - MAX_WITNESSES;
if let Some(oldest_block_number) = self.get_oldest_witness_number()? {
let prefix = oldest_block_number.to_be_bytes();
let mut to_delete = Vec::new();
{
let read_txn = self.backend.begin_read()?;
let iter = read_txn.prefix_iterator(EXECUTION_WITNESSES, &prefix)?;
for item in iter {
let (key, _value) = item?;
let mut block_number_bytes = [0u8; 8];
block_number_bytes.copy_from_slice(&key[0..8]);
let block_number = u64::from_be_bytes(block_number_bytes);
if block_number > threshold {
break;
}
to_delete.push(key.to_vec());
}
}
for key in to_delete {
self.delete(EXECUTION_WITNESSES, key)?;
}
};
self.update_oldest_witness_number(threshold + 1)?;
Ok(())
}
fn update_oldest_witness_number(&self, oldest_block_number: u64) -> Result<(), StoreError> {
self.write(
MISC_VALUES,
b"oldest_witness_block_number".to_vec(),
oldest_block_number.to_le_bytes().to_vec(),
)?;
Ok(())
}
fn get_oldest_witness_number(&self) -> Result<Option<u64>, StoreError> {
let Some(value) = self.read(MISC_VALUES, b"oldest_witness_block_number".to_vec())? else {
return Ok(None);
};
let array: [u8; 8] = value.as_slice().try_into().map_err(|_| {
StoreError::Custom("Invalid oldest witness block number bytes".to_string())
})?;
Ok(Some(u64::from_le_bytes(array)))
}
pub fn get_witness_json_bytes(
&self,
block_number: u64,
block_hash: BlockHash,
) -> Result<Option<Vec<u8>>, StoreError> {
let key = Self::make_witness_key(block_number, &block_hash);
self.read(EXECUTION_WITNESSES, key)
}
pub fn get_witness_by_number_and_hash(
&self,
block_number: u64,
block_hash: BlockHash,
) -> Result<Option<RpcExecutionWitness>, StoreError> {
let key = Self::make_witness_key(block_number, &block_hash);
match self.read(EXECUTION_WITNESSES, key)? {
Some(value) => {
let witness: RpcExecutionWitness = serde_json::from_slice(&value)?;
Ok(Some(witness))
}
None => Ok(None),
}
}
pub fn store_block_access_list(
&self,
block_hash: BlockHash,
bal: &BlockAccessList,
) -> Result<(), StoreError> {
let mut value = vec![];
bal.encode(&mut value);
self.store_block_access_list_encoded(block_hash, value)
}
pub fn store_block_access_list_encoded(
&self,
block_hash: BlockHash,
encoded: Vec<u8>,
) -> Result<(), StoreError> {
self.write(BLOCK_ACCESS_LISTS, block_hash.as_bytes().to_vec(), encoded)
}
pub fn get_block_access_list(
&self,
block_hash: BlockHash,
) -> Result<Option<BlockAccessList>, StoreError> {
let key = block_hash.as_bytes().to_vec();
match self.read(BLOCK_ACCESS_LISTS, key)? {
Some(value) => {
let bal = BlockAccessList::decode(&value)
.map_err(|e| StoreError::Custom(format!("Failed to decode BAL: {e}")))?;
Ok(Some(bal))
}
None => Ok(None),
}
}
pub fn iter_block_access_lists_by_hashes(
&self,
hashes: &[BlockHash],
) -> Result<Vec<Option<BlockAccessList>>, StoreError> {
let mut out = Vec::with_capacity(hashes.len());
for hash in hashes {
out.push(self.get_block_access_list(*hash)?);
}
Ok(out)
}
pub async fn add_initial_state(&mut self, genesis: Genesis) -> Result<(), StoreError> {
self.add_initial_state_inner(genesis, false).await
}
pub async fn add_initial_state_skip_validation(
&mut self,
genesis: Genesis,
) -> Result<(), StoreError> {
self.add_initial_state_inner(genesis, true).await
}
async fn add_initial_state_inner(
&mut self,
genesis: Genesis,
skip_genesis_validation: bool,
) -> Result<(), StoreError> {
debug!("Storing initial state from genesis");
let genesis_block = genesis.get_block();
let genesis_block_number = genesis_block.header.number;
let genesis_hash = genesis_block.hash();
let stored_genesis_header = self.load_block_header(genesis_block_number)?;
self.set_chain_config(&genesis.config).await?;
if let Some(latest) = self.load_latest_block_number().await? {
self.anchor_to_durable_head(latest).await?;
}
match stored_genesis_header {
Some(header) if skip_genesis_validation => {
info!(
stored_genesis = %header.hash(),
"Skipping genesis state validation; trusting the genesis header and state already stored in the datadir"
);
return Ok(());
}
Some(header) if header.hash() == genesis_hash => {
info!("Received genesis file matching a previously stored one, nothing to do");
return Ok(());
}
Some(_) => {
error!(
"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."
);
return Err(StoreError::IncompatibleChainConfig);
}
None => {
self.add_block_header(genesis_hash, genesis_block.header.clone())
.await?
}
}
let genesis_state_root = self.setup_genesis_state_trie(genesis.alloc).await?;
debug_assert_eq!(genesis_state_root, genesis_block.header.state_root);
info!(hash = %genesis_hash, "Storing genesis block");
self.add_block(genesis_block).await?;
self.update_earliest_block_number(genesis_block_number)
.await?;
self.forkchoice_update(vec![], genesis_block_number, genesis_hash, None, None)
.await?;
Ok(())
}
pub async fn load_initial_state(&self) -> Result<(), StoreError> {
info!("Loading initial state from DB");
let Some(latest) = self.load_latest_block_number().await? else {
return Err(StoreError::MissingLatestBlockNumber);
};
self.anchor_to_durable_head(latest).await?;
Ok(())
}
pub fn get_storage_at(
&self,
block_number: BlockNumber,
address: Address,
storage_key: H256,
) -> Result<Option<U256>, StoreError> {
match self.get_block_header(block_number)? {
Some(header) => self.get_storage_at_root(header.state_root, address, storage_key),
None => Ok(None),
}
}
pub fn get_storage_at_root(
&self,
state_root: H256,
address: Address,
storage_key: H256,
) -> Result<Option<U256>, StoreError> {
let account_hash = hash_address_fixed(&address);
let read_view = self.backend.begin_read()?;
let cache = self.gated_snapshot(state_root)?;
let last_written = self.last_written()?;
let use_fkv = Self::flatkeyvalue_computed_with_last_written(account_hash, &last_written)
&& !cache.overlay_serves(state_root);
let storage_root = if use_fkv {
EMPTY_TRIE_HASH
} else {
let state_trie = self.open_state_trie_shared(
state_root,
read_view.clone(),
cache.clone(),
last_written.clone(),
)?;
let Some(encoded_account) = state_trie.get(account_hash.as_bytes())? else {
return Ok(None);
};
let account = AccountState::decode(&encoded_account)?;
account.storage_root
};
let storage_trie = self.open_storage_trie_shared(
account_hash,
state_root,
storage_root,
read_view,
cache,
last_written,
)?;
let hashed_key = hash_key_fixed(&storage_key);
storage_trie
.get(&hashed_key)?
.map(|rlp| U256::decode(&rlp).map_err(StoreError::RLPDecode))
.transpose()
}
pub fn get_storage_at_root_with_known_storage_root(
&self,
state_root: H256,
account_hash: H256,
storage_root: H256,
storage_key: H256,
) -> Result<Option<U256>, StoreError> {
let read_view = self.backend.begin_read()?;
let cache = self.gated_snapshot(state_root)?;
let last_written = self.last_written()?;
let storage_root =
if Self::flatkeyvalue_computed_with_last_written(account_hash, &last_written)
&& !cache.overlay_serves(state_root)
{
EMPTY_TRIE_HASH
} else {
storage_root
};
let storage_trie = self.open_storage_trie_shared(
account_hash,
state_root,
storage_root,
read_view,
cache,
last_written,
)?;
let hashed_key = hash_key_fixed(&storage_key);
storage_trie
.get(&hashed_key)?
.map(|rlp| U256::decode(&rlp).map_err(StoreError::RLPDecode))
.transpose()
}
pub fn get_chain_config(&self) -> ChainConfig {
self.chain_config
}
pub async fn get_latest_canonical_block_hash(&self) -> Result<Option<BlockHash>, StoreError> {
Ok(Some(self.latest_block_header.get().hash()))
}
pub async fn forkchoice_update(
&self,
new_canonical_blocks: Vec<(BlockNumber, BlockHash)>,
head_number: BlockNumber,
head_hash: BlockHash,
safe: Option<BlockNumber>,
finalized: Option<BlockNumber>,
) -> Result<(), StoreError> {
let _guard = self.fcu_lock.lock().await;
let previous_head = self.latest_block_header.get();
let new_head = self
.get_block_header_by_hash(head_hash)?
.ok_or_else(|| StoreError::MissingLatestBlockNumber)?;
self.latest_block_header.update(new_head);
if let Err(err) = self
.forkchoice_update_inner(
new_canonical_blocks,
head_number,
head_hash,
safe,
finalized,
)
.await
{
self.latest_block_header.update((*previous_head).clone());
return Err(err);
}
if let Some(root) = self.compute_safe_commit_root(head_number)? {
if self.set_safe_commit_root(root)? {
let tx = self.persist_tx.clone();
let _ = tokio::task::spawn_blocking(move || tx.send(PersistMessage::Commit(root)))
.await;
}
}
Ok(())
}
pub(crate) fn set_safe_commit_root(&self, root: H256) -> Result<bool, StoreError> {
let mut guard = self
.safe_commit_root
.write()
.map_err(|_| StoreError::LockError)?;
let changed = *guard != root;
*guard = root;
Ok(changed)
}
pub(crate) fn compute_safe_commit_root(
&self,
head: BlockNumber,
) -> Result<Option<H256>, StoreError> {
let commit_threshold = self
.trie_cache
.read()
.map_err(|_| StoreError::LockError)?
.commit_threshold;
let Some(target) = head.checked_sub(commit_threshold as u64) else {
return Ok(None);
};
let Some(hash) = self.get_canonical_block_hash_sync(target)? else {
return Ok(None);
};
let Some(header) = self.get_block_header_by_hash(hash)? else {
return Ok(None);
};
Ok(Some(header.state_root))
}
pub fn state_trie(&self, block_hash: BlockHash) -> Result<Option<Trie>, StoreError> {
let Some(header) = self.get_block_header_by_hash(block_hash)? else {
return Ok(None);
};
Ok(Some(self.open_state_trie(header.state_root)?))
}
pub fn storage_trie(
&self,
block_hash: BlockHash,
address: Address,
) -> Result<Option<Trie>, StoreError> {
let Some(header) = self.get_block_header_by_hash(block_hash)? else {
return Ok(None);
};
let Some(state_trie) = self.state_trie(block_hash)? else {
return Ok(None);
};
let hashed_address = hash_address_fixed(&address);
let Some(encoded_account) = state_trie.get(hashed_address.as_bytes())? else {
return Ok(None);
};
let account = AccountState::decode(&encoded_account)?;
let storage_root = account.storage_root;
Ok(Some(self.open_storage_trie(
hashed_address,
header.state_root,
storage_root,
)?))
}
pub async fn get_account_state(
&self,
block_number: BlockNumber,
address: Address,
) -> Result<Option<AccountState>, StoreError> {
let Some(block_hash) = self.get_canonical_block_hash(block_number).await? else {
return Ok(None);
};
let Some(state_trie) = self.state_trie(block_hash)? else {
return Ok(None);
};
self.get_account_state_from_trie(&state_trie, address)
}
pub fn get_account_state_by_root(
&self,
state_root: H256,
address: Address,
) -> Result<Option<AccountState>, StoreError> {
let state_trie = self.open_state_trie(state_root)?;
self.get_account_state_from_trie(&state_trie, address)
}
pub fn get_account_state_from_trie(
&self,
state_trie: &Trie,
address: Address,
) -> Result<Option<AccountState>, StoreError> {
let hashed_address = hash_address_fixed(&address);
let Some(encoded_state) = state_trie.get(hashed_address.as_bytes())? else {
return Ok(None);
};
Ok(Some(AccountState::decode(&encoded_state)?))
}
pub fn get_account_states_batch_by_root(
&self,
state_root: H256,
addresses: &[Address],
) -> Result<Vec<Option<AccountState>>, StoreError> {
if addresses.is_empty() {
return Ok(Vec::new());
}
let last_written = self.last_written()?;
let trie_cache = self
.trie_cache
.read()
.map_err(|_| StoreError::LockError)?
.clone();
let mut results: Vec<Option<AccountState>> = vec![None; addresses.len()];
let leaf_paths: Vec<Vec<u8>> = addresses
.iter()
.map(|addr| {
let hashed = hash_address_fixed(addr);
Nibbles::from_bytes(hashed.as_bytes()).into_vec()
})
.collect();
let mut fkv_indices: Vec<usize> = Vec::new();
let mut trie_indices: Vec<usize> = Vec::new();
let overlay_active = trie_cache.overlay_serves(state_root);
let fkv_cursor: &[u8] = last_written.as_slice();
for (i, path) in leaf_paths.iter().enumerate() {
if let Some(value) = trie_cache.get(state_root, path.as_slice()) {
if !value.is_empty() {
results[i] = Some(AccountState::decode(&value)?);
}
continue;
}
if !overlay_active && fkv_cursor >= path.as_slice() {
fkv_indices.push(i);
} else {
trie_indices.push(i);
}
}
if !fkv_indices.is_empty() {
fkv_indices.sort_unstable_by(|&a, &b| leaf_paths[a].cmp(&leaf_paths[b]));
let read_view = self.backend.begin_read()?;
let keys: Vec<&[u8]> = fkv_indices
.iter()
.map(|&i| leaf_paths[i].as_slice())
.collect();
let shards = shard_count(keys.len());
let raw: Vec<Result<Option<Vec<u8>>, StoreError>> = if shards <= 1 {
read_view.multi_get(ACCOUNT_FLATKEYVALUE, &keys)
} else {
let chunk = keys.len().div_ceil(shards);
let rv = read_view.as_ref();
std::thread::scope(|scope| {
let handles: Vec<_> = keys
.chunks(chunk)
.map(|ck| scope.spawn(move || rv.multi_get(ACCOUNT_FLATKEYVALUE, ck)))
.collect();
handles
.into_iter()
.flat_map(|h| {
h.join().unwrap_or_else(|_| {
vec![Err(StoreError::Custom(
"account prefetch shard panicked".into(),
))]
})
})
.collect()
})
};
for (slot, res) in fkv_indices.iter().zip(raw.into_iter()) {
let Some(encoded) = res? else { continue };
if encoded.is_empty() {
continue;
}
results[*slot] = Some(AccountState::decode(&encoded)?);
}
}
if !trie_indices.is_empty() {
let state_trie = self.open_state_trie(state_root)?;
let fetched: Result<Vec<(usize, Option<AccountState>)>, StoreError> = trie_indices
.par_iter()
.map(|&i| {
self.get_account_state_from_trie(&state_trie, addresses[i])
.map(|s| (i, s))
})
.collect();
for (i, s) in fetched? {
results[i] = s;
}
}
Ok(results)
}
pub fn get_storage_values_batch_by_root(
&self,
state_root: H256,
slots: &[(H256, H256, H256)],
) -> Result<Vec<Option<U256>>, StoreError> {
if slots.is_empty() {
return Ok(Vec::new());
}
let last_written = self.last_written()?;
let trie_cache = self
.trie_cache
.read()
.map_err(|_| StoreError::LockError)?
.clone();
let mut results: Vec<Option<U256>> = vec![None; slots.len()];
let leaf_paths: Vec<Vec<u8>> = slots
.iter()
.map(|(account_hash, _storage_root, slot)| {
let hashed_slot = hash_key_fixed(slot);
apply_prefix(Some(*account_hash), Nibbles::from_bytes(&hashed_slot)).into_vec()
})
.collect();
let mut fkv_indices: Vec<usize> = Vec::new();
let mut trie_indices: Vec<usize> = Vec::new();
let overlay_active = trie_cache.overlay_serves(state_root);
let fkv_cursor: &[u8] = last_written.as_slice();
for (i, path) in leaf_paths.iter().enumerate() {
if let Some(value) = trie_cache.get(state_root, path.as_slice()) {
if !value.is_empty() {
results[i] = Some(U256::decode(&value).map_err(StoreError::RLPDecode)?);
}
continue;
}
if !overlay_active && fkv_cursor >= path.as_slice() {
fkv_indices.push(i);
} else {
trie_indices.push(i);
}
}
if !fkv_indices.is_empty() {
fkv_indices.sort_unstable_by(|&a, &b| leaf_paths[a].cmp(&leaf_paths[b]));
let read_view = self.backend.begin_read()?;
let keys: Vec<&[u8]> = fkv_indices
.iter()
.map(|&i| leaf_paths[i].as_slice())
.collect();
let shards = shard_count(keys.len());
let raw: Vec<Result<Option<Vec<u8>>, StoreError>> = if shards <= 1 {
read_view.multi_get(STORAGE_FLATKEYVALUE, &keys)
} else {
let chunk = keys.len().div_ceil(shards);
let rv = read_view.as_ref();
std::thread::scope(|scope| {
let handles: Vec<_> = keys
.chunks(chunk)
.map(|ck| scope.spawn(move || rv.multi_get(STORAGE_FLATKEYVALUE, ck)))
.collect();
handles
.into_iter()
.flat_map(|h| {
h.join().unwrap_or_else(|_| {
vec![Err(StoreError::Custom(
"storage prefetch shard panicked".into(),
))]
})
})
.collect()
})
};
for (slot_idx, res) in fkv_indices.iter().zip(raw.into_iter()) {
let Some(encoded) = res? else { continue };
if encoded.is_empty() {
continue;
}
results[*slot_idx] = Some(U256::decode(&encoded).map_err(StoreError::RLPDecode)?);
}
}
if !trie_indices.is_empty() {
let fetched: Result<Vec<(usize, Option<U256>)>, StoreError> = trie_indices
.par_iter()
.map(|&i| {
let (account_hash, storage_root, slot) = slots[i];
self.get_storage_at_root_with_known_storage_root(
state_root,
account_hash,
storage_root,
slot,
)
.map(|v| (i, v))
})
.collect();
for (i, v) in fetched? {
results[i] = v;
}
}
Ok(results)
}
pub fn prefetch_trie_nodes(
&self,
storage_slots: &[(Address, H256)],
accounts: &[Address],
) -> Result<usize, StoreError> {
const MAX_PREFETCH_DEPTH: usize = 8;
let mut storage_keys: Vec<Vec<u8>> =
Vec::with_capacity(storage_slots.len() * (MAX_PREFETCH_DEPTH + 1));
for (address, slot) in storage_slots {
let hashed_address = hash_address_fixed(address);
let hashed_slot = hash_key_fixed(slot);
let slot_nibbles = Nibbles::from_bytes(&hashed_slot);
let max_d = MAX_PREFETCH_DEPTH.min(slot_nibbles.len());
for d in 0..=max_d {
let prefix = apply_prefix(Some(hashed_address), slot_nibbles.slice(0, d));
storage_keys.push(prefix.into_vec());
}
}
let mut account_keys: Vec<Vec<u8>> =
Vec::with_capacity(accounts.len() * (MAX_PREFETCH_DEPTH + 1));
for address in accounts {
let hashed_address = hash_address_fixed(address);
let addr_nibbles = Nibbles::from_bytes(hashed_address.as_bytes());
let max_d = MAX_PREFETCH_DEPTH.min(addr_nibbles.len());
for d in 0..=max_d {
account_keys.push(addr_nibbles.slice(0, d).into_vec());
}
}
let hits = self.warm_trie_node_keys(STORAGE_TRIE_NODES, storage_keys)?
+ self.warm_trie_node_keys(ACCOUNT_TRIE_NODES, account_keys)?;
Ok(hits)
}
fn warm_trie_node_keys(
&self,
table: &'static str,
mut keys: Vec<Vec<u8>>,
) -> Result<usize, StoreError> {
if keys.is_empty() {
return Ok(0);
}
keys.sort_unstable();
keys.dedup();
let shards = shard_count(keys.len());
let read_view = self.backend.begin_read()?;
let count_hits = |res: Vec<Result<Option<Vec<u8>>, StoreError>>| {
res.into_iter().filter(|r| matches!(r, Ok(Some(_)))).count()
};
if shards <= 1 {
let refs: Vec<&[u8]> = keys.iter().map(|k| k.as_slice()).collect();
return Ok(count_hits(read_view.multi_get(table, &refs)));
}
let chunk = keys.len().div_ceil(shards);
let rv = read_view.as_ref();
let hits = std::thread::scope(|scope| {
let handles: Vec<_> = keys
.chunks(chunk)
.map(|ck| {
scope.spawn(move || {
let refs: Vec<&[u8]> = ck.iter().map(|k| k.as_slice()).collect();
count_hits(rv.multi_get(table, &refs))
})
})
.collect();
handles
.into_iter()
.map(|h| h.join().unwrap_or(0))
.sum::<usize>()
});
Ok(hits)
}
pub async fn get_account_proof(
&self,
state_root: H256,
address: Address,
storage_keys: &[H256],
) -> Result<Option<AccountProof>, StoreError> {
let state_trie = self.open_state_trie(state_root)?;
let address_path = hash_address_fixed(&address);
let proof = state_trie.get_proof(address_path.as_bytes())?;
let account_opt = state_trie
.get(address_path.as_bytes())?
.map(|encoded_state| AccountState::decode(&encoded_state))
.transpose()?;
let mut storage_proof = Vec::with_capacity(storage_keys.len());
if let Some(account) = &account_opt {
let storage_trie =
self.open_storage_trie(address_path, state_root, account.storage_root)?;
for key in storage_keys {
let hashed_key = hash_key(key);
let proof = storage_trie.get_proof(&hashed_key)?;
let value = storage_trie
.get(&hashed_key)?
.map(|rlp| U256::decode(&rlp).map_err(StoreError::RLPDecode))
.transpose()?
.unwrap_or_default();
let slot_proof = StorageSlotProof {
proof,
key: *key,
value,
};
storage_proof.push(slot_proof);
}
} else {
storage_proof.extend(storage_keys.iter().map(|key| StorageSlotProof {
proof: Vec::new(),
key: *key,
value: U256::zero(),
}));
}
let account = account_opt.unwrap_or_default();
let account_proof = AccountProof {
proof,
account,
storage_proof,
};
Ok(Some(account_proof))
}
pub fn iter_accounts_from(
&self,
state_root: H256,
starting_address: H256,
) -> Result<impl Iterator<Item = (H256, AccountState)>, StoreError> {
let mut iter = self.open_locked_state_trie(state_root)?.into_iter();
iter.advance(starting_address.0.to_vec())?;
Ok(iter.content().map_while(|(path, value)| {
Some((H256::from_slice(&path), AccountState::decode(&value).ok()?))
}))
}
pub fn iter_accounts(
&self,
state_root: H256,
) -> Result<impl Iterator<Item = (H256, AccountState)>, StoreError> {
self.iter_accounts_from(state_root, H256::zero())
}
pub fn iter_storage_from(
&self,
state_root: H256,
hashed_address: H256,
starting_slot: H256,
) -> Result<Option<impl Iterator<Item = (H256, U256)>>, StoreError> {
let state_trie = self.open_locked_state_trie(state_root)?;
let Some(account_rlp) = state_trie.get(hashed_address.as_bytes())? else {
return Ok(None);
};
let storage_root = AccountState::decode(&account_rlp)?.storage_root;
let mut iter = self
.open_locked_storage_trie(hashed_address, state_root, storage_root)?
.into_iter();
iter.advance(starting_slot.0.to_vec())?;
Ok(Some(iter.content().map_while(|(path, value)| {
Some((H256::from_slice(&path), U256::decode(&value).ok()?))
})))
}
pub fn iter_storage(
&self,
state_root: H256,
hashed_address: H256,
) -> Result<Option<impl Iterator<Item = (H256, U256)>>, StoreError> {
self.iter_storage_from(state_root, hashed_address, H256::zero())
}
pub fn get_account_range_proof(
&self,
state_root: H256,
starting_hash: H256,
last_hash: Option<H256>,
) -> Result<Vec<Vec<u8>>, StoreError> {
let state_trie = self.open_state_trie(state_root)?;
let mut proof = state_trie.get_proof(starting_hash.as_bytes())?;
if let Some(last_hash) = last_hash {
proof.extend_from_slice(&state_trie.get_proof(last_hash.as_bytes())?);
}
Ok(proof)
}
pub fn get_storage_range_proof(
&self,
state_root: H256,
hashed_address: H256,
starting_hash: H256,
last_hash: Option<H256>,
) -> Result<Option<Vec<Vec<u8>>>, StoreError> {
let state_trie = self.open_state_trie(state_root)?;
let Some(account_rlp) = state_trie.get(hashed_address.as_bytes())? else {
return Ok(None);
};
let storage_root = AccountState::decode(&account_rlp)?.storage_root;
let storage_trie = self.open_storage_trie(hashed_address, state_root, storage_root)?;
let mut proof = storage_trie.get_proof(starting_hash.as_bytes())?;
if let Some(last_hash) = last_hash {
proof.extend_from_slice(&storage_trie.get_proof(last_hash.as_bytes())?);
}
Ok(Some(proof))
}
pub fn get_trie_nodes(
&self,
state_root: H256,
paths: Vec<Vec<u8>>,
byte_limit: u64,
) -> Result<Vec<Vec<u8>>, StoreError> {
let Some(account_path) = paths.first() else {
return Ok(vec![]);
};
let state_trie = self.open_state_trie(state_root)?;
if paths.len() == 1 {
let node = state_trie.get_node(account_path)?;
return Ok(vec![node]);
}
let Some(account_state) = state_trie
.get(account_path)?
.map(|ref rlp| AccountState::decode(rlp))
.transpose()?
else {
return Ok(vec![]);
};
let Ok(hashed_address) = account_path.clone().try_into().map(H256) else {
return Ok(vec![]);
};
let storage_trie =
self.open_storage_trie(hashed_address, state_root, account_state.storage_root)?;
let mut nodes = vec![];
let mut bytes_used = 0;
for path in paths.iter().skip(1) {
if bytes_used >= byte_limit {
break;
}
let node = storage_trie.get_node(path)?;
bytes_used += node.len() as u64;
nodes.push(node);
}
Ok(nodes)
}
fn gated_snapshot(&self, state_root: H256) -> Result<Arc<TrieLayerCache>, StoreError> {
self.pending_trie_roots.wait_until_ready(state_root)?;
Ok(self
.trie_cache
.read()
.map_err(|_| StoreError::LockError)?
.clone())
}
pub fn open_state_trie(&self, state_root: H256) -> Result<Trie, StoreError> {
let trie_db = TrieWrapper::new(
state_root,
self.gated_snapshot(state_root)?,
Box::new(BackendTrieDB::new_for_accounts(
self.backend.clone(),
self.last_written()?,
)?),
None,
);
Ok(Trie::open(Box::new(trie_db), state_root))
}
pub fn open_direct_state_trie(&self, state_root: H256) -> Result<Trie, StoreError> {
Ok(Trie::open(
Box::new(BackendTrieDB::new_for_accounts(
self.backend.clone(),
self.last_written()?,
)?),
state_root,
))
}
pub fn open_locked_state_trie(&self, state_root: H256) -> Result<Trie, StoreError> {
let trie_db = TrieWrapper::new(
state_root,
self.gated_snapshot(state_root)?,
Box::new(state_trie_locked_backend(
self.backend.as_ref(),
self.last_written()?,
)?),
None,
);
Ok(Trie::open(Box::new(trie_db), state_root))
}
pub fn open_storage_trie(
&self,
account_hash: H256,
state_root: H256,
storage_root: H256,
) -> Result<Trie, StoreError> {
let trie_db = TrieWrapper::new(
state_root,
self.gated_snapshot(state_root)?,
Box::new(BackendTrieDB::new_for_storages(
self.backend.clone(),
self.last_written()?,
)?),
Some(account_hash),
);
Ok(Trie::open(Box::new(trie_db), storage_root))
}
fn open_state_trie_shared(
&self,
state_root: H256,
read_view: Arc<dyn StorageReadView>,
cache: Arc<TrieLayerCache>,
last_written: Vec<u8>,
) -> Result<Trie, StoreError> {
let trie_db = TrieWrapper::new(
state_root,
cache,
Box::new(BackendTrieDB::new_for_accounts_with_view(
self.backend.clone(),
read_view,
last_written,
)?),
None,
);
Ok(Trie::open(Box::new(trie_db), state_root))
}
fn open_storage_trie_shared(
&self,
account_hash: H256,
state_root: H256,
storage_root: H256,
read_view: Arc<dyn StorageReadView>,
cache: Arc<TrieLayerCache>,
last_written: Vec<u8>,
) -> Result<Trie, StoreError> {
let trie_db = TrieWrapper::new(
state_root,
cache,
Box::new(BackendTrieDB::new_for_storages_with_view(
self.backend.clone(),
read_view,
last_written,
)?),
Some(account_hash),
);
Ok(Trie::open(Box::new(trie_db), storage_root))
}
pub fn open_direct_storage_trie(
&self,
account_hash: H256,
storage_root: H256,
) -> Result<Trie, StoreError> {
Ok(Trie::open(
Box::new(BackendTrieDB::new_for_account_storage(
self.backend.clone(),
account_hash,
self.last_written()?,
)?),
storage_root,
))
}
pub fn open_locked_storage_trie(
&self,
account_hash: H256,
state_root: H256,
storage_root: H256,
) -> Result<Trie, StoreError> {
let trie_db = TrieWrapper::new(
state_root,
self.gated_snapshot(state_root)?,
Box::new(state_trie_locked_backend(
self.backend.as_ref(),
self.last_written()?,
)?),
Some(account_hash),
);
Ok(Trie::open(Box::new(trie_db), storage_root))
}
pub fn has_state_root(&self, state_root: H256) -> Result<bool, StoreError> {
if state_root == EMPTY_TRIE_HASH {
return Ok(true);
}
let trie = self.open_state_trie(state_root)?;
let Some(root) = trie.db().get(Nibbles::default())? else {
return Ok(false);
};
let root_hash = ethrex_trie::Node::decode(&root)?
.compute_hash(&NativeCrypto)
.finalize(&NativeCrypto);
Ok(state_root == root_hash)
}
pub fn is_state_in_layer_cache(&self, state_root: H256) -> Result<bool, StoreError> {
let trie = self
.trie_cache
.read()
.map_err(|_| StoreError::LockError)?
.clone();
Ok(trie.contains(state_root))
}
pub fn highest_state_history_block_number(&self) -> Result<Option<BlockNumber>, StoreError> {
let read = self.backend.begin_read()?;
let Some(key) = read.last_key(STATE_HISTORY)? else {
return Ok(None);
};
let arr = <[u8; 8]>::try_from(key.as_slice()).map_err(|_| {
StoreError::Custom(format!(
"STATE_HISTORY key has unexpected length: {}",
key.len()
))
})?;
Ok(Some(BlockNumber::from_be_bytes(arr)))
}
pub fn get_state_history_block_hash(
&self,
block_number: BlockNumber,
) -> Result<Option<BlockHash>, StoreError> {
let read = self.backend.begin_read()?;
let Some(bytes) = read.get(STATE_HISTORY, &block_number.to_be_bytes())? else {
return Ok(None);
};
JournalEntry::decode_block_hash(&bytes)
.map(Some)
.map_err(|e| StoreError::Custom(format!("STATE_HISTORY entry {block_number}: {e}")))
}
pub fn lowest_state_history_block_number(&self) -> Result<Option<BlockNumber>, StoreError> {
let read = self.backend.begin_read()?;
let Some(key) = read.first_key(STATE_HISTORY)? else {
return Ok(None);
};
let arr = <[u8; 8]>::try_from(key.as_slice()).map_err(|_| {
StoreError::Custom(format!(
"STATE_HISTORY key has unexpected length: {}",
key.len()
))
})?;
Ok(Some(BlockNumber::from_be_bytes(arr)))
}
pub fn reorg_retention(&self) -> Result<u64, StoreError> {
let cache = self.trie_cache.read().map_err(|_| StoreError::LockError)?;
Ok(cache.commit_threshold() as u64)
}
#[doc(hidden)]
pub fn put_state_history_entry_for_test(
&self,
block_number: BlockNumber,
encoded: &[u8],
) -> Result<(), StoreError> {
let mut tx = self.backend.begin_write()?;
tx.put(STATE_HISTORY, &block_number.to_be_bytes(), encoded)?;
tx.commit()?;
Ok(())
}
pub fn install_overlay_for_reorg(
&self,
from_block: BlockNumber,
to_block: BlockNumber,
expected_hash: impl Fn(BlockNumber) -> Option<H256>,
) -> Result<(), StoreError> {
let overlay =
Overlay::from_journal(self.backend.as_ref(), from_block, to_block, expected_hash)
.map_err(|e| StoreError::Custom(format!("overlay construction failed: {e}")))?;
let threshold = {
let current = self.trie_cache.read().map_err(|_| StoreError::LockError)?;
current.commit_threshold()
};
let mut fresh =
TrieLayerCache::new_with_safe_commit(threshold, self.safe_commit_root.clone());
fresh.set_overlay(Arc::new(overlay));
self.rendezvous_persist_worker("install_overlay_for_reorg")?;
let mut guard = self.trie_cache.write().map_err(|_| StoreError::LockError)?;
*guard = Arc::new(fresh);
Ok(())
}
fn rendezvous_persist_worker(&self, caller: &str) -> Result<(), StoreError> {
let (ack_tx, ack_rx) = sync_channel::<Result<(), StoreError>>(1);
self.persist_tx
.send(PersistMessage::Ping(ack_tx))
.map_err(|e| {
StoreError::Custom(format!("{caller}: failed to ping persist worker: {e}"))
})?;
ack_rx.recv().map_err(|e| {
StoreError::Custom(format!("{caller}: persist worker ping ack failed: {e}"))
})??;
Ok(())
}
pub fn reorg_overlay_size_hint(&self) -> Result<(usize, usize), StoreError> {
let guard = self.trie_cache.read().map_err(|_| StoreError::LockError)?;
match guard.overlay() {
Some(ov) => Ok((ov.len(), ov.byte_size())),
None => Ok((0, 0)),
}
}
pub fn set_journal_pruning_paused(&self, paused: bool) {
self.journal_pruning_paused
.store(paused, std::sync::atomic::Ordering::Release);
}
pub fn clear_reorg_overlay(&self) -> Result<(), StoreError> {
let mut guard = self.trie_cache.write().map_err(|_| StoreError::LockError)?;
let mut updated = (**guard).clone();
updated.clear_overlay();
*guard = Arc::new(updated);
Ok(())
}
pub fn abort_reorg(&self) -> Result<(), StoreError> {
self.rendezvous_persist_worker("abort_reorg")?;
let mut guard = self.trie_cache.write().map_err(|_| StoreError::LockError)?;
let threshold = guard.commit_threshold();
*guard = Arc::new(TrieLayerCache::new_with_safe_commit(
threshold,
self.safe_commit_root.clone(),
));
Ok(())
}
pub fn ancestors(&self, block_hash: BlockHash) -> AncestorIterator {
AncestorIterator {
store: self.clone(),
next_hash: block_hash,
}
}
pub fn is_canonical_sync(&self, block_hash: BlockHash) -> Result<bool, StoreError> {
let Some(block_number) = self.get_block_number_sync(block_hash)? else {
return Ok(false);
};
Ok(self
.get_canonical_block_hash_sync(block_number)?
.is_some_and(|h| h == block_hash))
}
pub fn generate_flatkeyvalue(&self) -> Result<(), StoreError> {
self.flatkeyvalue_control_tx
.send(FKVGeneratorControlMessage::Continue)
.map_err(|_| StoreError::Custom("FlatKeyValue thread disconnected.".to_string()))
}
pub fn create_checkpoint(&self, path: impl AsRef<Path>) -> Result<(), StoreError> {
self.backend.create_checkpoint(path.as_ref())?;
init_metadata_file(path.as_ref())?;
Ok(())
}
pub fn get_store_directory(&self) -> Result<PathBuf, StoreError> {
Ok(self.db_path.clone())
}
async fn load_latest_block_number(&self) -> Result<Option<BlockNumber>, StoreError> {
self.read_chain_data_block_number(ChainDataIndex::LatestBlockNumber)
.await
}
fn load_canonical_block_hash(
&self,
block_number: BlockNumber,
) -> Result<Option<BlockHash>, StoreError> {
let txn = self.backend.begin_read()?;
txn.get(
CANONICAL_BLOCK_HASHES,
block_number.to_le_bytes().as_slice(),
)?
.map(|bytes| H256::decode(bytes.as_slice()))
.transpose()
.map_err(StoreError::from)
}
fn load_block_header(
&self,
block_number: BlockNumber,
) -> Result<Option<BlockHeader>, StoreError> {
let Some(block_hash) = self.load_canonical_block_hash(block_number)? else {
return Ok(None);
};
self.load_block_header_by_hash(block_hash)
}
fn load_block_header_by_hash(
&self,
block_hash: BlockHash,
) -> Result<Option<BlockHeader>, StoreError> {
let txn = self.backend.begin_read()?;
let hash_key = block_hash.encode_to_vec();
let header_value = txn.get(HEADERS, hash_key.as_slice())?;
let mut header = header_value
.map(|bytes| BlockHeaderRLP::from_bytes(bytes).to())
.transpose()
.map_err(StoreError::from)?;
header.as_mut().inspect(|h| {
let _ = h.hash.set(block_hash);
});
Ok(header)
}
pub fn last_written(&self) -> Result<Vec<u8>, StoreError> {
let last_computed_flatkeyvalue = self
.last_computed_flatkeyvalue
.read()
.map_err(|_| StoreError::LockError)?;
Ok(last_computed_flatkeyvalue.clone())
}
pub fn flatkeyvalue_fully_generated(&self) -> Result<bool, StoreError> {
let tx = self.backend.begin_read()?;
let marker = tx.get(MISC_VALUES, "last_written".as_bytes())?;
Ok(Self::flatkeyvalue_generation_complete(marker.as_deref()))
}
fn flatkeyvalue_generation_complete(marker: Option<&[u8]>) -> bool {
marker == Some([0xff].as_slice())
}
fn flatkeyvalue_computed_with_last_written(account: H256, last_written: &[u8]) -> bool {
let account_nibbles = Nibbles::from_bytes(account.as_bytes());
&last_written[0..64] > account_nibbles.as_ref()
}
pub fn advance_flushed_upto(&self, block_number: BlockNumber) -> Result<(), StoreError> {
if self
.read_flushed_upto_opt()?
.is_some_and(|current| current >= block_number)
{
return Ok(());
}
let mut tx = self.backend.begin_write()?;
write_flushed_upto(tx.as_mut(), block_number)?;
tx.commit()?;
mutate_block_buffer(&self.block_data_buffer, |b| {
b.set_flushed_upto(block_number)
})
}
pub fn read_flushed_upto(&self) -> Result<BlockNumber, StoreError> {
Ok(self.read_flushed_upto_opt()?.unwrap_or(0))
}
fn read_flushed_upto_opt(&self) -> Result<Option<BlockNumber>, StoreError> {
let tx = self.backend.begin_read()?;
match tx.get(MISC_VALUES, FLUSHED_UPTO_KEY)? {
Some(bytes) => Ok(Some(decode_flushed_upto(&bytes)?)),
None => Ok(None),
}
}
#[cfg(any(test, feature = "testing"))]
pub fn buffer_block_for_test(&self, block: &Block) {
mutate_block_buffer(&self.block_data_buffer, |b| {
b.insert(block.clone(), vec![], vec![])
})
.expect("block_data_buffer lock poisoned");
}
#[cfg(any(test, feature = "testing"))]
pub fn flush_block_data_for_test(&self) -> Result<(), StoreError> {
flush_block_data(self.backend.as_ref(), &self.block_data_buffer)
}
#[cfg(any(test, feature = "testing"))]
pub fn get_trie_node_for_test(
&self,
is_account: bool,
key: &[u8],
) -> Result<Option<Vec<u8>>, StoreError> {
let table = if is_account {
ACCOUNT_TRIE_NODES
} else {
STORAGE_TRIE_NODES
};
self.backend.begin_read()?.get(table, key)
}
#[cfg(any(test, feature = "testing"))]
pub fn buffer_block_with_codes_for_test(&self, block: &Block, codes: Vec<(H256, Code)>) {
mutate_block_buffer(&self.block_data_buffer, |b| {
b.insert(block.clone(), vec![], codes)
})
.expect("block_data_buffer lock poisoned");
}
#[cfg(any(test, feature = "testing"))]
pub fn register_pending_root_for_test(&self, root: H256) -> Result<(), StoreError> {
self.pending_trie_roots.register(root)
}
#[cfg(any(test, feature = "testing"))]
pub fn clear_pending_root_for_test(&self, root: H256) {
self.pending_trie_roots.clear(root)
}
async fn anchor_to_durable_head(&self, latest: BlockNumber) -> Result<(), StoreError> {
let marker = self.read_flushed_upto_opt()?;
let start = match marker {
Some(flushed) => flushed.min(latest),
None => latest,
};
let mut head = start;
let latest_block_header = loop {
match self.load_block_header(head)? {
Some(header) => break header,
None if marker.is_none() => return Err(StoreError::MissingLatestBlockNumber),
None if head == 0 => return Err(StoreError::MissingLatestBlockNumber),
None => {
warn!(
"durable head {head}: canonical hash has no on-disk header \
(reorg inside flush window); rewinding"
);
head -= 1;
}
}
};
self.latest_block_header.update(latest_block_header);
let reanchor = head != latest;
let rewrite_marker = marker != Some(head);
if reanchor || rewrite_marker {
let mut tx = self.backend.begin_write()?;
if reanchor {
let latest_key = chain_data_key(ChainDataIndex::LatestBlockNumber);
tx.put(CHAIN_DATA, &latest_key, &head.to_le_bytes())?;
}
if rewrite_marker {
write_flushed_upto(tx.as_mut(), head)?;
}
tx.commit()?;
}
Ok(())
}
}
pub fn write_flushed_upto(
tx: &mut dyn StorageWriteBatch,
n: BlockNumber,
) -> Result<(), StoreError> {
tx.put(MISC_VALUES, FLUSHED_UPTO_KEY, &n.to_le_bytes())
}
fn decode_flushed_upto(bytes: &[u8]) -> Result<BlockNumber, StoreError> {
let arr: [u8; 8] = bytes
.try_into()
.map_err(|_| StoreError::Custom("Invalid flushed_upto bytes".to_string()))?;
Ok(BlockNumber::from_le_bytes(arr))
}
fn mutate_block_buffer(
buffer: &Arc<RwLock<Arc<BlockDataBuffer>>>,
f: impl FnOnce(&mut BlockDataBuffer),
) -> Result<(), StoreError> {
let mut new_buf = (*buffer.read().map_err(|_| StoreError::LockError)?.clone()).clone();
f(&mut new_buf);
*buffer.write().map_err(|_| StoreError::LockError)? = Arc::new(new_buf);
Ok(())
}
const DEFAULT_PERSIST_CHANNEL_CAPACITY: usize = 2;
struct BlockPersist {
blocks: Vec<(Block, Vec<Receipt>)>,
codes: Vec<(H256, Code)>,
parent_state_root: H256,
child_state_root: H256,
account_updates: TrieNodesUpdate,
storage_updates: Vec<(H256, TrieNodesUpdate)>,
commit_depth: Option<usize>,
wait_for_flush: bool,
block_number: BlockNumber,
block_hash: H256,
ack: std::sync::mpsc::SyncSender<Result<(), StoreError>>,
}
enum PersistMessage {
Block(Box<BlockPersist>),
Commit(H256),
Ping(std::sync::mpsc::SyncSender<Result<(), StoreError>>),
Shutdown {
ack: std::sync::mpsc::SyncSender<Result<(), StoreError>>,
},
}
fn write_block_data(
tx: &mut dyn StorageWriteBatch,
number: BlockNumber,
hash: BlockHash,
header: &BlockHeader,
body: &BlockBody,
) -> Result<(), StoreError> {
let hash_key = hash.encode_to_vec();
tx.put(
HEADERS,
&hash_key,
BlockHeaderRLP::from(header.clone()).bytes(),
)?;
tx.put(
BODIES,
&hash_key,
BlockBodyRLP::from_bytes(body.encode_to_vec()).bytes(),
)?;
tx.put(BLOCK_NUMBERS, &hash_key, &number.to_le_bytes())?;
for (index, transaction) in body.transactions.iter().enumerate() {
tx.merge(
TRANSACTION_LOCATIONS,
transaction.hash(&NativeCrypto).as_bytes(),
&encode_tx_location_operand(number, hash, index as u64),
)?;
}
Ok(())
}
fn flush_block_data(
backend: &dyn StorageBackend,
buffer: &Arc<RwLock<Arc<BlockDataBuffer>>>,
) -> Result<(), StoreError> {
let snapshot = buffer.read().map_err(|_| StoreError::LockError)?.clone();
let to_flush = snapshot.flushable();
if to_flush.is_empty() {
return Ok(());
}
let hashes: Vec<_> = to_flush.iter().map(|b| b.header.hash()).collect();
let codes = snapshot.codes_for(&hashes);
let mut max_number = snapshot.flushed_upto();
let mut tx = backend.begin_write()?;
for b in &to_flush {
let hash = b.header.hash();
write_block_data(tx.as_mut(), b.number, hash, &b.header, &b.body)?;
for (index, receipt) in b.receipts.iter().enumerate() {
tx.put(
RECEIPTS_V2,
&receipt_key(&hash, index as u64),
&receipt.encode_to_vec(),
)?;
}
max_number = max_number.max(b.number);
}
for (code_hash, code) in codes {
let buf = encode_code(&code);
tx.put(ACCOUNT_CODES, code_hash.as_ref(), &buf)?;
tx.put(
ACCOUNT_CODE_METADATA,
code_hash.as_ref(),
&(code.len() as u64).to_be_bytes(),
)?;
}
write_flushed_upto(tx.as_mut(), max_number)?;
tx.commit()?;
mutate_block_buffer(buffer, |b| b.evict_flushed(max_number))
}
type TrieNodesUpdate = Vec<(Nibbles, Vec<u8>)>;
#[derive(Debug, Default)]
struct PendingTrieRoots {
count: AtomicUsize,
roots: Mutex<HashSet<H256>>,
ready: Condvar,
}
impl PendingTrieRoots {
fn register(&self, root: H256) -> Result<(), StoreError> {
let mut roots = self.roots.lock().map_err(|_| StoreError::LockError)?;
if roots.insert(root) {
self.count.fetch_add(1, Ordering::Release);
}
Ok(())
}
fn clear(&self, root: H256) {
let Ok(mut roots) = self.roots.lock() else {
return;
};
if roots.remove(&root) {
self.count.fetch_sub(1, Ordering::Release);
self.ready.notify_all();
}
}
fn wait_until_ready(&self, root: H256) -> Result<(), StoreError> {
if self.count.load(Ordering::Acquire) == 0 {
return Ok(());
}
let mut roots = self.roots.lock().map_err(|_| StoreError::LockError)?;
while roots.contains(&root) {
roots = self.ready.wait(roots).map_err(|_| StoreError::LockError)?;
}
Ok(())
}
}
#[allow(clippy::too_many_arguments)]
fn apply_trie_phase1(
trie_cache: &Arc<RwLock<Arc<TrieLayerCache>>>,
pending_roots: &PendingTrieRoots,
parent_state_root: H256,
child_state_root: H256,
block_number: BlockNumber,
block_hash: H256,
account_updates: TrieNodesUpdate,
storage_updates: Vec<(H256, TrieNodesUpdate)>,
) -> Result<(), StoreError> {
let build: Result<(), StoreError> = (|| {
let new_layer = storage_updates
.into_iter()
.flat_map(|(account_hash, nodes)| {
nodes
.into_iter()
.map(move |(path, node)| (apply_prefix(Some(account_hash), path), node))
})
.chain(account_updates)
.collect();
let trie = trie_cache
.read()
.map_err(|_| StoreError::LockError)?
.clone();
let mut trie_mut = (*trie).clone();
trie_mut.put_batch(
parent_state_root,
child_state_root,
block_number,
block_hash,
new_layer,
);
*trie_cache.write().map_err(|_| StoreError::LockError)? = Arc::new(trie_mut);
Ok(())
})();
pending_roots.clear(child_state_root);
build
}
fn commit_trie_if_due(
backend: &dyn StorageBackend,
trie_cache: &Arc<RwLock<Arc<TrieLayerCache>>>,
fkv_ctl: &SyncSender<FKVGeneratorControlMessage>,
parent_state_root: H256,
commit_depth: Option<usize>,
is_batch: bool,
) -> Result<(), StoreError> {
let trie = trie_cache
.read()
.map_err(|_| StoreError::LockError)?
.clone();
let commitable = match commit_depth {
Some(depth) => trie.get_commitable_by_depth(parent_state_root, depth),
None => trie.get_commitable(parent_state_root),
};
let Some(root) = commitable else {
return Ok(());
};
commit_to_disk(backend, fkv_ctl, trie_cache, &trie, root, is_batch)
}
fn commit_to_disk(
backend: &dyn StorageBackend,
fkv_ctl: &SyncSender<FKVGeneratorControlMessage>,
trie_cache: &Arc<RwLock<Arc<TrieLayerCache>>>,
trie: &Arc<TrieLayerCache>,
root: H256,
is_batch: bool,
) -> Result<(), StoreError> {
if !trie.has_layer(root) {
debug!(
root = ?root,
layers = trie.layer_count(),
is_batch,
"Skipping trie commit: state root has no in-memory layer. Expected when the block \
did not change the state root (empty L2 blocks) or the root was already flushed."
);
return Ok(());
}
let _ = fkv_ctl.send(FKVGeneratorControlMessage::Stop);
let mut trie_mut = (**trie).clone();
let read_view = backend.begin_read()?;
let last_written = read_view
.get(MISC_VALUES, "last_written".as_bytes())?
.unwrap_or_default();
let mut write_tx = backend.begin_write()?;
let overlay_for_reconciliation = if !is_batch {
trie.overlay().cloned()
} else {
None
};
let root = if overlay_for_reconciliation.is_some() {
trie.bottom_layer_root(root)
} else {
root
};
let committed_layers = trie_mut.commit(root).ok_or_else(|| {
StoreError::Custom(format!(
"trie layer for state root {root:?} disappeared between the has_layer check and \
commit (layers={}, is_batch={is_batch}): the TrieLayerCache clone lost the layer \
map, so this block's state was not written",
trie.layer_count(),
))
})?;
debug_assert!(
overlay_for_reconciliation.is_none() || committed_layers.len() == 1,
"overlay-backed reconciliation must commit exactly one layer (T), got {}",
committed_layers.len()
);
let extra_writes: Vec<(Vec<u8>, Vec<u8>)> = match &overlay_for_reconciliation {
Some(overlay) => {
let layer_keys: rustc_hash::FxHashSet<&Vec<u8>> = committed_layers
.iter()
.flat_map(|l| l.nodes.iter().map(|(k, _)| k))
.collect();
overlay
.iter_all_entries()
.filter(|(_, key, _)| !layer_keys.contains(key))
.map(|(_, key, value)| (key.clone(), value.clone().unwrap_or_default()))
.collect()
}
None => Vec::new(),
};
let reorg_heights = overlay_for_reconciliation
.as_ref()
.map(|ov| (ov.to_block(), ov.from_block()));
let mut overlay: HashMap<Vec<u8>, Option<Vec<u8>>> = HashMap::new();
let mut result = Ok(());
'layers: for layer in &committed_layers {
let mut journal_account_trie: FlatDiff = Vec::new();
let mut journal_storage_trie: FlatDiff = Vec::new();
let mut journal_account_flat: FlatDiff = Vec::new();
let mut journal_storage_flat: FlatDiff = Vec::new();
let is_reconciliation_layer = overlay_for_reconciliation.is_some();
let extra: &[(Vec<u8>, Vec<u8>)] = if is_reconciliation_layer {
&extra_writes
} else {
&[]
};
let pivot_overlay = overlay_for_reconciliation.as_ref();
for (key, value) in layer.nodes.iter().chain(extra.iter()) {
let (is_leaf, is_account) = classify_trie_key(key.len());
if is_leaf && key.as_slice() > last_written.as_slice() {
continue;
}
let table = if is_leaf {
if is_account {
&ACCOUNT_FLATKEYVALUE
} else {
&STORAGE_FLATKEYVALUE
}
} else if is_account {
&ACCOUNT_TRIE_NODES
} else {
&STORAGE_TRIE_NODES
};
let prev_value = if !is_batch {
match overlay.get(key) {
Some(v) => Some(v.clone()),
None => match pivot_overlay
.and_then(|ov| ov.lookup(OverlayCf::classify_by_key_length(key.len()), key))
{
Some(v) => Some(v),
None => match read_view.get(table, key) {
Ok(v) => Some(v),
Err(e) => {
result = Err(e);
break 'layers;
}
},
},
}
} else {
None
};
let new_value = if value.is_empty() {
result = write_tx.delete(table, key);
None
} else {
result = write_tx.put(table, key, value);
Some(value.clone())
};
if result.is_err() {
break 'layers;
}
if let Some(prev) = prev_value {
let bucket = match (is_leaf, is_account) {
(false, true) => &mut journal_account_trie,
(false, false) => &mut journal_storage_trie,
(true, true) => &mut journal_account_flat,
(true, false) => &mut journal_storage_flat,
};
bucket.push((key.clone(), prev));
overlay.insert(key.clone(), new_value);
}
}
if !is_batch && let Some((t, d)) = reorg_heights {
debug_assert_eq!(
layer.block_number, t,
"first new-chain commit must be at the pivot's T height (overlay.to_block)"
);
if layer.block_number == t {
let start = t.to_be_bytes();
let end = d.saturating_add(1).to_be_bytes();
result = write_tx.delete_range(STATE_HISTORY, &start, &end);
if result.is_err() {
break 'layers;
}
} else {
error!(
block_number = layer.block_number,
t,
d,
"deep-reorg reconciliation skipped: committed layer not at pivot height; skipping STATE_HISTORY delete_range to avoid history gaps"
);
}
}
if !is_batch {
let entry = JournalEntry {
block_hash: layer.block_hash,
parent_state_root: layer.parent_state_root,
account_trie_diff: journal_account_trie,
storage_trie_diff: journal_storage_trie,
account_flat_diff: journal_account_flat,
storage_flat_diff: journal_storage_flat,
};
result = write_tx.put(
STATE_HISTORY,
&layer.block_number.to_be_bytes(),
&entry.encode(),
);
if result.is_err() {
break 'layers;
}
}
}
if result.is_ok() {
result = write_tx.commit();
}
let _ = fkv_ctl.send(FKVGeneratorControlMessage::Continue);
result?;
if overlay_for_reconciliation.is_some() {
trie_mut.clear_overlay();
}
*trie_cache.write().map_err(|_| StoreError::LockError)? = Arc::new(trie_mut);
Ok(())
}
fn flatkeyvalue_generator(
backend: &Arc<dyn StorageBackend>,
last_computed_fkv: &RwLock<Vec<u8>>,
control_rx: &std::sync::mpsc::Receiver<FKVGeneratorControlMessage>,
) -> Result<(), StoreError> {
info!("Generation of FlatKeyValue started.");
let initial_last_written = backend
.begin_read()?
.get(MISC_VALUES, "last_written".as_bytes())?
.unwrap_or_default();
if initial_last_written.is_empty() {
backend.clear_table(ACCOUNT_FLATKEYVALUE)?;
backend.clear_table(STORAGE_FLATKEYVALUE)?;
} else if initial_last_written == [0xff] {
info!("FlatKeyValue already generated. Skipping.");
return Ok(());
}
loop {
let read_tx = backend.begin_read()?;
let root = read_tx
.get(ACCOUNT_TRIE_NODES, &[])?
.ok_or(StoreError::MissingLatestBlockNumber)?;
let root: Node = ethrex_trie::Node::decode(&root)?;
let state_root = root.compute_hash(&NativeCrypto).finalize(&NativeCrypto);
let last_written = read_tx
.get(MISC_VALUES, "last_written".as_bytes())?
.unwrap_or_default();
let last_written_account = last_written
.get(0..64)
.map(|v| Nibbles::from_hex(v.to_vec()))
.unwrap_or_default();
let mut last_written_storage = last_written
.get(66..130)
.map(|v| Nibbles::from_hex(v.to_vec()))
.unwrap_or_default();
debug!("Starting FlatKeyValue loop pivot={last_written:?} SR={state_root:x}");
let mut ctr = 0;
let mut write_txn = backend.begin_write()?;
let mut iter = Trie::open(
Box::new(BackendTrieDB::new_for_accounts_with_view(
backend.clone(),
read_tx.clone(),
last_written.clone(),
)?),
state_root,
)
.into_iter();
if last_written_account > Nibbles::default() {
iter.advance(last_written_account.to_bytes())?;
}
let res = iter.try_for_each(|(path, node)| -> Result<(), StoreError> {
let Node::Leaf(node) = node else {
return Ok(());
};
let account_state = AccountState::decode(&node.value)?;
let account_hash = H256::from_slice(&path.to_bytes());
write_txn.put(MISC_VALUES, "last_written".as_bytes(), path.as_ref())?;
write_txn.put(ACCOUNT_FLATKEYVALUE, path.as_ref(), &node.value)?;
ctr += 1;
if ctr > 10_000 {
write_txn.commit()?;
write_txn = backend.begin_write()?;
*last_computed_fkv
.write()
.map_err(|_| StoreError::LockError)? = path.as_ref().to_vec();
ctr = 0;
}
let mut iter_inner = Trie::open(
Box::new(BackendTrieDB::new_for_account_storage_with_view(
backend.clone(),
read_tx.clone(),
account_hash,
path.as_ref().to_vec(),
)?),
account_state.storage_root,
)
.into_iter();
if last_written_storage > Nibbles::default() {
iter_inner.advance(last_written_storage.to_bytes())?;
last_written_storage = Nibbles::default();
}
iter_inner.try_for_each(|(path, node)| -> Result<(), StoreError> {
let Node::Leaf(node) = node else {
return Ok(());
};
let key = apply_prefix(Some(account_hash), path);
write_txn.put(MISC_VALUES, "last_written".as_bytes(), key.as_ref())?;
write_txn.put(STORAGE_FLATKEYVALUE, key.as_ref(), &node.value)?;
ctr += 1;
if ctr > 10_000 {
write_txn.commit()?;
write_txn = backend.begin_write()?;
*last_computed_fkv
.write()
.map_err(|_| StoreError::LockError)? = key.into_vec();
ctr = 0;
}
fkv_check_for_stop_msg(control_rx)?;
Ok(())
})?;
fkv_check_for_stop_msg(control_rx)?;
Ok(())
});
match res {
Err(StoreError::PivotChanged) => {
match control_rx.recv() {
Ok(FKVGeneratorControlMessage::Continue) => {}
Ok(FKVGeneratorControlMessage::Stop) => {
return Err(StoreError::Custom("Unexpected Stop message".to_string()));
}
Err(std::sync::mpsc::RecvError) => {
info!("Store closed, stopping FlatKeyValue generation.");
return Ok(());
}
}
}
Err(err) => return Err(err),
Ok(()) => {
write_txn.put(MISC_VALUES, "last_written".as_bytes(), &[0xff])?;
write_txn.commit()?;
*last_computed_fkv
.write()
.map_err(|_| StoreError::LockError)? = vec![0xff; 131];
info!("FlatKeyValue generation finished.");
return Ok(());
}
};
}
}
fn fkv_check_for_stop_msg(
control_rx: &std::sync::mpsc::Receiver<FKVGeneratorControlMessage>,
) -> Result<(), StoreError> {
match control_rx.try_recv() {
Ok(FKVGeneratorControlMessage::Stop) | Err(TryRecvError::Disconnected) => {
return Err(StoreError::PivotChanged);
}
Ok(FKVGeneratorControlMessage::Continue) => {
return Err(StoreError::Custom(
"Unexpected Continue message".to_string(),
));
}
Err(TryRecvError::Empty) => {}
}
Ok(())
}
fn state_trie_locked_backend(
backend: &dyn StorageBackend,
last_written: Vec<u8>,
) -> Result<BackendTrieDBLocked, StoreError> {
BackendTrieDBLocked::new(backend, last_written)
}
pub struct AccountProof {
pub proof: Vec<NodeRLP>,
pub account: AccountState,
pub storage_proof: Vec<StorageSlotProof>,
}
pub struct StorageSlotProof {
pub proof: Vec<NodeRLP>,
pub key: H256,
pub value: U256,
}
pub struct AncestorIterator {
store: Store,
next_hash: BlockHash,
}
impl Iterator for AncestorIterator {
type Item = Result<(BlockHash, BlockHeader), StoreError>;
fn next(&mut self) -> Option<Self::Item> {
let next_hash = self.next_hash;
match self.store.get_block_header_by_hash(next_hash) {
Ok(Some(header)) => {
let ret_hash = self.next_hash;
self.next_hash = header.parent_hash;
Some(Ok((ret_hash, header)))
}
Ok(None) => None,
Err(e) => Some(Err(e)),
}
}
}
pub fn hash_address(address: &Address) -> Vec<u8> {
keccak_hash(address.to_fixed_bytes()).to_vec()
}
fn hash_address_fixed(address: &Address) -> H256 {
keccak(address.to_fixed_bytes())
}
pub fn hash_key(key: &H256) -> Vec<u8> {
keccak_hash(key.to_fixed_bytes()).to_vec()
}
pub fn hash_key_fixed(key: &H256) -> [u8; 32] {
keccak_hash(key.to_fixed_bytes())
}
fn chain_data_key(index: ChainDataIndex) -> Vec<u8> {
(index as u8).encode_to_vec()
}
fn decode_block_number(bytes: Vec<u8>) -> Result<BlockNumber, StoreError> {
let array: [u8; 8] = bytes
.try_into()
.map_err(|_| StoreError::Custom("Invalid BlockNumber bytes".to_string()))?;
Ok(BlockNumber::from_le_bytes(array))
}
fn snap_state_key(index: SnapStateIndex) -> Vec<u8> {
(index as u8).encode_to_vec()
}
pub fn receipt_key(block_hash: &BlockHash, index: u64) -> Vec<u8> {
let mut key = Vec::with_capacity(40);
key.extend_from_slice(block_hash.as_bytes());
key.extend_from_slice(&index.to_be_bytes());
key
}
pub fn encode_code(code: &Code) -> Vec<u8> {
let jumpdests = code.jumpdests();
let mut buf = Vec::with_capacity(6 + code.len() + jumpdests.len());
code.code().encode(&mut buf);
jumpdests.encode(&mut buf);
buf
}
fn decode_jumpdests(code: &[u8], encoded: &[u8]) -> Result<(Arc<[u8]>, bool), StoreError> {
let (is_list, payload, _) = decode_rlp_item(encoded)?;
if is_list {
return Ok((Code::compute_jumpdests(code), true));
}
Ok((Arc::from(payload), false))
}
#[derive(Debug, Default, Clone)]
struct LatestBlockHeaderCache {
current: Arc<Mutex<Arc<BlockHeader>>>,
}
impl LatestBlockHeaderCache {
pub fn get(&self) -> Arc<BlockHeader> {
self.current.lock().expect("poisoned mutex").clone()
}
pub fn update(&self, header: BlockHeader) {
let new = Arc::new(header);
*self.current.lock().expect("poisoned mutex") = new;
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct StoreMetadata {
pub schema_version: u64,
}
impl StoreMetadata {
pub fn new(schema_version: u64) -> Self {
Self { schema_version }
}
}
fn read_store_schema_version(path: &Path) -> Result<Option<u64>, StoreError> {
let metadata_path = path.join(STORE_METADATA_FILENAME);
if !metadata_path.exists() {
return Ok(None);
}
if !metadata_path.is_file() {
return Err(StoreError::Custom(
"store schema path exists but is not a file".to_string(),
));
}
let file_contents = std::fs::read_to_string(metadata_path)?;
let metadata: StoreMetadata = serde_json::from_str(&file_contents)?;
Ok(Some(metadata.schema_version))
}
fn init_metadata_file(parent_path: &Path) -> Result<(), StoreError> {
std::fs::create_dir_all(parent_path)?;
let metadata_path = parent_path.join(STORE_METADATA_FILENAME);
let metadata = StoreMetadata::new(STORE_SCHEMA_VERSION);
let serialized_metadata = serde_json::to_string_pretty(&metadata)?;
let mut new_file = std::fs::File::create_new(metadata_path)?;
new_file.write_all(serialized_metadata.as_bytes())?;
Ok(())
}
fn dir_contains_legacy_db(path: &Path) -> Result<bool, StoreError> {
if path.join("CURRENT").is_file() {
return Ok(true);
}
for entry in std::fs::read_dir(path)? {
let entry = entry?;
if !entry.file_type()?.is_file() {
continue;
}
if entry.file_name().to_string_lossy().starts_with("MANIFEST-") {
return Ok(true);
}
}
Ok(false)
}
pub fn has_valid_db(path: &Path) -> bool {
let metadata_path = path.join(STORE_METADATA_FILENAME);
if !metadata_path.is_file() {
return false;
}
let Ok(contents) = std::fs::read_to_string(&metadata_path) else {
return false;
};
let Ok(metadata) = serde_json::from_str::<StoreMetadata>(&contents) else {
return false;
};
metadata.schema_version >= 1 && metadata.schema_version <= STORE_SCHEMA_VERSION
}
pub fn read_chain_id_from_db(path: &Path) -> Option<u64> {
if !has_valid_db(path) {
return None;
}
#[cfg(feature = "rocksdb")]
{
let backend = match RocksDBBackend::open(path, MAX_ROCKSDB_BLOCK_CACHE_SIZE_BYTES) {
Ok(backend) => backend,
Err(e) => {
warn!("Failed to open RocksDB at {path:?} to read chain ID: {e}");
return None;
}
};
let read = match backend.begin_read() {
Ok(read) => read,
Err(e) => {
warn!("Failed to begin read transaction at {path:?}: {e}");
return None;
}
};
let key = chain_data_key(ChainDataIndex::ChainConfig);
let bytes = match read.get(CHAIN_DATA, &key) {
Ok(Some(bytes)) => bytes,
Ok(None) => {
warn!("Chain config entry not found in database at {path:?}");
return None;
}
Err(e) => {
warn!("Failed to read chain config from database at {path:?}: {e}");
return None;
}
};
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct ChainIdOnly {
chain_id: u64,
}
match serde_json::from_slice::<ChainIdOnly>(&bytes) {
Ok(partial) => Some(partial.chain_id),
Err(e) => {
warn!("Failed to deserialize chain ID from database at {path:?}: {e}");
None
}
}
}
#[cfg(not(feature = "rocksdb"))]
{
let _ = path;
None
}
}
#[cfg(test)]
mod account_code_tests {
use super::*;
const JUMPDEST: u8 = 0x5b;
const PUSH1: u8 = 0x60;
#[test]
fn metadata_falls_back_to_the_bytecode_without_a_tokio_runtime() {
use crate::backend::in_memory::InMemoryBackend;
let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
let dir = tempfile::tempdir().unwrap();
let store = Store::from_backend(
backend.clone(),
dir.path().to_path_buf(),
1,
DEFAULT_PERSIST_CHANNEL_CAPACITY,
)
.unwrap();
let code = jumpdest_dense_code();
let mut tx = backend.begin_write().unwrap();
tx.put(ACCOUNT_CODES, code.hash.as_bytes(), &encode_code(&code))
.unwrap();
tx.commit().unwrap();
let metadata = store
.get_code_metadata(code.hash)
.expect("metadata read must not fail off-runtime");
assert_eq!(metadata.map(|m| m.length), Some(code.len() as u64));
}
fn jumpdest_dense_code() -> Code {
let mut bytecode = vec![JUMPDEST; 24576];
bytecode[0] = 0x00;
Code::from_bytecode_unchecked(bytecode.into(), H256::zero())
}
fn encode_code_legacy(code: &Code) -> Vec<u8> {
let offsets: Vec<u32> = (0..code.len())
.filter(|offset| code.is_valid_jumpdest(*offset))
.map(|offset| offset as u32)
.collect();
let mut buf = Vec::new();
code.code().encode(&mut buf);
offsets.encode(&mut buf);
buf
}
#[test]
fn encoded_value_carries_one_bitmap_bit_per_code_byte() {
let code = jumpdest_dense_code();
let encoded = encode_code(&code);
assert_eq!(encoded.len(), 6 + code.len() + code.len().div_ceil(8));
}
#[test]
fn round_trip_preserves_the_bitmap() {
let code = jumpdest_dense_code();
let encoded = encode_code(&code);
let (bytecode, jumpdests) = decode_bytes(&encoded).unwrap();
assert_eq!(bytecode, code.code());
let (rebuilt, was_legacy) = decode_jumpdests(bytecode, jumpdests).unwrap();
assert_eq!(rebuilt.as_ref(), code.jumpdests());
assert!(!was_legacy, "bitmap format must not be flagged legacy");
}
#[test]
fn legacy_offset_list_values_decode_to_the_same_bitmap() {
for (bytecode, offsets) in [
(vec![0x00, JUMPDEST, 0x00, JUMPDEST], vec![1u32, 3]),
(vec![PUSH1, JUMPDEST, JUMPDEST], vec![2u32]),
(vec![0x00; 32], vec![]),
] {
let code = Code::from_bytecode_unchecked(bytecode.clone().into(), H256::zero());
let legacy = encode_code_legacy(&code);
let mut expected = vec![0u8; bytecode.len().div_ceil(8)];
for offset in &offsets {
let index = usize::try_from(*offset).expect("offset fits usize");
expected[index / 8] |= 1 << (index % 8);
}
if offsets.is_empty() {
expected.clear();
}
let (decoded_bytecode, jumpdests) = decode_bytes(&legacy).unwrap();
assert_eq!(decoded_bytecode, code.code());
let (rebuilt, was_legacy) = decode_jumpdests(decoded_bytecode, jumpdests).unwrap();
assert_eq!(
rebuilt.as_ref(),
expected.as_slice(),
"rebuilt bitmap disagrees with the legacy offsets for {bytecode:?}"
);
assert!(
was_legacy,
"legacy RLP-list format must be flagged for self-heal"
);
}
}
#[test]
fn repeated_inserts_do_not_inflate_the_accounted_size() {
let code = jumpdest_dense_code();
let mut cache = CodeCache::default();
cache.insert(&code).unwrap();
let after_first = cache.cache_size;
for _ in 0..8 {
cache.insert(&code).unwrap();
}
assert_eq!(cache.cache_size, after_first);
assert_eq!(cache.entries.len(), 1);
}
#[test]
fn accounted_size_covers_the_bytecode_and_bitmap() {
let code = jumpdest_dense_code();
let mut cache = CodeCache::default();
cache.insert(&code).unwrap();
assert!(cache.cache_size >= (code.len() + code.jumpdests().len()) as u64);
}
const TEST_RNG_SEED: u64 = 0x9E37_79B9_7F4A_7C15;
#[test]
fn cache_evicts_down_to_its_budget() {
let mut cache = CodeCache {
max_size: 128 * 1024,
rng: TEST_RNG_SEED,
..Default::default()
};
for i in 0..16u8 {
let mut bytecode = vec![JUMPDEST; 24576];
bytecode[0] = i;
cache
.insert(&Code::from_bytecode_unchecked(
bytecode.into(),
H256::from_low_u64_be(i.into()),
))
.unwrap();
}
assert!(cache.cache_size <= cache.max_size);
assert!(cache.entries.len() < 16);
}
fn numbered_code(i: u64, len: usize) -> Code {
let mut bytecode = vec![JUMPDEST; len];
bytecode[..8].copy_from_slice(&i.to_be_bytes());
Code::from_bytecode_unchecked(bytecode.into(), H256::from_low_u64_be(i))
}
#[test]
fn a_cyclic_scan_larger_than_the_budget_still_hits() {
let entry_size = CodeCache::entry_size(&numbered_code(0, 4096));
let mut cache = CodeCache {
max_size: 400 * entry_size,
rng: TEST_RNG_SEED,
..Default::default()
};
let codes: Vec<Code> = (0..500).map(|i| numbered_code(i, 4096)).collect();
let (mut hits, mut reads) = (0u64, 0u64);
for round in 0..20 {
for code in &codes {
let cached = cache.get(&code.hash).unwrap();
if round >= 5 {
reads += 1;
hits += u64::from(cached.is_some());
}
if cached.is_none() {
cache.insert(code).unwrap();
}
}
}
assert!(cache.cache_size <= cache.max_size);
assert!(
hits * 2 > reads,
"only {hits} of {reads} reads hit once the scan outgrew the budget"
);
}
#[test]
fn eviction_keeps_the_entry_being_inserted() {
let entry_size = CodeCache::entry_size(&numbered_code(0, 4096));
let mut cache = CodeCache {
max_size: 2 * entry_size,
rng: TEST_RNG_SEED,
..Default::default()
};
for i in 0..64 {
let code = numbered_code(i, 4096);
cache.insert(&code).unwrap();
assert!(cache.get(&code.hash).unwrap().is_some());
assert!(cache.cache_size <= cache.max_size);
}
}
#[test]
fn an_entry_larger_than_the_budget_is_not_cached() {
let small = numbered_code(0, 1024);
let mut cache = CodeCache {
max_size: 4 * CodeCache::entry_size(&small),
rng: TEST_RNG_SEED,
..Default::default()
};
cache.insert(&small).unwrap();
let large = numbered_code(1, 8192);
assert!(CodeCache::entry_size(&large) > cache.max_size);
cache.insert(&large).unwrap();
assert!(!cache.contains(&large.hash));
assert!(cache.contains(&small.hash));
assert_eq!(cache.cache_size, CodeCache::entry_size(&small));
}
#[test]
fn debug_output_leaves_out_the_cached_code() {
let mut cache = CodeCache::default();
cache.insert(&numbered_code(7, 4096)).unwrap();
let printed = format!("{cache:?}");
assert!(printed.len() < 200, "{printed}");
}
#[test]
fn small_contracts_are_charged_their_entry_overhead() {
let mut cache = CodeCache {
max_size: 160 * 1024,
rng: TEST_RNG_SEED,
..Default::default()
};
let mut charged = 0;
for i in 0..4096u64 {
let mut bytecode = vec![JUMPDEST; 32];
bytecode[..8].copy_from_slice(&i.to_be_bytes());
let code = Code::from_bytecode_unchecked(bytecode.into(), H256::from_low_u64_be(i));
charged = code.size() as u64 + CODE_CACHE_ENTRY_OVERHEAD;
cache.insert(&code).unwrap();
}
assert!(cache.cache_size <= cache.max_size);
assert_eq!(cache.entries.len() as u64, cache.max_size / charged);
}
#[test]
fn default_cache_uses_the_configured_budget() {
assert_eq!(CodeCache::default().max_size, CODE_CACHE_MAX_SIZE);
}
#[test]
fn store_uses_the_configured_code_cache_budget() {
let config = StoreConfig {
code_cache_size: Some(300 * 1024 * 1024),
..StoreConfig::with_rocksdb_block_cache_size(MIN_ROCKSDB_BLOCK_CACHE_SIZE_BYTES)
};
let store = Store::new_with_config("", EngineType::InMemory, config).unwrap();
assert_eq!(store.code_cache_budget_bytes(), 300 * 1024 * 1024);
let config = StoreConfig::with_rocksdb_block_cache_size(MIN_ROCKSDB_BLOCK_CACHE_SIZE_BYTES);
let store = Store::new_with_config("", EngineType::InMemory, config).unwrap();
assert_eq!(store.code_cache_budget_bytes(), CODE_CACHE_MAX_SIZE);
}
}
#[cfg(test)]
mod state_history_tests {
use super::*;
use crate::api::tables::STATE_HISTORY;
use crate::backend::in_memory::InMemoryBackend;
use crate::journal::JournalEntry;
use ethrex_common::types::{BlockBody, BlockHeader};
use ethrex_trie::Nibbles;
use std::time::{Duration, Instant};
fn make_block(number: BlockNumber, parent_hash: H256, state_root: H256) -> Block {
let header = BlockHeader {
number,
parent_hash,
state_root,
..Default::default()
};
Block::new(header, BlockBody::default())
}
fn await_journal_entry(
backend: &Arc<dyn StorageBackend>,
block_number: BlockNumber,
timeout: Duration,
) -> Option<Vec<u8>> {
let key = block_number.to_be_bytes();
let deadline = Instant::now() + timeout;
loop {
let read = backend.begin_read().ok()?;
if let Ok(Some(v)) = read.get(STATE_HISTORY, &key) {
return Some(v);
}
if Instant::now() >= deadline {
return None;
}
std::thread::sleep(Duration::from_millis(10));
}
}
fn assert_no_journal_entry(backend: &Arc<dyn StorageBackend>, block_number: BlockNumber) {
let window = Duration::from_millis(500);
let key = block_number.to_be_bytes();
let deadline = Instant::now() + window;
loop {
let read = backend.begin_read().expect("read view");
let v = read.get(STATE_HISTORY, &key).expect("get");
assert!(
v.is_none(),
"expected no STATE_HISTORY entry for block {block_number}, got {v:?}"
);
if Instant::now() >= deadline {
return;
}
std::thread::sleep(Duration::from_millis(20));
}
}
#[test]
fn journal_entry_written_per_block_in_regular_mode() {
let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
let dir = tempfile::tempdir().unwrap();
let store = Store::from_backend(
backend.clone(),
dir.path().to_path_buf(),
1,
DEFAULT_PERSIST_CHANNEL_CAPACITY,
)
.unwrap();
let state_root_1 = H256::repeat_byte(0x11);
let block1 = make_block(1, H256::zero(), state_root_1);
let block1_hash = block1.hash();
store
.store_block_updates(UpdateBatch {
account_updates: vec![(Nibbles::from_raw(&[0x00, 0x01], false), vec![0xab, 0xcd])],
storage_updates: vec![],
blocks: vec![block1],
receipts: vec![],
code_updates: vec![],
commit_depth: None,
wait_for_flush: false,
})
.unwrap();
store.set_safe_commit_root(state_root_1).unwrap();
let state_root_2 = H256::repeat_byte(0x22);
let block2 = make_block(2, block1_hash, state_root_2);
let block2_hash = block2.hash();
store
.store_block_updates(UpdateBatch {
account_updates: vec![(Nibbles::from_raw(&[0x00, 0x02], false), vec![0xef, 0x11])],
storage_updates: vec![],
blocks: vec![block2],
receipts: vec![],
code_updates: vec![],
commit_depth: None,
wait_for_flush: false,
})
.unwrap();
let bytes = await_journal_entry(&backend, 1, Duration::from_secs(2))
.expect("STATE_HISTORY entry for block 1 should appear after block 2 commits it");
let entry = JournalEntry::decode(&bytes).unwrap();
assert_eq!(entry.block_hash, block1_hash);
assert_eq!(entry.parent_state_root, H256::zero());
assert!(!entry.account_trie_diff.is_empty());
let (path, prev) = &entry.account_trie_diff[0];
assert_eq!(prev, &None, "first-time write means previous value is None");
assert!(path.len() < 65);
store.set_safe_commit_root(state_root_2).unwrap();
let state_root_3 = H256::repeat_byte(0x33);
let block3 = make_block(3, block2_hash, state_root_3);
store
.store_block_updates(UpdateBatch {
account_updates: vec![(Nibbles::from_raw(&[0x00, 0x03], false), vec![0x77])],
storage_updates: vec![],
blocks: vec![block3],
receipts: vec![],
code_updates: vec![],
commit_depth: None,
wait_for_flush: false,
})
.unwrap();
let bytes = await_journal_entry(&backend, 2, Duration::from_secs(2))
.expect("STATE_HISTORY entry for block 2 should appear after block 3 commits it");
let entry = JournalEntry::decode(&bytes).unwrap();
assert_eq!(entry.block_hash, block2_hash);
assert_eq!(entry.parent_state_root, state_root_1);
assert!(!entry.account_trie_diff.is_empty());
}
#[test]
fn depth_gated_per_block_commit_journals_with_correct_pre_image() {
let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
let dir = tempfile::tempdir().unwrap();
let store = Store::from_backend(
backend.clone(),
dir.path().to_path_buf(),
1,
DEFAULT_PERSIST_CHANNEL_CAPACITY,
)
.unwrap();
let shared = Nibbles::from_raw(&[0x0a, 0x0b], false);
let mut prev_hash = H256::zero();
let mut hashes = vec![H256::zero()];
let mut roots = vec![H256::zero()];
for n in 1..=6u64 {
let state_root = H256::repeat_byte(0xc0 | (n as u8));
let block = make_block(n, prev_hash, state_root);
prev_hash = block.hash();
hashes.push(prev_hash);
roots.push(state_root);
store
.store_block_updates(UpdateBatch {
account_updates: vec![(shared.clone(), vec![0xb0 | (n as u8)])],
storage_updates: vec![],
blocks: vec![block],
receipts: vec![],
code_updates: vec![],
commit_depth: Some(3),
wait_for_flush: false,
})
.unwrap();
}
let bytes = await_journal_entry(&backend, 1, Duration::from_secs(2))
.expect("a depth-gated per-block commit must journal");
let entry = JournalEntry::decode(&bytes).unwrap();
assert_eq!(
entry.block_hash, hashes[1],
"entry 1 carries block 1's identity"
);
assert_eq!(entry.parent_state_root, H256::zero());
assert_eq!(
entry.account_trie_diff[0].1, None,
"block 1 first-writes the key, so its pre-image is absence"
);
let bytes =
await_journal_entry(&backend, 2, Duration::from_secs(2)).expect("entry for block 2");
let entry = JournalEntry::decode(&bytes).unwrap();
assert_eq!(
entry.block_hash, hashes[2],
"entry 2 carries block 2's identity"
);
assert_eq!(
entry.parent_state_root, roots[1],
"entry 2's parent_state_root is block 1's state root"
);
assert_eq!(
entry.account_trie_diff[0].1,
Some(vec![0xb0 | 1u8]),
"block 2's pre-image is the value block 1 wrote"
);
}
#[test]
fn journal_skipped_in_batch_mode() {
let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
let dir = tempfile::tempdir().unwrap();
let store = Store::from_backend(
backend.clone(),
dir.path().to_path_buf(),
1,
DEFAULT_PERSIST_CHANNEL_CAPACITY,
)
.unwrap();
let mut prev_hash = H256::zero();
for n in 1..=5u64 {
let state_root = H256::repeat_byte(0xa0 | (n as u8));
let block = make_block(n, prev_hash, state_root);
prev_hash = block.hash();
store
.store_block_updates(UpdateBatch {
account_updates: vec![(
Nibbles::from_raw(&[n as u8], false),
vec![0xde, 0xad, n as u8],
)],
storage_updates: vec![],
blocks: vec![block],
receipts: vec![],
code_updates: vec![],
commit_depth: Some(BATCH_COMMIT_THRESHOLD),
wait_for_flush: true,
})
.unwrap();
}
for n in 1..=5u64 {
assert_no_journal_entry(&backend, n);
}
}
#[test]
fn journal_storage_updates_appear_in_storage_diff() {
let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
let dir = tempfile::tempdir().unwrap();
let store = Store::from_backend(
backend.clone(),
dir.path().to_path_buf(),
1,
DEFAULT_PERSIST_CHANNEL_CAPACITY,
)
.unwrap();
let account_hash_a = H256::repeat_byte(0xa0);
let account_hash_b = H256::repeat_byte(0xb0);
let state_root_1 = H256::repeat_byte(0x33);
let block1 = make_block(1, H256::zero(), state_root_1);
let block1_hash = block1.hash();
store
.store_block_updates(UpdateBatch {
account_updates: vec![],
storage_updates: vec![
(
account_hash_a,
vec![(Nibbles::from_raw(&[0x05], false), vec![0x01])],
),
(
account_hash_b,
vec![(Nibbles::from_raw(&[0x06], false), vec![0x02])],
),
],
blocks: vec![block1],
receipts: vec![],
code_updates: vec![],
commit_depth: None,
wait_for_flush: false,
})
.unwrap();
store.set_safe_commit_root(state_root_1).unwrap();
let state_root_2 = H256::repeat_byte(0x44);
let block2 = make_block(2, block1_hash, state_root_2);
store
.store_block_updates(UpdateBatch {
account_updates: vec![(Nibbles::from_raw(&[0xee], false), vec![0xff])],
storage_updates: vec![],
blocks: vec![block2],
receipts: vec![],
code_updates: vec![],
commit_depth: None,
wait_for_flush: false,
})
.unwrap();
let bytes = await_journal_entry(&backend, 1, Duration::from_secs(2))
.expect("STATE_HISTORY entry for block 1");
let entry = JournalEntry::decode(&bytes).unwrap();
assert_eq!(entry.block_hash, block1_hash);
assert_eq!(
entry.storage_trie_diff.len(),
2,
"two distinct account hashes must produce two storage_trie entries"
);
for (_key, prev) in &entry.storage_trie_diff {
assert_eq!(prev, &None, "first-time storage write has None pre-image");
}
assert!(entry.account_trie_diff.is_empty());
}
fn seed_journal_entries(backend: &Arc<dyn StorageBackend>, block_numbers: &[BlockNumber]) {
let mut tx = backend.begin_write().unwrap();
for n in block_numbers {
let entry = JournalEntry {
block_hash: H256::repeat_byte(*n as u8),
parent_state_root: H256::zero(),
account_trie_diff: vec![(vec![*n as u8], None)],
storage_trie_diff: vec![],
account_flat_diff: vec![],
storage_flat_diff: vec![],
};
tx.put(STATE_HISTORY, &n.to_be_bytes(), &entry.encode())
.unwrap();
}
tx.commit().unwrap();
}
fn journal_entry_exists(backend: &Arc<dyn StorageBackend>, block_number: BlockNumber) -> bool {
backend
.begin_read()
.unwrap()
.get(STATE_HISTORY, &block_number.to_be_bytes())
.unwrap()
.is_some()
}
#[tokio::test]
async fn finality_advance_prunes_journal_below_boundary() {
let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
let dir = tempfile::tempdir().unwrap();
let store = Store::from_backend(
backend.clone(),
dir.path().to_path_buf(),
1,
DEFAULT_PERSIST_CHANNEL_CAPACITY,
)
.unwrap();
seed_journal_entries(&backend, &(1..=10).collect::<Vec<_>>());
for n in 1..=10 {
assert!(journal_entry_exists(&backend, n), "seed entry {n} present");
}
store
.forkchoice_update_inner(vec![], 100, H256::zero(), None, Some(5))
.await
.unwrap();
for n in 1..=5 {
assert!(
!journal_entry_exists(&backend, n),
"entry {n} should have been pruned (<= finalized)"
);
}
for n in 6..=10 {
assert!(
journal_entry_exists(&backend, n),
"entry {n} should remain (> finalized)"
);
}
}
#[tokio::test]
async fn finality_no_op_does_not_prune() {
let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
let dir = tempfile::tempdir().unwrap();
let store = Store::from_backend(
backend.clone(),
dir.path().to_path_buf(),
1,
DEFAULT_PERSIST_CHANNEL_CAPACITY,
)
.unwrap();
store
.forkchoice_update_inner(vec![], 100, H256::zero(), None, Some(5))
.await
.unwrap();
seed_journal_entries(&backend, &(6..=10).collect::<Vec<_>>());
store
.forkchoice_update_inner(vec![], 100, H256::zero(), None, Some(5))
.await
.unwrap();
for n in 6..=10 {
assert!(
journal_entry_exists(&backend, n),
"entry {n} should still exist after no-op finality update"
);
}
store
.forkchoice_update_inner(vec![], 100, H256::zero(), None, None)
.await
.unwrap();
for n in 6..=10 {
assert!(
journal_entry_exists(&backend, n),
"entry {n} should still exist when finalized is None"
);
}
}
#[tokio::test]
async fn malformed_finalized_returns_error_not_silent_zero() {
let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
let dir = tempfile::tempdir().unwrap();
let store = Store::from_backend(
backend.clone(),
dir.path().to_path_buf(),
1,
DEFAULT_PERSIST_CHANNEL_CAPACITY,
)
.unwrap();
let mut tx = backend.begin_write().unwrap();
let finalized_key = chain_data_key(ChainDataIndex::FinalizedBlockNumber);
tx.put(CHAIN_DATA, &finalized_key, &[0u8, 0, 0, 0]).unwrap();
tx.commit().unwrap();
seed_journal_entries(&backend, &(1..=5).collect::<Vec<_>>());
let err = store
.forkchoice_update_inner(vec![], 100, H256::zero(), None, Some(3))
.await
.expect_err("malformed finalized must surface as an error");
let msg = format!("{err}");
assert!(
msg.contains("FinalizedBlockNumber has unexpected length"),
"expected length-mismatch error, got: {msg}"
);
for n in 1..=5 {
assert!(
journal_entry_exists(&backend, n),
"entry {n} must NOT be pruned when finalized read failed"
);
}
}
#[tokio::test]
async fn highest_state_history_block_number_finds_max() {
let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
let dir = tempfile::tempdir().unwrap();
let store = Store::from_backend(
backend.clone(),
dir.path().to_path_buf(),
1,
DEFAULT_PERSIST_CHANNEL_CAPACITY,
)
.unwrap();
assert_eq!(store.highest_state_history_block_number().unwrap(), None);
seed_journal_entries(&backend, &[3, 7, 5, 11, 8]);
assert_eq!(
store.highest_state_history_block_number().unwrap(),
Some(11),
"max over present entries"
);
}
#[tokio::test]
async fn journaled_block_hash_reads_back() {
let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
let dir = tempfile::tempdir().unwrap();
let store = Store::from_backend(
backend.clone(),
dir.path().to_path_buf(),
1,
DEFAULT_PERSIST_CHANNEL_CAPACITY,
)
.unwrap();
seed_journal_entries(&backend, &[3, 11]);
assert_eq!(
store.get_state_history_block_hash(11).unwrap(),
Some(H256::repeat_byte(11))
);
assert_eq!(store.get_state_history_block_hash(4).unwrap(), None);
}
#[tokio::test]
async fn advance_flushed_upto_only_moves_forward() {
let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
let dir = tempfile::tempdir().unwrap();
let store = Store::from_backend(
backend,
dir.path().to_path_buf(),
1,
DEFAULT_PERSIST_CHANNEL_CAPACITY,
)
.unwrap();
store.advance_flushed_upto(10).unwrap();
assert_eq!(store.read_flushed_upto().unwrap(), 10);
store.advance_flushed_upto(5).unwrap();
assert_eq!(store.read_flushed_upto().unwrap(), 10);
store.advance_flushed_upto(12).unwrap();
assert_eq!(store.read_flushed_upto().unwrap(), 12);
}
#[tokio::test]
async fn lowest_state_history_block_number_finds_min() {
let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
let dir = tempfile::tempdir().unwrap();
let store = Store::from_backend(
backend.clone(),
dir.path().to_path_buf(),
1,
DEFAULT_PERSIST_CHANNEL_CAPACITY,
)
.unwrap();
assert_eq!(store.lowest_state_history_block_number().unwrap(), None);
seed_journal_entries(&backend, &[7, 3, 11, 5, 8]);
assert_eq!(
store.lowest_state_history_block_number().unwrap(),
Some(3),
"min over present entries"
);
}
#[tokio::test]
async fn install_overlay_replaces_cache_with_fresh_one() {
let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
let dir = tempfile::tempdir().unwrap();
let store = Store::from_backend(
backend.clone(),
dir.path().to_path_buf(),
4,
DEFAULT_PERSIST_CHANNEL_CAPACITY,
)
.unwrap();
seed_journal_entries(&backend, &[3, 4]);
{
let mut guard = store.trie_cache.write().unwrap();
let mut updated = (**guard).clone();
updated.put_batch(
H256::zero(),
H256::repeat_byte(0xff),
42,
H256::repeat_byte(0x42),
vec![],
);
*guard = Arc::new(updated);
}
assert!(
store
.is_state_in_layer_cache(H256::repeat_byte(0xff))
.unwrap()
);
store
.install_overlay_for_reorg(4, 3, |_| None)
.expect("install must succeed");
assert!(
!store
.is_state_in_layer_cache(H256::repeat_byte(0xff))
.unwrap(),
"old cache layer must be gone after swap"
);
let cache = store.trie_cache.read().unwrap().clone();
assert!(cache.overlay().is_some(), "overlay must be installed");
assert_eq!(
cache.commit_threshold(),
4,
"fresh cache must inherit threshold"
);
}
#[tokio::test]
async fn install_overlay_failure_preserves_cache() {
let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
let dir = tempfile::tempdir().unwrap();
let store = Store::from_backend(
backend.clone(),
dir.path().to_path_buf(),
4,
DEFAULT_PERSIST_CHANNEL_CAPACITY,
)
.unwrap();
seed_journal_entries(&backend, &[5]);
{
let mut guard = store.trie_cache.write().unwrap();
let mut updated = (**guard).clone();
updated.put_batch(
H256::zero(),
H256::repeat_byte(0xaa),
7,
H256::repeat_byte(0x77),
vec![],
);
*guard = Arc::new(updated);
}
assert!(
store
.is_state_in_layer_cache(H256::repeat_byte(0xaa))
.unwrap()
);
let err = store
.install_overlay_for_reorg(5, 3, |_| None)
.expect_err("missing entries 3 and 4 must abort");
let msg = format!("{err}");
assert!(
msg.contains("overlay construction failed"),
"error should explain construction failure: {msg}"
);
assert!(
store
.is_state_in_layer_cache(H256::repeat_byte(0xaa))
.unwrap(),
"cache must survive a failed overlay install"
);
let cache = store.trie_cache.read().unwrap().clone();
assert!(cache.overlay().is_none(), "no overlay should be installed");
}
#[tokio::test]
async fn abort_reorg_resets_cache_to_fresh() {
let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
let dir = tempfile::tempdir().unwrap();
let store = Store::from_backend(
backend.clone(),
dir.path().to_path_buf(),
4,
DEFAULT_PERSIST_CHANNEL_CAPACITY,
)
.unwrap();
seed_journal_entries(&backend, &[3, 4]);
store.install_overlay_for_reorg(4, 3, |_| None).unwrap();
{
let mut guard = store.trie_cache.write().unwrap();
let mut updated = (**guard).clone();
updated.put_batch(
H256::zero(),
H256::repeat_byte(0xbb),
4,
H256::repeat_byte(0xb4),
vec![],
);
*guard = Arc::new(updated);
}
assert!(
store
.is_state_in_layer_cache(H256::repeat_byte(0xbb))
.unwrap()
);
store.abort_reorg().unwrap();
assert!(
!store
.is_state_in_layer_cache(H256::repeat_byte(0xbb))
.unwrap(),
"partial new-chain layer must be discarded"
);
let cache = store.trie_cache.read().unwrap().clone();
assert!(cache.overlay().is_none(), "overlay must be cleared");
assert_eq!(
cache.commit_threshold(),
4,
"fresh cache must inherit threshold"
);
}
#[tokio::test]
async fn clear_reorg_overlay_removes_overlay_and_is_idempotent() {
let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
let dir = tempfile::tempdir().unwrap();
let store = Store::from_backend(
backend.clone(),
dir.path().to_path_buf(),
4,
DEFAULT_PERSIST_CHANNEL_CAPACITY,
)
.unwrap();
store.clear_reorg_overlay().unwrap();
assert!(store.trie_cache.read().unwrap().overlay().is_none());
seed_journal_entries(&backend, &[7]);
store.install_overlay_for_reorg(7, 7, |_| None).unwrap();
assert!(store.trie_cache.read().unwrap().overlay().is_some());
store.clear_reorg_overlay().unwrap();
assert!(store.trie_cache.read().unwrap().overlay().is_none());
store.clear_reorg_overlay().unwrap();
assert!(store.trie_cache.read().unwrap().overlay().is_none());
}
#[tokio::test]
async fn deep_reorg_overlay_disables_flat_kv_fast_paths() {
let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
{
let mut tx = backend.begin_write().unwrap();
tx.put(MISC_VALUES, "last_written".as_bytes(), &[0xff])
.unwrap();
tx.commit().unwrap();
}
let dir = tempfile::tempdir().unwrap();
let store = Store::from_backend(
backend.clone(),
dir.path().to_path_buf(),
1,
DEFAULT_PERSIST_CHANNEL_CAPACITY,
)
.unwrap();
let address = Address::from_low_u64_be(0xbeef);
let account_hash = hash_address_fixed(&address);
let slot = H256::from_low_u64_be(1);
let slot_hash = hash_key_fixed(&slot);
let generator_account = AccountState {
nonce: 7,
balance: U256::from(999u64),
storage_root: H256::zero(),
code_hash: H256::zero(),
};
let generator_slot_value = U256::from(42u64);
{
let mut tx = backend.begin_write().unwrap();
tx.put(
ACCOUNT_FLATKEYVALUE,
&Nibbles::from_bytes(account_hash.as_bytes()).into_vec(),
&generator_account.encode_to_vec(),
)
.unwrap();
tx.put(
STORAGE_FLATKEYVALUE,
&apply_prefix(Some(account_hash), Nibbles::from_bytes(&slot_hash)).into_vec(),
&generator_slot_value.encode_to_vec(),
)
.unwrap();
tx.commit().unwrap();
}
let pivot_root = EMPTY_TRIE_HASH;
{
let mut tx = backend.begin_write().unwrap();
for (n, parent_root) in [(10u64, pivot_root), (11, H256::repeat_byte(0x10))] {
let entry = JournalEntry {
block_hash: H256::repeat_byte(n as u8),
parent_state_root: parent_root,
account_trie_diff: vec![],
storage_trie_diff: vec![],
account_flat_diff: vec![],
storage_flat_diff: vec![],
};
tx.put(STATE_HISTORY, &n.to_be_bytes(), &entry.encode())
.unwrap();
}
tx.commit().unwrap();
}
store
.install_overlay_for_reorg(11, 10, |_| None)
.expect("overlay install must succeed");
let accounts = store
.get_account_states_batch_by_root(pivot_root, &[address])
.unwrap();
assert_eq!(
accounts,
vec![None],
"batch account read must not serve the generator's stale flat-KV value"
);
let slot_value = store
.get_storage_at_root(pivot_root, address, slot)
.unwrap();
assert_eq!(
slot_value, None,
"storage read must not serve the generator's stale flat-KV value"
);
let batched_slots = store
.get_storage_values_batch_by_root(
pivot_root,
&[(hash_address_fixed(&address), EMPTY_TRIE_HASH, slot)],
)
.unwrap();
assert_eq!(
batched_slots,
vec![None],
"batch storage read must not serve the generator's stale flat-KV value"
);
let state_trie = store.open_state_trie(pivot_root).unwrap();
assert_eq!(
state_trie.get(account_hash.as_bytes()).unwrap(),
None,
"trie read must not serve the generator's stale flat-KV value"
);
let unserved_root = H256::repeat_byte(0x99);
let accounts = store
.get_account_states_batch_by_root(unserved_root, &[address])
.unwrap();
assert_eq!(
accounts,
vec![Some(generator_account)],
"unserved roots must keep the flat-KV fast path"
);
let slot_value = store
.get_storage_at_root(unserved_root, address, slot)
.unwrap();
assert_eq!(
slot_value,
Some(generator_slot_value),
"unserved roots must keep the flat-KV fast path"
);
let batched_slots = store
.get_storage_values_batch_by_root(
unserved_root,
&[(hash_address_fixed(&address), EMPTY_TRIE_HASH, slot)],
)
.unwrap();
assert_eq!(
batched_slots,
vec![Some(generator_slot_value)],
"unserved roots must keep the flat-KV fast path"
);
}
#[tokio::test]
async fn overlay_backed_commit_only_commits_bottom_layer_per_pass() {
let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
let dir = tempfile::tempdir().unwrap();
let store = Store::from_backend(
backend.clone(),
dir.path().to_path_buf(),
4,
DEFAULT_PERSIST_CHANNEL_CAPACITY,
)
.unwrap();
let pivot_root = H256::repeat_byte(0x09);
{
let mut tx = backend.begin_write().unwrap();
for (n, parent_root) in [(10u64, pivot_root), (11, H256::repeat_byte(0x0a))] {
let entry = JournalEntry {
block_hash: H256::repeat_byte(n as u8),
parent_state_root: parent_root,
account_trie_diff: vec![(vec![0x00, n as u8], None)],
storage_trie_diff: vec![],
account_flat_diff: vec![],
storage_flat_diff: vec![],
};
tx.put(STATE_HISTORY, &n.to_be_bytes(), &entry.encode())
.unwrap();
}
tx.commit().unwrap();
}
store.install_overlay_for_reorg(11, 10, |_| None).unwrap();
let roots: Vec<H256> = (10..=12u8).map(H256::repeat_byte).collect();
{
let mut guard = store.trie_cache.write().unwrap();
let mut updated = (**guard).clone();
let mut parent = pivot_root;
for (i, root) in roots.iter().enumerate() {
let n = 10 + i as u64;
updated.put_batch(
parent,
*root,
n,
H256::repeat_byte(n as u8),
vec![(Nibbles::from_raw(&[0x01, n as u8], false), vec![n as u8])],
);
parent = *root;
}
*guard = Arc::new(updated);
}
let trie = store.trie_cache.read().unwrap().clone();
commit_to_disk(
store.backend.as_ref(),
&store.flatkeyvalue_control_tx,
&store.trie_cache,
&trie,
roots[2],
false,
)
.expect("bottom-layer commit must succeed (no debug_assert panic)");
assert!(
!store.is_state_in_layer_cache(roots[0]).unwrap(),
"bottom layer must be committed and pruned"
);
assert!(
store.is_state_in_layer_cache(roots[1]).unwrap()
&& store.is_state_in_layer_cache(roots[2]).unwrap(),
"upper layers must remain resident after the bottom-layer-only commit"
);
assert!(
store.trie_cache.read().unwrap().overlay().is_none(),
"reconciliation must clear the overlay"
);
assert!(
journal_entry_exists(&backend, 10),
"bottom layer T must be journaled"
);
assert!(!journal_entry_exists(&backend, 11));
assert!(!journal_entry_exists(&backend, 12));
let entry_bytes = backend
.begin_read()
.unwrap()
.get(STATE_HISTORY, &10u64.to_be_bytes())
.unwrap()
.expect("journal entry for block 10");
let entry = JournalEntry::decode(&entry_bytes).unwrap();
let diff_keys: Vec<&Vec<u8>> = entry.account_trie_diff.iter().map(|(k, _)| k).collect();
assert!(
diff_keys.contains(&&vec![0x00, 10]) && diff_keys.contains(&&vec![0x00, 11]),
"bridge keys from the overlay must be folded into T's journal entry, got {diff_keys:?}"
);
let trie = store.trie_cache.read().unwrap().clone();
commit_to_disk(
store.backend.as_ref(),
&store.flatkeyvalue_control_tx,
&store.trie_cache,
&trie,
roots[2],
false,
)
.unwrap();
assert!(!store.is_state_in_layer_cache(roots[1]).unwrap());
assert!(!store.is_state_in_layer_cache(roots[2]).unwrap());
assert!(journal_entry_exists(&backend, 11));
assert!(journal_entry_exists(&backend, 12));
}
#[tokio::test]
async fn journal_pruning_pauses_during_reorg_and_catches_up_after() {
let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
let dir = tempfile::tempdir().unwrap();
let store = Store::from_backend(
backend.clone(),
dir.path().to_path_buf(),
1,
DEFAULT_PERSIST_CHANNEL_CAPACITY,
)
.unwrap();
seed_journal_entries(&backend, &(1..=5).collect::<Vec<_>>());
store.set_journal_pruning_paused(true);
store
.forkchoice_update_inner(vec![], 100, H256::zero(), None, Some(3))
.await
.unwrap();
for n in 1..=5 {
assert!(
journal_entry_exists(&backend, n),
"entry {n} must survive pruning while the reorg pause is held"
);
}
store.set_journal_pruning_paused(false);
store
.forkchoice_update_inner(vec![], 100, H256::zero(), None, Some(4))
.await
.unwrap();
for n in 1..=4 {
assert!(
!journal_entry_exists(&backend, n),
"entry {n} must be pruned once the pause is released"
);
}
assert!(journal_entry_exists(&backend, 5));
}
}
#[cfg(test)]
mod merge_tests {
use super::*;
fn h256(b: u8) -> H256 {
H256::from_low_u64_be(b as u64)
}
fn op(bn: BlockNumber, bh: H256, idx: Index) -> Vec<u8> {
encode_tx_location_operand(bn, bh, idx)
}
fn decode(v: &[u8]) -> Vec<(BlockNumber, BlockHash, Index)> {
<Vec<(BlockNumber, BlockHash, Index)>>::decode(v).unwrap()
}
#[test]
fn single_operand_on_empty_base() {
let out = tx_locations_merge(None, vec![op(100, h256(0x10), 0)]).unwrap();
assert_eq!(decode(&out), vec![(100, h256(0x10), 0)]);
}
#[test]
fn operand_appended_to_existing_base() {
let base = vec![(100u64, h256(0x10), 0u64)].encode_to_vec();
let out = tx_locations_merge(Some(&base), vec![op(101, h256(0x11), 5)]).unwrap();
let mut got = decode(&out);
got.sort();
let mut want = vec![(100, h256(0x10), 0), (101, h256(0x11), 5)];
want.sort();
assert_eq!(got, want);
}
#[test]
fn multiple_operands_combined() {
let out = tx_locations_merge(
None,
vec![
op(100, h256(0x10), 0),
op(100, h256(0x11), 1),
op(101, h256(0x12), 2),
],
)
.unwrap();
assert_eq!(decode(&out).len(), 3);
}
#[test]
fn same_block_hash_is_deduped() {
let out =
tx_locations_merge(None, vec![op(100, h256(0x10), 0), op(100, h256(0x10), 7)]).unwrap();
assert_eq!(decode(&out), vec![(100, h256(0x10), 7)]);
}
#[test]
fn malformed_operand_aborts_merge() {
let out = tx_locations_merge(None, vec![vec![0xff, 0xff], op(100, h256(0x10), 0)]);
assert!(out.is_none(), "merge must abort on a malformed operand");
}
#[test]
fn malformed_base_value_aborts_merge() {
let out = tx_locations_merge(Some(&[0xff, 0xff]), vec![op(100, h256(0x10), 0)]);
assert!(out.is_none(), "merge must abort on a corrupt base value");
}
#[test]
fn partial_merge_result_is_a_valid_operand() {
let partial =
tx_locations_merge(None, vec![op(100, h256(0x10), 0), op(101, h256(0x11), 1)]).unwrap();
let base = vec![(99u64, h256(0x09), 9u64)].encode_to_vec();
let out = tx_locations_merge(Some(&base), vec![partial]).unwrap();
let mut got = decode(&out);
got.sort();
let mut want = vec![
(99, h256(0x09), 9),
(100, h256(0x10), 0),
(101, h256(0x11), 1),
];
want.sort();
assert_eq!(
got, want,
"no entries may be lost when re-merging a partial result"
);
}
#[test]
fn operand_encoding_matches_value_encoding() {
let operand = op(100, h256(0x10), 3);
assert_eq!(decode(&operand), vec![(100, h256(0x10), 3)]);
}
#[test]
fn chained_partial_merges() {
let p1 = tx_locations_merge(None, vec![op(1, h256(0x01), 0)]).unwrap();
let p2 = tx_locations_merge(None, vec![p1, op(2, h256(0x02), 0)]).unwrap();
let p3 = tx_locations_merge(None, vec![p2, op(3, h256(0x03), 0)]).unwrap();
let out = tx_locations_merge(None, vec![p3]).unwrap();
assert_eq!(decode(&out).len(), 3);
}
}
#[cfg(test)]
mod backfill_write_tests {
use super::*;
use ethrex_common::types::{EIP1559Transaction, TxType};
fn test_header(number: BlockNumber) -> BlockHeader {
BlockHeader {
number,
..Default::default()
}
}
fn test_tx(nonce: u64) -> Transaction {
Transaction::EIP1559Transaction(EIP1559Transaction {
nonce,
..Default::default()
})
}
fn test_receipt() -> Receipt {
Receipt {
tx_type: TxType::EIP1559,
succeeded: true,
cumulative_gas_used: 21_000,
logs: vec![],
payer: None,
frame_receipts: None,
}
}
#[tokio::test]
async fn writes_data_indexes_txs_and_lowers_frontier() {
let store = Store::new("", EngineType::InMemory).expect("in-memory store");
store.update_earliest_block_number(100).await.unwrap();
let header = test_header(99);
let hash = header.hash();
let tx = test_tx(7);
let body = BlockBody {
transactions: vec![tx.clone()],
ommers: vec![],
withdrawals: Some(vec![]),
};
store.add_block_headers(vec![header.clone()]).await.unwrap();
store
.forkchoice_update(vec![(99, hash)], 99, hash, None, None)
.await
.unwrap();
store
.add_backfilled_blocks(
vec![BackfilledBlock {
header,
body,
receipts: vec![test_receipt()],
index_transactions: true,
}],
99,
)
.await
.unwrap();
assert_eq!(
store.get_earliest_block_number().await.unwrap(),
99,
"frontier must be lowered atomically with the data write"
);
assert!(
store.get_block_body_by_hash(hash).await.unwrap().is_some(),
"body must be stored"
);
assert_eq!(
store.get_receipts_for_block(&hash).await.unwrap().len(),
1,
"receipts must be stored"
);
assert_eq!(
store
.get_transaction_location(tx.hash(&NativeCrypto))
.await
.unwrap(),
Some((99, hash, 0)),
"indexed transaction must be locatable"
);
}
#[tokio::test]
async fn skips_tx_index_when_disabled() {
let store = Store::new("", EngineType::InMemory).expect("in-memory store");
store.update_earliest_block_number(100).await.unwrap();
let header = test_header(99);
let hash = header.hash();
let tx = test_tx(7);
let body = BlockBody {
transactions: vec![tx.clone()],
ommers: vec![],
withdrawals: Some(vec![]),
};
store.add_block_headers(vec![header.clone()]).await.unwrap();
store
.forkchoice_update(vec![(99, hash)], 99, hash, None, None)
.await
.unwrap();
store
.add_backfilled_blocks(
vec![BackfilledBlock {
header,
body,
receipts: vec![test_receipt()],
index_transactions: false,
}],
99,
)
.await
.unwrap();
assert!(
store.get_block_body_by_hash(hash).await.unwrap().is_some(),
"body must still be stored"
);
assert_eq!(
store.get_receipts_for_block(&hash).await.unwrap().len(),
1,
"receipts must still be stored"
);
assert_eq!(
store
.get_transaction_location(tx.hash(&NativeCrypto))
.await
.unwrap(),
None,
"transaction index must be skipped when index_transactions is false"
);
}
#[tokio::test]
async fn historical_getters_transition_from_null_to_present() {
let store = Store::new("", EngineType::InMemory).expect("in-memory store");
store.update_earliest_block_number(100).await.unwrap();
let header = test_header(99);
let hash = header.hash();
let tx = test_tx(7);
let tx_hash = tx.hash(&NativeCrypto);
let body = BlockBody {
transactions: vec![tx],
ommers: vec![],
withdrawals: Some(vec![]),
};
store.add_block_headers(vec![header.clone()]).await.unwrap();
store
.forkchoice_update(vec![(99, hash)], 99, hash, None, None)
.await
.unwrap();
assert!(
store.get_block_body(99).await.unwrap().is_none(),
"body absent before backfill"
);
assert!(
store
.get_transaction_by_hash(tx_hash)
.await
.unwrap()
.is_none(),
"tx unlocatable before backfill"
);
assert!(
store.get_receipt(99, 0).await.unwrap().is_none(),
"receipt absent before backfill"
);
store
.add_backfilled_blocks(
vec![BackfilledBlock {
header,
body,
receipts: vec![test_receipt()],
index_transactions: true,
}],
99,
)
.await
.unwrap();
assert!(
store.get_block_body(99).await.unwrap().is_some(),
"body present after backfill"
);
assert!(
store
.get_transaction_by_hash(tx_hash)
.await
.unwrap()
.is_some(),
"tx locatable after backfill"
);
assert!(
store.get_receipt(99, 0).await.unwrap().is_some(),
"receipt present after backfill"
);
}
#[tokio::test]
async fn resume_fills_contiguously_without_gaps() {
let store = Store::new("", EngineType::InMemory).expect("in-memory store");
store.update_earliest_block_number(100).await.unwrap();
let make_batch = |range: std::ops::Range<u64>| -> Vec<BackfilledBlock> {
range
.rev()
.map(|n| BackfilledBlock {
header: test_header(n),
body: BlockBody {
transactions: vec![],
ommers: vec![],
withdrawals: Some(vec![]),
},
receipts: vec![],
index_transactions: false,
})
.collect()
};
store
.add_backfilled_blocks(make_batch(90..100), 90)
.await
.unwrap();
assert_eq!(store.get_earliest_block_number().await.unwrap(), 90);
let resume_from = store.get_earliest_block_number().await.unwrap();
assert_eq!(resume_from, 90, "resume reads the persisted frontier");
store
.add_backfilled_blocks(make_batch(80..90), 80)
.await
.unwrap();
assert_eq!(store.get_earliest_block_number().await.unwrap(), 80);
for n in 80..100 {
assert!(
store
.get_block_body_by_hash(test_header(n).hash())
.await
.unwrap()
.is_some(),
"block {n} must have a body (no gap left by resume)"
);
}
}
}
#[cfg(test)]
#[cfg(feature = "rocksdb")]
mod schema_version_guard_tests {
use super::*;
fn write_marker(dir: &Path, schema_version: u64) {
let metadata = StoreMetadata::new(schema_version);
std::fs::write(
dir.join(STORE_METADATA_FILENAME),
serde_json::to_string_pretty(&metadata).unwrap(),
)
.unwrap();
}
fn read_marker(dir: &Path) -> u64 {
read_store_schema_version(dir).unwrap().unwrap()
}
#[test]
fn a_database_ahead_of_the_binary_is_refused_and_left_untouched() {
let dir = tempfile::tempdir().unwrap();
let newer = STORE_SCHEMA_VERSION + 1;
write_marker(dir.path(), newer);
let result =
Store::new_with_config(dir.path(), EngineType::RocksDB, StoreConfig::default());
assert!(
matches!(
result,
Err(StoreError::IncompatibleDBVersion { found, expected })
if found == newer && expected == STORE_SCHEMA_VERSION
),
"expected IncompatibleDBVersion, got {:?}",
result.err()
);
assert_eq!(read_marker(dir.path()), newer);
}
#[test]
fn a_zero_schema_version_is_incompatible_not_a_failed_migration() {
let dir = tempfile::tempdir().unwrap();
write_marker(dir.path(), 0);
let result =
Store::new_with_config(dir.path(), EngineType::RocksDB, StoreConfig::default());
assert!(
matches!(
result,
Err(StoreError::IncompatibleDBVersion { found: 0, expected })
if expected == STORE_SCHEMA_VERSION
),
"expected IncompatibleDBVersion, got {:?}",
result.err()
);
}
}
#[cfg(test)]
mod datadir_tests {
use super::*;
use std::fs;
#[test]
fn empty_dir_has_no_existing_db() {
let dir = tempfile::tempdir().unwrap();
assert!(!dir_contains_legacy_db(dir.path()).unwrap());
}
#[test]
fn dir_with_only_unrelated_files_has_no_existing_db() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("jwt.hex"), "0xdeadbeef").unwrap();
fs::write(dir.path().join("LOG"), "noise").unwrap();
assert!(!dir_contains_legacy_db(dir.path()).unwrap());
}
#[test]
fn dir_with_rocksdb_markers_has_existing_db() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("CURRENT"), "MANIFEST-000001\n").unwrap();
assert!(dir_contains_legacy_db(dir.path()).unwrap());
let dir2 = tempfile::tempdir().unwrap();
fs::write(dir2.path().join("MANIFEST-000007"), "x").unwrap();
assert!(dir_contains_legacy_db(dir2.path()).unwrap());
}
#[test]
fn dir_with_marker_named_subdirectories_has_no_existing_db() {
let dir = tempfile::tempdir().unwrap();
fs::create_dir(dir.path().join("CURRENT")).unwrap();
fs::create_dir(dir.path().join("MANIFEST-000001")).unwrap();
assert!(!dir_contains_legacy_db(dir.path()).unwrap());
}
}
#[cfg(test)]
mod flatkeyvalue_completeness_tests {
use super::*;
#[test]
fn only_the_one_byte_ff_sentinel_counts_as_complete() {
assert!(Store::flatkeyvalue_generation_complete(Some(&[0xff])));
assert!(!Store::flatkeyvalue_generation_complete(None)); assert!(!Store::flatkeyvalue_generation_complete(Some(&[]))); assert!(!Store::flatkeyvalue_generation_complete(Some(&[0u8; 64]))); assert!(!Store::flatkeyvalue_generation_complete(Some(&[0x0a; 64]))); assert!(!Store::flatkeyvalue_generation_complete(Some(&[0xff; 64])));
assert!(!Store::flatkeyvalue_generation_complete(Some(&[
0xff, 0xff
])));
}
}
#[cfg(test)]
mod cgroup_memory_limit_tests {
use super::*;
use std::fs;
fn cgroup_tree(root: &Path, relative: &str, file: &str, limits: &[(&str, &str)]) {
fs::create_dir_all(root.join(relative)).expect("create cgroup tree");
for (dir, contents) in limits {
fs::write(root.join(dir).join(file), contents).expect("write limit");
}
}
#[test]
fn ancestor_limit_applies() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
cgroup_tree(
root,
"system.slice/ethrex.service",
"memory.max",
&[("", "max"), ("system.slice", "4294967296")],
);
assert_eq!(
cgroup_limit_up_to_root(root, "/system.slice/ethrex.service", "memory.max"),
Some(4 * 1024 * 1024 * 1024)
);
}
#[test]
fn smallest_limit_in_the_chain_wins() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
cgroup_tree(
root,
"kubepods/pod123/container",
"memory.max",
&[
("kubepods", "8589934592"),
("kubepods/pod123", "2147483648"),
("kubepods/pod123/container", "4294967296"),
],
);
assert_eq!(
cgroup_limit_up_to_root(root, "kubepods/pod123/container", "memory.max"),
Some(2 * 1024 * 1024 * 1024)
);
}
#[test]
fn unlimited_chain_yields_no_limit() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
cgroup_tree(root, "docker/abc", "memory.max", &[("", "max")]);
assert_eq!(
cgroup_limit_up_to_root(root, "docker/abc", "memory.max"),
None
);
assert_eq!(
cgroup_limit_up_to_root(root, "not/here", "memory.max"),
None
);
}
#[test]
fn root_limit_applies_to_the_root_cgroup() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
cgroup_tree(root, "", "memory.limit_in_bytes", &[("", "536870912")]);
assert_eq!(
cgroup_limit_up_to_root(root, "/", "memory.limit_in_bytes"),
Some(512 * 1024 * 1024)
);
}
}