use ethrex_common::{
H256,
seeded_hash::{SeededHashMap, SeededState},
types::BlockNumber,
};
use fastbloom::AtomicBloomFilter;
use rayon::prelude::*;
use rustc_hash::FxHashMap;
use std::{
fmt,
sync::{Arc, RwLock},
};
use ethrex_trie::{Nibbles, TrieDB, TrieError};
use crate::{
api::{StorageBackend, tables::STATE_HISTORY},
error::StoreError,
journal::{JournalDecodeError, JournalEntry},
trie::classify_trie_key,
};
const BLOOM_SIZE: usize = 1_000_000;
const FALSE_POSITIVE_RATE: f64 = 0.02;
#[derive(Debug, Clone)]
struct TrieLayer {
nodes: SeededHashMap<Vec<u8>, Vec<u8>>,
parent: H256,
id: usize,
block_number: BlockNumber,
block_hash: H256,
}
#[derive(Clone)]
pub struct TrieLayerCache {
last_id: usize,
pub(crate) commit_threshold: usize,
layers: FxHashMap<H256, Arc<TrieLayer>>,
bloom: AtomicBloomFilter<SeededState>,
overlay: Option<Arc<Overlay>>,
pub(crate) safe_commit_root: Arc<RwLock<H256>>,
}
impl fmt::Debug for TrieLayerCache {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let safe_commit = match self.safe_commit_root.read() {
Ok(guard) => format!("{:?}", *guard),
Err(_) => "<poisoned>".to_string(),
};
f.debug_struct("TrieLayerCache")
.field("last_id", &self.last_id)
.field("commit_threshold", &self.commit_threshold)
.field("layers", &self.layers)
.field("bloom", &"AtomicBloomFilter")
.field("overlay", &self.overlay)
.field("safe_commit_root", &safe_commit)
.finish()
}
}
impl TrieLayerCache {
pub fn new_with_safe_commit(
commit_threshold: usize,
safe_commit_root: Arc<RwLock<H256>>,
) -> Self {
Self {
bloom: Self::create_filter(BLOOM_SIZE),
last_id: 0,
layers: Default::default(),
commit_threshold,
overlay: None,
safe_commit_root,
}
}
pub fn set_overlay(&mut self, overlay: Arc<Overlay>) {
self.overlay = Some(overlay);
}
pub fn clear_overlay(&mut self) {
self.overlay = None;
}
pub fn overlay(&self) -> Option<&Arc<Overlay>> {
self.overlay.as_ref()
}
pub fn overlay_serves(&self, state_root: H256) -> bool {
self.overlay
.as_ref()
.is_some_and(|o| state_root == o.serves_root() || self.layers.contains_key(&state_root))
}
pub fn lookup_overlay(&self, key: &[u8]) -> Option<Option<Vec<u8>>> {
let overlay = self.overlay.as_ref()?;
let cf = OverlayCf::classify_by_key_length(key.len());
overlay.lookup(cf, key)
}
pub fn contains(&self, state_root: H256) -> bool {
self.layers.contains_key(&state_root)
}
pub fn commit_threshold(&self) -> usize {
self.commit_threshold
}
fn create_filter(expected_items: usize) -> AtomicBloomFilter<SeededState> {
AtomicBloomFilter::with_false_pos(FALSE_POSITIVE_RATE)
.hasher(SeededState::default())
.expected_items(expected_items.max(BLOOM_SIZE))
}
pub fn get(&self, state_root: H256, key: &[u8]) -> Option<Vec<u8>> {
if !self.bloom.contains(key) {
return None;
}
let mut current_state_root = state_root;
while let Some(layer) = self.layers.get(¤t_state_root) {
if let Some(value) = layer.nodes.get(key) {
return Some(value.clone());
}
current_state_root = layer.parent;
if current_state_root == state_root {
panic!("State cycle found");
}
}
None
}
pub fn get_commitable(&self, parent_state_root: H256) -> Option<H256> {
let safe_root = *self.safe_commit_root.read().ok()?;
if safe_root.is_zero() {
return None;
}
if parent_state_root == safe_root {
return self.has_layer(safe_root).then_some(safe_root);
}
let mut current = parent_state_root;
let mut steps = 0usize;
let max_steps = self.layers.len();
while let Some(layer) = self.layers.get(¤t) {
if current == safe_root {
return Some(safe_root);
}
let next = layer.parent;
if next == parent_state_root {
return None;
}
steps += 1;
if steps > max_steps {
return None;
}
current = next;
}
None
}
pub(crate) fn get_commitable_by_depth(
&self,
mut state_root: H256,
threshold: usize,
) -> Option<H256> {
let mut counter = 0;
while let Some(layer) = self.layers.get(&state_root) {
counter += 1;
if counter >= threshold {
return Some(state_root);
}
state_root = layer.parent;
}
None
}
pub fn put_batch(
&mut self,
parent: H256,
state_root: H256,
block_number: BlockNumber,
block_hash: H256,
key_values: Vec<(Nibbles, Vec<u8>)>,
) {
if parent == state_root && key_values.is_empty() {
return;
} else if parent == state_root {
tracing::trace!("parent == state_root but key_values not empty");
return;
}
if self.layers.contains_key(&state_root) {
tracing::warn!("tried to insert a state_root that's already inserted");
return;
}
for (p, _) in &key_values {
self.bloom.insert(p.as_ref());
}
let nodes: SeededHashMap<Vec<u8>, Vec<u8>> = key_values
.into_iter()
.map(|(path, value)| (path.into_vec(), value))
.collect();
self.last_id += 1;
let entry = TrieLayer {
nodes,
parent,
id: self.last_id,
block_number,
block_hash,
};
self.layers.insert(state_root, Arc::new(entry));
}
pub fn rebuild_bloom(&mut self) {
let total_keys: usize = self.layers.values().map(|layer| layer.nodes.len()).sum();
let filter = Self::create_filter(total_keys.max(BLOOM_SIZE));
self.layers.par_iter().for_each(|(_, layer)| {
for path in layer.nodes.keys() {
filter.insert(path);
}
});
self.bloom = filter;
}
pub fn has_layer(&self, state_root: H256) -> bool {
self.layers.contains_key(&state_root)
}
pub fn layer_count(&self) -> usize {
self.layers.len()
}
pub(crate) fn bottom_layer_root(&self, root: H256) -> H256 {
let mut current = root;
while let Some(layer) = self.layers.get(¤t) {
if !self.layers.contains_key(&layer.parent) {
break;
}
current = layer.parent;
}
current
}
pub fn commit(&mut self, state_root: H256) -> Option<Vec<CommittedLayer>> {
let mut layers_to_commit = vec![];
let mut current_state_root = state_root;
while let Some(layer) = self.layers.remove(¤t_state_root) {
let layer = Arc::unwrap_or_clone(layer);
current_state_root = layer.parent;
layers_to_commit.push(layer);
}
let top_layer_id = layers_to_commit.first()?.id;
self.layers.retain(|_, item| item.id > top_layer_id);
self.rebuild_bloom(); let committed = layers_to_commit
.into_iter()
.rev()
.map(|layer| CommittedLayer {
block_number: layer.block_number,
block_hash: layer.block_hash,
parent_state_root: layer.parent,
nodes: layer.nodes.into_iter().collect(),
})
.collect();
Some(committed)
}
}
#[derive(Debug)]
pub struct CommittedLayer {
pub block_number: BlockNumber,
pub block_hash: H256,
pub parent_state_root: H256,
pub nodes: Vec<(Vec<u8>, Vec<u8>)>,
}
pub struct TrieWrapper {
pub state_root: H256,
pub inner: Arc<TrieLayerCache>,
pub db: Box<dyn TrieDB>,
prefix_nibbles: Option<Nibbles>,
}
impl TrieWrapper {
pub fn new(
state_root: H256,
inner: Arc<TrieLayerCache>,
db: Box<dyn TrieDB>,
prefix: Option<H256>,
) -> Self {
let prefix_nibbles = prefix.map(|p| Nibbles::from_bytes(p.as_bytes()).append_new(17));
Self {
state_root,
inner,
db,
prefix_nibbles,
}
}
}
pub fn apply_prefix(prefix: Option<H256>, path: Nibbles) -> Nibbles {
match prefix {
Some(prefix) => Nibbles::from_bytes(prefix.as_bytes())
.append_new(17)
.concat(&path),
None => path,
}
}
impl TrieDB for TrieWrapper {
fn flatkeyvalue_computed(&self, key: Nibbles) -> bool {
if self.inner.overlay_serves(self.state_root) {
return false;
}
let key = match &self.prefix_nibbles {
Some(prefix) => prefix.concat(&key),
None => key,
};
self.db.flatkeyvalue_computed(key)
}
fn get(&self, key: Nibbles) -> Result<Option<Vec<u8>>, TrieError> {
let key = match &self.prefix_nibbles {
Some(prefix) => prefix.concat(&key),
None => key,
};
if let Some(value) = self.inner.get(self.state_root, key.as_ref()) {
return Ok(Some(value));
}
if self.inner.overlay_serves(self.state_root)
&& let Some(overlay_result) = self.inner.lookup_overlay(key.as_ref())
{
return Ok(overlay_result);
}
self.db.get(key)
}
fn multi_get(&self, keys: &[Nibbles]) -> Vec<Result<Option<Vec<u8>>, TrieError>> {
let mut results: Vec<Option<Result<Option<Vec<u8>>, TrieError>>> =
(0..keys.len()).map(|_| None).collect();
let mut miss_indices = Vec::new();
let mut miss_keys = Vec::new();
let overlay_serves = self.inner.overlay_serves(self.state_root);
for (idx, key) in keys.iter().enumerate() {
let key = match &self.prefix_nibbles {
Some(prefix) => prefix.concat(key),
None => key.clone(),
};
if let Some(value) = self.inner.get(self.state_root, key.as_ref()) {
results[idx] = Some(Ok(Some(value)));
} else if overlay_serves
&& let Some(overlay_result) = self.inner.lookup_overlay(key.as_ref())
{
results[idx] = Some(Ok(overlay_result));
} else {
miss_indices.push(idx);
miss_keys.push(key);
}
}
if !miss_keys.is_empty() {
let db_results = self.db.multi_get(&miss_keys);
for (idx, res) in miss_indices.into_iter().zip(db_results) {
results[idx] = Some(res);
}
}
results
.into_iter()
.map(|r| r.expect("every key is resolved in-memory or batched as a db miss"))
.collect()
}
fn put_batch(&self, _key_values: Vec<(Nibbles, Vec<u8>)>) -> Result<(), TrieError> {
unimplemented!("This function should not be called");
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OverlayCf {
AccountTrie,
StorageTrie,
AccountFlat,
StorageFlat,
}
impl OverlayCf {
pub fn classify_by_key_length(len: usize) -> Self {
let (is_leaf, is_account) = classify_trie_key(len);
match (is_leaf, is_account) {
(true, true) => OverlayCf::AccountFlat,
(true, false) => OverlayCf::StorageFlat,
(false, true) => OverlayCf::AccountTrie,
(false, false) => OverlayCf::StorageTrie,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum OverlayError {
#[error("missing journal entry for block {0}")]
MissingEntry(BlockNumber),
#[error(
"journal block_hash mismatch at block {block_number}: expected {expected:?}, found {found:?}"
)]
HashMismatch {
block_number: BlockNumber,
expected: H256,
found: H256,
},
#[error("invalid overlay range: from_block ({from_block}) < to_block ({to_block})")]
InvalidRange {
from_block: BlockNumber,
to_block: BlockNumber,
},
#[error("journal decode error: {0}")]
Decode(#[from] JournalDecodeError),
#[error("storage error: {0}")]
Store(#[from] StoreError),
}
pub struct Overlay {
account_trie: SeededHashMap<Vec<u8>, Option<Vec<u8>>>,
storage_trie: SeededHashMap<Vec<u8>, Option<Vec<u8>>>,
account_flat: SeededHashMap<Vec<u8>, Option<Vec<u8>>>,
storage_flat: SeededHashMap<Vec<u8>, Option<Vec<u8>>>,
bloom: AtomicBloomFilter<SeededState>,
from_block: BlockNumber,
to_block: BlockNumber,
serves_root: H256,
}
impl fmt::Debug for Overlay {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Overlay")
.field("account_trie_len", &self.account_trie.len())
.field("storage_trie_len", &self.storage_trie.len())
.field("account_flat_len", &self.account_flat.len())
.field("storage_flat_len", &self.storage_flat.len())
.field("from_block", &self.from_block)
.field("to_block", &self.to_block)
.finish()
}
}
impl Default for Overlay {
fn default() -> Self {
Self {
account_trie: SeededHashMap::default(),
storage_trie: SeededHashMap::default(),
account_flat: SeededHashMap::default(),
storage_flat: SeededHashMap::default(),
bloom: AtomicBloomFilter::with_false_pos(FALSE_POSITIVE_RATE)
.hasher(SeededState::default())
.expected_items(Self::BLOOM_INITIAL_CAPACITY),
from_block: 0,
to_block: 0,
serves_root: H256::zero(),
}
}
}
impl Overlay {
const BLOOM_INITIAL_CAPACITY: usize = 64 * 1024;
pub fn from_journal(
backend: &dyn StorageBackend,
from_block: BlockNumber,
to_block: BlockNumber,
expected_hash: impl Fn(BlockNumber) -> Option<H256>,
) -> Result<Self, OverlayError> {
if from_block < to_block {
return Err(OverlayError::InvalidRange {
from_block,
to_block,
});
}
let mut overlay = Overlay {
from_block,
to_block,
..Default::default()
};
let read = backend.begin_read()?;
let mut n = from_block;
loop {
let bytes = read
.get(STATE_HISTORY, &n.to_be_bytes())?
.ok_or(OverlayError::MissingEntry(n))?;
let entry = JournalEntry::decode(&bytes)?;
if let Some(expected) = expected_hash(n)
&& entry.block_hash != expected
{
return Err(OverlayError::HashMismatch {
block_number: n,
expected,
found: entry.block_hash,
});
}
if n == to_block {
overlay.serves_root = entry.parent_state_root;
}
overlay.absorb(entry);
if n == to_block {
break;
}
n -= 1;
}
Ok(overlay)
}
fn absorb(&mut self, entry: JournalEntry) {
for (k, v) in entry.account_trie_diff {
self.bloom.insert(&k);
self.account_trie.insert(k, v);
}
for (k, v) in entry.storage_trie_diff {
self.bloom.insert(&k);
self.storage_trie.insert(k, v);
}
for (k, v) in entry.account_flat_diff {
self.bloom.insert(&k);
self.account_flat.insert(k, v);
}
for (k, v) in entry.storage_flat_diff {
self.bloom.insert(&k);
self.storage_flat.insert(k, v);
}
}
pub fn lookup(&self, cf: OverlayCf, key: &[u8]) -> Option<Option<Vec<u8>>> {
if !self.bloom.contains(key) {
return None;
}
let map = match cf {
OverlayCf::AccountTrie => &self.account_trie,
OverlayCf::StorageTrie => &self.storage_trie,
OverlayCf::AccountFlat => &self.account_flat,
OverlayCf::StorageFlat => &self.storage_flat,
};
map.get(key).cloned()
}
pub fn len(&self) -> usize {
self.account_trie.len()
+ self.storage_trie.len()
+ self.account_flat.len()
+ self.storage_flat.len()
}
#[allow(dead_code)]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn byte_size(&self) -> usize {
[
&self.account_trie,
&self.storage_trie,
&self.account_flat,
&self.storage_flat,
]
.iter()
.flat_map(|map| map.iter())
.map(|(k, v)| k.len() + v.as_ref().map_or(0, |v| v.len()))
.sum()
}
#[allow(
clippy::wrong_self_convention,
reason = "field accessor: name matches struct field"
)]
pub fn from_block(&self) -> BlockNumber {
self.from_block
}
pub fn serves_root(&self) -> H256 {
self.serves_root
}
pub fn to_block(&self) -> BlockNumber {
self.to_block
}
pub fn iter_all_entries(
&self,
) -> impl Iterator<Item = (OverlayCf, &Vec<u8>, &Option<Vec<u8>>)> {
self.account_trie
.iter()
.map(|(k, v)| (OverlayCf::AccountTrie, k, v))
.chain(
self.storage_trie
.iter()
.map(|(k, v)| (OverlayCf::StorageTrie, k, v)),
)
.chain(
self.account_flat
.iter()
.map(|(k, v)| (OverlayCf::AccountFlat, k, v)),
)
.chain(
self.storage_flat
.iter()
.map(|(k, v)| (OverlayCf::StorageFlat, k, v)),
)
}
}
#[cfg(test)]
mod overlay_tests {
use super::*;
use crate::backend::in_memory::InMemoryBackend;
use crate::journal::FlatDiff;
fn h(b: u8) -> H256 {
H256::repeat_byte(b)
}
fn seed(backend: &Arc<dyn StorageBackend>, per_block: &[(BlockNumber, H256, FlatDiff)]) {
let mut tx = backend.begin_write().unwrap();
for (n, block_hash, diff) in per_block {
let entry = JournalEntry {
block_hash: *block_hash,
parent_state_root: H256::zero(),
account_trie_diff: diff.clone(),
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();
}
#[test]
fn from_journal_loads_descending_range() {
let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
seed(
&backend,
&[
(3, h(0x03), vec![(vec![0xa], Some(vec![0x33]))]),
(4, h(0x04), vec![(vec![0xb], Some(vec![0x44]))]),
(5, h(0x05), vec![(vec![0xc], Some(vec![0x55]))]),
],
);
let overlay =
Overlay::from_journal(backend.as_ref(), 5, 3, |n| Some(H256::repeat_byte(n as u8)))
.unwrap();
assert_eq!(overlay.len(), 3);
assert_eq!(overlay.from_block(), 5);
assert_eq!(overlay.to_block(), 3);
assert_eq!(
overlay.lookup(OverlayCf::AccountTrie, &[0xa]),
Some(Some(vec![0x33]))
);
assert_eq!(
overlay.lookup(OverlayCf::AccountTrie, &[0xb]),
Some(Some(vec![0x44]))
);
assert_eq!(
overlay.lookup(OverlayCf::AccountTrie, &[0xc]),
Some(Some(vec![0x55]))
);
}
#[test]
fn older_entry_wins_when_key_repeats() {
let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
seed(
&backend,
&[
(3, h(0x03), vec![(vec![0xaa], Some(b"X".to_vec()))]),
(4, h(0x04), vec![(vec![0xaa], Some(b"Y3".to_vec()))]),
(5, h(0x05), vec![(vec![0xaa], Some(b"Y4".to_vec()))]),
],
);
let overlay =
Overlay::from_journal(backend.as_ref(), 5, 3, |n| Some(H256::repeat_byte(n as u8)))
.unwrap();
assert_eq!(
overlay.lookup(OverlayCf::AccountTrie, &[0xaa]),
Some(Some(b"X".to_vec())),
"oldest reverse-diff value should win after descending walk"
);
}
#[test]
fn absent_key_passes_through_bloom() {
let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
seed(
&backend,
&[(3, h(0x03), vec![(vec![0xaa], Some(vec![0x11]))])],
);
let overlay =
Overlay::from_journal(backend.as_ref(), 3, 3, |n| Some(H256::repeat_byte(n as u8)))
.unwrap();
assert_eq!(overlay.lookup(OverlayCf::AccountTrie, &[0xff]), None);
}
#[test]
fn hash_mismatch_aborts() {
let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
seed(&backend, &[(7, h(0x07), vec![(vec![0xaa], None)])]);
let err = Overlay::from_journal(backend.as_ref(), 7, 7, |_| Some(h(0xff))).unwrap_err();
match err {
OverlayError::HashMismatch { block_number, .. } => assert_eq!(block_number, 7),
other => panic!("expected HashMismatch, got {other:?}"),
}
}
#[test]
fn missing_entry_aborts() {
let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
seed(&backend, &[(5, h(0x05), vec![])]);
let err = Overlay::from_journal(backend.as_ref(), 5, 3, |_| None).unwrap_err();
match err {
OverlayError::MissingEntry(n) => assert_eq!(n, 4),
other => panic!("expected MissingEntry, got {other:?}"),
}
}
#[test]
fn skip_verification_when_callback_returns_none() {
let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
seed(&backend, &[(7, h(0xab), vec![(vec![0x01], None)])]);
let overlay = Overlay::from_journal(backend.as_ref(), 7, 7, |_| None).unwrap();
assert_eq!(overlay.lookup(OverlayCf::AccountTrie, &[0x01]), Some(None));
}
#[test]
fn classify_by_key_length_matches_backend_table_routing() {
assert_eq!(OverlayCf::classify_by_key_length(0), OverlayCf::AccountTrie);
assert_eq!(
OverlayCf::classify_by_key_length(64),
OverlayCf::AccountTrie
);
assert_eq!(
OverlayCf::classify_by_key_length(65),
OverlayCf::AccountFlat
);
assert_eq!(
OverlayCf::classify_by_key_length(66),
OverlayCf::StorageTrie
);
assert_eq!(
OverlayCf::classify_by_key_length(130),
OverlayCf::StorageTrie
);
assert_eq!(
OverlayCf::classify_by_key_length(131),
OverlayCf::StorageFlat
);
assert_eq!(
OverlayCf::classify_by_key_length(132),
OverlayCf::StorageTrie
);
}
#[test]
fn from_journal_captures_serves_root_from_to_block_entry() {
let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
let pivot = h(0x77);
let mut tx = backend.begin_write().unwrap();
for (n, psr) in [(3u64, pivot), (4, h(0x44)), (5, h(0x55))] {
let entry = JournalEntry {
block_hash: h(n as u8),
parent_state_root: psr,
account_trie_diff: vec![(vec![n as u8], Some(vec![n as u8]))],
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();
let overlay = Overlay::from_journal(backend.as_ref(), 5, 3, |_| None).unwrap();
assert_eq!(overlay.serves_root(), pivot);
}
#[test]
fn overlay_serves_only_consuming_roots() {
let pivot = h(0xaa);
let new_chain = h(0xbb);
let unrelated = h(0xcc);
let parent = h(0xa9);
let mut cache =
TrieLayerCache::new_with_safe_commit(128, Arc::new(RwLock::new(H256::zero())));
assert!(!cache.overlay_serves(pivot));
cache.put_batch(
parent,
new_chain,
1,
h(0xb1),
vec![(Nibbles::from_bytes(&[0x01]), vec![0x02])],
);
let overlay = Overlay {
serves_root: pivot,
from_block: 5,
to_block: 3,
..Default::default()
};
cache.set_overlay(Arc::new(overlay));
assert!(
cache.overlay_serves(pivot),
"pivot root must consume the overlay"
);
assert!(
cache.overlay_serves(new_chain),
"new-chain layer root must consume the overlay"
);
assert!(
!cache.overlay_serves(unrelated),
"unrelated root must NOT see the overlay (would leak pivot state over disk)"
);
cache.clear_overlay();
assert!(!cache.overlay_serves(pivot));
assert!(!cache.overlay_serves(new_chain));
}
#[test]
fn flatkeyvalue_computed_is_disabled_while_overlay_serves() {
struct AlwaysComputedDb;
impl TrieDB for AlwaysComputedDb {
fn flatkeyvalue_computed(&self, _key: Nibbles) -> bool {
true
}
fn get(&self, _key: Nibbles) -> Result<Option<Vec<u8>>, TrieError> {
Ok(None)
}
fn put_batch(&self, _key_values: Vec<(Nibbles, Vec<u8>)>) -> Result<(), TrieError> {
unimplemented!()
}
}
let pivot = h(0xaa);
let unrelated = h(0xcc);
let key = Nibbles::from_bytes(&[0x01; 32]);
let mut cache =
TrieLayerCache::new_with_safe_commit(128, Arc::new(RwLock::new(H256::zero())));
cache.set_overlay(Arc::new(Overlay {
serves_root: pivot,
..Default::default()
}));
let cache = Arc::new(cache);
let served = TrieWrapper::new(pivot, cache.clone(), Box::new(AlwaysComputedDb), None);
assert!(
!served.flatkeyvalue_computed(key.clone()),
"served root must not trust disk flat-KV"
);
let unserved = TrieWrapper::new(unrelated, cache.clone(), Box::new(AlwaysComputedDb), None);
assert!(
unserved.flatkeyvalue_computed(key.clone()),
"unserved root must keep the flat-KV fast path"
);
let served_storage =
TrieWrapper::new(pivot, cache, Box::new(AlwaysComputedDb), Some(h(0x01)));
assert!(
!served_storage.flatkeyvalue_computed(key),
"served storage root must not trust disk flat-KV"
);
}
#[test]
fn multi_get_walks_the_same_cascade_as_get() {
struct DiskDb;
impl TrieDB for DiskDb {
fn get(&self, key: Nibbles) -> Result<Option<Vec<u8>>, TrieError> {
Ok(Some([b"disk-".as_slice(), key.as_ref()].concat()))
}
fn put_batch(&self, _key_values: Vec<(Nibbles, Vec<u8>)>) -> Result<(), TrieError> {
unimplemented!()
}
}
let pivot = h(0xaa);
let unrelated = h(0xcc);
let overlay_hit = Nibbles::from_raw(&[0xab, 0xcd], false);
let absent_at_pivot = Nibbles::from_raw(&[0x12, 0x34], false);
let overlay_miss = Nibbles::from_raw(&[0x56, 0x78], false);
let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
seed(
&backend,
&[(
1,
h(0x01),
vec![
(overlay_hit.as_ref().to_vec(), Some(b"pivot-value".to_vec())),
(absent_at_pivot.as_ref().to_vec(), None),
],
)],
);
let mut overlay = Overlay::from_journal(backend.as_ref(), 1, 1, |_| None).unwrap();
overlay.serves_root = pivot;
let mut cache =
TrieLayerCache::new_with_safe_commit(128, Arc::new(RwLock::new(H256::zero())));
cache.set_overlay(Arc::new(overlay));
let cache = Arc::new(cache);
let keys = [overlay_hit, absent_at_pivot, overlay_miss.clone()];
let served = TrieWrapper::new(pivot, cache.clone(), Box::new(DiskDb), None);
let batched: Vec<_> = served
.multi_get(&keys)
.into_iter()
.map(|r| r.unwrap())
.collect();
let serial: Vec<_> = keys
.iter()
.map(|k| served.get(k.clone()).unwrap())
.collect();
assert_eq!(batched, serial, "batched reads must match serial reads");
assert_eq!(batched[0], Some(b"pivot-value".to_vec()));
assert_eq!(
batched[1], None,
"absent at pivot must not fall through to the old chain's disk node"
);
assert_eq!(
batched[2],
Some([b"disk-".as_slice(), overlay_miss.as_ref()].concat()),
"overlay miss falls through to disk"
);
let unserved = TrieWrapper::new(unrelated, cache, Box::new(DiskDb), None);
let batched: Vec<_> = unserved
.multi_get(&keys)
.into_iter()
.map(|r| r.unwrap())
.collect();
let serial: Vec<_> = keys
.iter()
.map(|k| unserved.get(k.clone()).unwrap())
.collect();
assert_eq!(batched, serial);
assert!(
batched
.iter()
.all(|v| v.as_ref().is_some_and(|b| b.starts_with(b"disk-"))),
"unserved root must not see overlay values"
);
}
#[test]
fn overlay_lookup_returns_none_when_no_overlay_installed() {
let cache = TrieLayerCache::new_with_safe_commit(1, Arc::new(RwLock::new(H256::zero())));
for key_len in [4usize, 65, 67, 131] {
let key = vec![0xab; key_len];
assert_eq!(
cache.lookup_overlay(&key),
None,
"no overlay installed -> outer None at length {key_len}"
);
}
}
#[test]
fn overlay_lookup_classifies_cf_by_key_length() {
let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
let entry = JournalEntry {
block_hash: h(0x01),
parent_state_root: H256::zero(),
account_trie_diff: vec![(vec![0x10; 4], Some(b"acct-trie".to_vec()))],
storage_trie_diff: vec![(vec![0x20; 67], Some(b"stor-trie".to_vec()))],
account_flat_diff: vec![(vec![0x30; 65], Some(b"acct-flat".to_vec()))],
storage_flat_diff: vec![(vec![0x40; 131], None)],
};
let mut tx = backend.begin_write().unwrap();
tx.put(STATE_HISTORY, &1u64.to_be_bytes(), &entry.encode())
.unwrap();
tx.commit().unwrap();
let overlay = Overlay::from_journal(backend.as_ref(), 1, 1, |_| None).unwrap();
let mut cache =
TrieLayerCache::new_with_safe_commit(1, Arc::new(RwLock::new(H256::zero())));
cache.set_overlay(Arc::new(overlay));
assert_eq!(
cache.lookup_overlay(&[0x10; 4]),
Some(Some(b"acct-trie".to_vec()))
);
assert_eq!(
cache.lookup_overlay(&[0x20; 67]),
Some(Some(b"stor-trie".to_vec()))
);
assert_eq!(
cache.lookup_overlay(&[0x30; 65]),
Some(Some(b"acct-flat".to_vec()))
);
assert_eq!(
cache.lookup_overlay(&[0x40; 131]),
Some(None),
"overlay with None means key was absent at pivot"
);
assert_eq!(cache.lookup_overlay(&[0xee; 4]), None);
}
#[test]
fn set_and_clear_overlay_round_trips() {
let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
seed(&backend, &[(1, h(0x01), vec![(vec![0xaa], None)])]);
let overlay = Overlay::from_journal(backend.as_ref(), 1, 1, |_| None).unwrap();
let mut cache =
TrieLayerCache::new_with_safe_commit(1, Arc::new(RwLock::new(H256::zero())));
assert!(cache.overlay().is_none());
cache.set_overlay(Arc::new(overlay));
assert!(cache.overlay().is_some());
cache.clear_overlay();
assert!(cache.overlay().is_none());
cache.clear_overlay();
assert!(cache.overlay().is_none());
}
#[test]
fn single_entry_at_genesis() {
let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
seed(
&backend,
&[(0, h(0x00), vec![(vec![0xaa], Some(vec![0x11]))])],
);
let overlay = Overlay::from_journal(backend.as_ref(), 0, 0, |_| None).unwrap();
assert_eq!(overlay.len(), 1);
assert_eq!(overlay.from_block(), 0);
assert_eq!(overlay.to_block(), 0);
assert_eq!(
overlay.lookup(OverlayCf::AccountTrie, &[0xaa]),
Some(Some(vec![0x11]))
);
}
#[test]
fn swapped_args_returns_error() {
let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
let err = Overlay::from_journal(backend.as_ref(), 3, 5, |_| None).unwrap_err();
match err {
OverlayError::InvalidRange {
from_block,
to_block,
} => {
assert_eq!(from_block, 3);
assert_eq!(to_block, 5);
}
other => panic!("expected InvalidRange, got {other:?}"),
}
}
#[test]
fn empty_but_present_round_trips() {
let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
seed(&backend, &[(1, h(0x01), vec![(vec![0xaa], Some(vec![]))])]);
let overlay = Overlay::from_journal(backend.as_ref(), 1, 1, |_| None).unwrap();
assert_eq!(
overlay.lookup(OverlayCf::AccountTrie, &[0xaa]),
Some(Some(vec![])),
"empty-but-present value must NOT degrade to Some(None)"
);
}
#[test]
fn iter_all_entries_visits_each_cf() {
let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
let entry = JournalEntry {
block_hash: h(0x01),
parent_state_root: H256::zero(),
account_trie_diff: vec![(vec![0x10; 4], Some(b"at".to_vec()))],
storage_trie_diff: vec![(vec![0x20; 67], Some(b"st".to_vec()))],
account_flat_diff: vec![(vec![0x30; 65], None)],
storage_flat_diff: vec![(vec![0x40; 131], Some(b"sf".to_vec()))],
};
let mut tx = backend.begin_write().unwrap();
tx.put(STATE_HISTORY, &1u64.to_be_bytes(), &entry.encode())
.unwrap();
tx.commit().unwrap();
let overlay = Overlay::from_journal(backend.as_ref(), 1, 1, |_| None).unwrap();
let mut cfs: Vec<OverlayCf> = overlay.iter_all_entries().map(|(cf, _, _)| cf).collect();
cfs.sort_by_key(|cf| match cf {
OverlayCf::AccountTrie => 0,
OverlayCf::StorageTrie => 1,
OverlayCf::AccountFlat => 2,
OverlayCf::StorageFlat => 3,
});
assert_eq!(
cfs,
vec![
OverlayCf::AccountTrie,
OverlayCf::StorageTrie,
OverlayCf::AccountFlat,
OverlayCf::StorageFlat,
]
);
assert_eq!(overlay.len(), 4);
assert!(!overlay.is_empty());
}
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use std::sync::{Arc, RwLock};
use ethrex_common::H256;
use ethrex_trie::Nibbles;
use super::TrieLayerCache;
fn h256(b: u8) -> H256 {
let mut bytes = [0u8; 32];
bytes[31] = b;
H256(bytes)
}
fn key(b: u8) -> Nibbles {
Nibbles::from_bytes(&[b; 32])
}
fn build_chain(cache: &mut TrieLayerCache, n: u8) -> Vec<H256> {
let mut roots = Vec::with_capacity(n as usize);
let mut parent = H256::zero();
for i in 1..=n {
let root = h256(i);
cache.put_batch(parent, root, i as u64, root, vec![(key(i), vec![i])]);
roots.push(root);
parent = root;
}
roots
}
fn cache_with_cell(threshold: usize, root: H256) -> (TrieLayerCache, Arc<RwLock<H256>>) {
let cell = Arc::new(RwLock::new(root));
let cache = TrieLayerCache::new_with_safe_commit(threshold, Arc::clone(&cell));
(cache, cell)
}
#[test]
fn zero_cell_yields_none() {
let (mut cache, _cell) = cache_with_cell(4, H256::zero());
let roots = build_chain(&mut cache, 5);
assert_eq!(cache.get_commitable(*roots.last().unwrap()), None);
}
#[test]
fn safe_root_on_walk_yields_some() {
let safe = h256(2);
let (mut cache, _cell) = cache_with_cell(4, safe);
let roots = build_chain(&mut cache, 4);
assert_eq!(cache.get_commitable(roots[3]), Some(safe));
}
#[test]
fn parent_equals_safe_root_yields_some() {
let roots = {
let (mut cache, _cell) = cache_with_cell(4, H256::zero());
build_chain(&mut cache, 3)
};
let l3 = roots[2];
let (mut cache, _cell) = cache_with_cell(4, l3);
build_chain(&mut cache, 3);
assert_eq!(cache.get_commitable(l3), Some(l3));
}
#[test]
fn parent_equals_safe_root_without_layer_yields_none() {
let orphan = h256(7);
let (mut cache, _cell) = cache_with_cell(4, orphan);
cache.put_batch(orphan, orphan, 7, orphan, vec![]);
assert!(
!cache.has_layer(orphan),
"empty-block root must not create a layer"
);
assert_eq!(
cache.get_commitable(orphan),
None,
"a safe root with no layer is not committable, even when it equals the parent"
);
}
#[test]
fn already_committed_safe_root_yields_none() {
let safe = h256(2);
let (mut cache, _cell) = cache_with_cell(4, safe);
build_chain(&mut cache, 3);
assert_eq!(cache.get_commitable(safe), Some(safe));
cache.commit(safe).expect("first commit flushes the layer");
assert!(!cache.has_layer(safe), "commit prunes the flushed layer");
assert_eq!(
cache.get_commitable(safe),
None,
"re-committing an already-flushed root must not be offered again"
);
}
#[test]
fn safe_root_not_ancestor_yields_none() {
let (mut cache, _cell) = cache_with_cell(4, h256(99));
let roots = build_chain(&mut cache, 6);
assert_eq!(cache.get_commitable(*roots.last().unwrap()), None);
}
#[test]
fn cycle_guard_terminates() {
let b = h256(20);
let c = h256(21);
let (mut cache, _cell) = cache_with_cell(4, h256(99));
cache.put_batch(b, c, 21, c, vec![(key(21), vec![21])]);
cache.put_batch(c, b, 20, b, vec![(key(20), vec![20])]);
assert_eq!(cache.get_commitable(c), None);
}
#[test]
fn commit_retains_layers_above_safe_root() {
let safe = h256(2);
let (mut cache, _cell) = cache_with_cell(4, safe);
let roots = build_chain(&mut cache, 4);
let (l3, l4) = (roots[2], roots[3]);
assert_eq!(cache.get_commitable(roots[3]), Some(safe));
cache.commit(safe);
let remaining: HashSet<H256> = cache.layers.keys().copied().collect();
let expected: HashSet<H256> = [l3, l4].into_iter().collect();
assert_eq!(
remaining, expected,
"commit(safe) must retain only the layers above it"
);
}
#[test]
fn memory_bound_after_commit() {
let threshold = 4usize;
let n = (threshold + 3) as u8;
let safe = h256(n - threshold as u8); let (mut cache, _cell) = cache_with_cell(threshold, safe);
let roots = build_chain(&mut cache, n);
let tip = *roots.last().unwrap();
let commitable = cache.get_commitable(tip).expect("safe root on the walk");
cache.commit(commitable);
assert!(
cache.layers.len() <= threshold + 1,
"retained layers ({}) must stay within commit_threshold + 1 ({})",
cache.layers.len(),
threshold + 1
);
}
#[test]
fn live_canonical_gate_holds_while_depth_gate_would_commit() {
let (mut cache, _cell) = cache_with_cell(4, H256::zero());
let roots = build_chain(&mut cache, 5);
let l5 = *roots.last().unwrap();
assert!(
cache.get_commitable_by_depth(l5, 4).is_some(),
"depth-only gate commits at depth 4 regardless of canonicality"
);
assert_eq!(
cache.get_commitable(l5),
None,
"canonical gate must commit nothing while safe_commit is zero (the wedge fix)"
);
}
#[test]
fn batch_depth_gate_flushes_without_safe_commit() {
let (mut cache, _cell) = cache_with_cell(4, H256::zero());
let roots = build_chain(&mut cache, 5);
let tip = *roots.last().unwrap();
assert_eq!(
cache.get_commitable(tip),
None,
"canonical gate never flushes batch layers while safe_commit is zero"
);
assert_eq!(
cache.get_commitable_by_depth(tip, 4),
Some(roots[1]),
"batch depth gate must flush the layer 4 deep, bounding memory"
);
}
}
#[cfg(test)]
mod seeded_map_tests {
use super::*;
use std::hash::BuildHasher;
#[track_caller]
fn assert_seeded<S: BuildHasher + Default>(state: &S) {
let path = vec![0x5au8; 65];
assert_ne!(state.hash_one(&path), S::default().hash_one(&path));
}
#[test]
fn layer_and_overlay_maps_are_seeded() {
let layer = TrieLayer {
nodes: SeededHashMap::default(),
parent: H256::zero(),
id: 0,
block_number: 0,
block_hash: H256::zero(),
};
assert_seeded(layer.nodes.hasher());
let overlay = Overlay::default();
assert_seeded(overlay.account_trie.hasher());
assert_seeded(overlay.storage_trie.hasher());
assert_seeded(overlay.account_flat.hasher());
assert_seeded(overlay.storage_flat.hasher());
}
}