From 0542704d885e5d87a0bf9153d87d9814c8540e6a Mon Sep 17 00:00:00 2001 From: sadiq1971 Date: Mon, 17 Nov 2025 20:03:13 +0600 Subject: [PATCH 1/4] implemented pruner for proof storage --- Cargo.lock | 72 ++++ Cargo.toml | 1 + crates/optimism/trie/Cargo.toml | 2 + crates/optimism/trie/src/api.rs | 8 + crates/optimism/trie/src/db/store.rs | 17 +- crates/optimism/trie/src/lib.rs | 3 + crates/optimism/trie/src/prune/error.rs | 56 +++ crates/optimism/trie/src/prune/mod.rs | 5 + crates/optimism/trie/src/prune/pruner.rs | 484 +++++++++++++++++++++++ 9 files changed, 639 insertions(+), 9 deletions(-) create mode 100644 crates/optimism/trie/src/prune/error.rs create mode 100644 crates/optimism/trie/src/prune/mod.rs create mode 100644 crates/optimism/trie/src/prune/pruner.rs diff --git a/Cargo.lock b/Cargo.lock index 7f96abfcc3c..b6655b21144 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3132,6 +3132,12 @@ dependencies = [ "litrs", ] +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + [[package]] name = "dunce" version = "1.0.5" @@ -4014,6 +4020,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fragile" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" + [[package]] name = "fsevent-sys" version = "4.1.0" @@ -5821,6 +5833,32 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "mockall" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39a6bfcc6c8c7eed5ee98b9c3e33adc726054389233e201c95dab2d41a3839d2" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.109", +] + [[package]] name = "modular-bitfield" version = "0.11.2" @@ -6698,6 +6736,32 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "predicates" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +dependencies = [ + "anstyle", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" + +[[package]] +name = "predicates-tree" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "pretty_assertions" version = "1.4.1" @@ -9748,6 +9812,7 @@ dependencies = [ "eyre", "itertools 0.14.0", "metrics", + "mockall", "reth-chainspec", "reth-codecs", "reth-db", @@ -9762,6 +9827,7 @@ dependencies = [ "reth-primitives-traits", "reth-provider", "reth-revm", + "reth-storage-errors", "reth-testing-utils", "reth-trie", "secp256k1 0.30.0", @@ -12440,6 +12506,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + [[package]] name = "test-case" version = "3.3.1" diff --git a/Cargo.toml b/Cargo.toml index db55ac3d93c..bec40a3a147 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -733,6 +733,7 @@ vergen = "9.0.4" visibility = "0.1.1" walkdir = "2.3.3" vergen-git2 = "1.0.5" +mockall = "0.13.1" # [patch.crates-io] # alloy-consensus = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" } diff --git a/crates/optimism/trie/Cargo.toml b/crates/optimism/trie/Cargo.toml index d6450b03d1c..ada9ffd06d7 100644 --- a/crates/optimism/trie/Cargo.toml +++ b/crates/optimism/trie/Cargo.toml @@ -64,7 +64,9 @@ reth-db-common.workspace = true reth-ethereum-primitives.workspace = true reth-evm-ethereum.workspace = true reth-testing-utils.workspace = true +reth-storage-errors.workspace = true secp256k1.workspace = true +mockall.workspace = true # misc serial_test.workspace = true diff --git a/crates/optimism/trie/src/api.rs b/crates/optimism/trie/src/api.rs index c43c852e5b6..41bab13c9fd 100644 --- a/crates/optimism/trie/src/api.rs +++ b/crates/optimism/trie/src/api.rs @@ -57,6 +57,14 @@ pub struct BlockStateDiff { pub post_state: HashedPostState, } +impl BlockStateDiff { + /// Extend the `BlockStateDiff` from other latest `BlockStateDiff` + pub fn extend(&mut self, other: Self) { + self.trie_updates.extend(other.trie_updates); + self.post_state.extend(other.post_state); + } +} + /// Counts of trie updates written to storage. #[derive(Debug, Clone, Default)] pub struct WriteCounts { diff --git a/crates/optimism/trie/src/db/store.rs b/crates/optimism/trie/src/db/store.rs index bd3426fad8d..0a1c3243023 100644 --- a/crates/optimism/trie/src/db/store.rs +++ b/crates/optimism/trie/src/db/store.rs @@ -120,14 +120,17 @@ impl MdbxProofsStorage { } return Ok(keys); } - // Hard delete removed entries (vv.value == None) at this exact subkey; append the rest. + // We need hard deletions at the time of pruning where we need to perform these steps: + // Hard delete all the tombstones + // Update new state to block zero (not append) let (to_delete, to_append): (Vec<_>, Vec<_>) = pairs.into_iter().partition(|(_, vv)| vv.value.0.is_none()); self.delete_dup_sorted::(tx, block_number, to_delete.into_iter().map(|(k, _)| k))?; for (k, vv) in to_append { - cur.append_dup(k, vv)?; + // For block 0, we need to update the existing entries. + cur.upsert(k, &vv)?; } Ok(keys) @@ -662,7 +665,7 @@ impl OpProofsStore for MdbxProofsStorage { return Ok(()); // Nothing to prune } - let _ = self.env.update(|tx| { + self.env.update(|tx| { // First, delete the old entries for the block range excluding block 0 self.delete_history_ranged( tx, @@ -678,12 +681,8 @@ impl OpProofsStore for MdbxProofsStorage { tx, new_earliest_block_number, new_earliest_block_ref.block.hash, - )?; - - Ok::<(), DatabaseError>(()) - })?; - - Ok(()) + ) + })? } async fn replace_updates( diff --git a/crates/optimism/trie/src/lib.rs b/crates/optimism/trie/src/lib.rs index cee57d0af82..c2e1a87a5fd 100644 --- a/crates/optimism/trie/src/lib.rs +++ b/crates/optimism/trie/src/lib.rs @@ -52,3 +52,6 @@ pub use cursor_factory::{OpProofsHashedAccountCursorFactory, OpProofsTrieCursorF pub mod error; pub use error::{OpProofsStorageError, OpProofsStorageResult}; + +mod prune; +pub use prune::{OpProofStoragePruner, OpProofStoragePrunerResult, PrunerError, PrunerOutput}; diff --git a/crates/optimism/trie/src/prune/error.rs b/crates/optimism/trie/src/prune/error.rs new file mode 100644 index 00000000000..ca43536633a --- /dev/null +++ b/crates/optimism/trie/src/prune/error.rs @@ -0,0 +1,56 @@ +use crate::OpProofsStorageError; +use reth_provider::ProviderError; +use std::{ + fmt, + fmt::{Display, Formatter}, + time::Duration, +}; +use strum::Display; +use thiserror::Error; + +/// Result of [`OpProofStoragePruner::run`] execution. +pub type OpProofStoragePrunerResult = Result; + +/// Successful prune summary. +#[derive(Debug, Clone, Default, Eq, PartialEq)] +pub struct PrunerOutput { + /// Total elapsed wall time for this run (fetch + apply). + pub duration: Duration, + /// Earliest block at the start of the run. + pub start_block: u64, + /// New earliest block at the end of the run. + pub end_block: u64, + /// Total number of entries removed across tables. + pub total_entries_pruned: u64, +} + +impl Display for PrunerOutput { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let blocks = self.end_block.saturating_sub(self.start_block); + write!( + f, + "Pruned {}→{} ({} blocks), entries={}, elapsed={:.3}s", + self.start_block, + self.end_block, + blocks, + self.total_entries_pruned, + self.duration.as_secs_f64(), + ) + } +} + +/// Error returned by the pruner. +#[derive(Debug, Error, Display)] +pub enum PrunerError { + /// Wrapped error from the underlying `OpProofStorage` layer. + Storage(#[from] OpProofsStorageError), + + /// Wrapped error from the reth db provider. + Provider(#[from] ProviderError), + + /// Block not found in the underlying reth storage provider. + BlockNotFound(u64), + + /// The pruner timed out before finishing the prune + TimedOut(Duration), +} diff --git a/crates/optimism/trie/src/prune/mod.rs b/crates/optimism/trie/src/prune/mod.rs new file mode 100644 index 00000000000..1c24d9b9488 --- /dev/null +++ b/crates/optimism/trie/src/prune/mod.rs @@ -0,0 +1,5 @@ +mod error; +pub use error::{OpProofStoragePrunerResult, PrunerError, PrunerOutput}; + +mod pruner; +pub use pruner::OpProofStoragePruner; diff --git a/crates/optimism/trie/src/prune/pruner.rs b/crates/optimism/trie/src/prune/pruner.rs new file mode 100644 index 00000000000..dc569e9803e --- /dev/null +++ b/crates/optimism/trie/src/prune/pruner.rs @@ -0,0 +1,484 @@ +use crate::{ + prune::error::{OpProofStoragePrunerResult, PrunerError, PrunerOutput}, + BlockStateDiff, OpProofsStore, +}; +use alloy_eips::{eip1898::BlockWithParent, BlockNumHash}; +use derive_more::Constructor; +use reth_provider::BlockHashReader; +use tokio::time::Instant; +use tracing::{error, info, trace}; + +/// Prunes the proof storage by calling `prune_earliest_state` on the storage provider. +#[derive(Debug, Constructor)] +pub struct OpProofStoragePruner { + // Database provider for the prune + provider: P, + /// Reader to fetch block hash by block number + block_hash_reader: H, + /// Keep at least these many recent blocks + min_block_interval: u64, + // TODO: add timeout - Maximum time for one pruner run. If `None`, no timeout. + // TODO: metrics +} + +impl OpProofStoragePruner +where + P: OpProofsStore, + H: BlockHashReader, +{ + async fn run_inner(self) -> OpProofStoragePrunerResult { + let t = Instant::now(); + // TODO: handle timeout + + let latest_block_opt = self.provider.get_latest_block_number().await?; + if latest_block_opt.is_none() { + trace!(target: "trie::pruner", "No latest blocks in the proof storage"); + return Ok(PrunerOutput::default()) + } + + let earliest_block_opt = self.provider.get_earliest_block_number().await?; + if earliest_block_opt.is_none() { + trace!(target: "trie::pruner", "No earliest blocks in the proof storage"); + return Ok(PrunerOutput::default()) + } + + let latest_block = latest_block_opt.unwrap().0; + let mut earliest_block = earliest_block_opt.unwrap().0; + if earliest_block == 0 { + // block 0 is reserved + earliest_block = 1 + } + + let interval = latest_block.saturating_sub(earliest_block); + if interval < self.min_block_interval { + trace!(target: "trie::pruner", "Nothing to prune"); + return Ok(PrunerOutput::default()) + } + + // at this point `latest_block` is always greater than `min_block_interval` + let new_earliest_block = latest_block - self.min_block_interval; + + info!( + target: "trie::pruner", + from_block = earliest_block, + to_block = new_earliest_block - 1, + "Starting pruning proof storage", + ); + + let mut final_diff = BlockStateDiff::default(); + for i in earliest_block..new_earliest_block { + let diff = self.provider.fetch_trie_updates(i).await?; + final_diff.extend(diff); + } + + let new_earliest_block_hash = self + .block_hash_reader + .block_hash(new_earliest_block)? + .ok_or(PrunerError::BlockNotFound(new_earliest_block))?; + + let parent_block_num = new_earliest_block - 1; + let parent_block_hash = self + .block_hash_reader + .block_hash(parent_block_num)? + .ok_or(PrunerError::BlockNotFound(parent_block_num))?; + + let block_with_parent = BlockWithParent { + parent: parent_block_hash, + block: BlockNumHash { number: new_earliest_block, hash: new_earliest_block_hash }, + }; + + self.provider.prune_earliest_state(block_with_parent, final_diff).await?; + + Ok(PrunerOutput { + duration: t.elapsed(), + start_block: earliest_block, + end_block: new_earliest_block - 1, + total_entries_pruned: 0, // TODO: get it from the prune_earliest_state + }) + } + + /// Run the pruner + pub async fn run(self) { + let res = self.run_inner().await; + if let Err(e) = res { + error!(target: "trie::pruner", "Pruner failed: {:?}", e); + return; + } + info!(target: "trie::pruner", result = %res.unwrap(), "Finished pruning proof storage"); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{db::MdbxProofsStorage, OpProofsHashedCursorRO, OpProofsTrieCursorRO}; + use alloy_eips::{BlockHashOrNumber, NumHash}; + use alloy_primitives::{BlockNumber, B256, U256}; + use mockall::mock; + use reth_primitives_traits::Account; + use reth_storage_errors::provider::ProviderResult; + use reth_trie::{updates::StorageTrieUpdates, BranchNodeCompact, HashedStorage, Nibbles}; + use std::sync::Arc; + use tempfile::TempDir; + + mock! ( + #[derive(Debug)] + pub BlockHashReader {} + + impl BlockHashReader for BlockHashReader { + fn block_hash(&self, number: BlockNumber) -> ProviderResult>; + + fn convert_block_hash( + &self, + _hash_or_number: BlockHashOrNumber, + ) -> ProviderResult>; + + fn canonical_hashes_range( + &self, + _start: BlockNumber, + _end: BlockNumber, + ) -> ProviderResult>; + } + ); + + fn b256(n: u64) -> B256 { + use alloy_primitives::keccak256; + keccak256(n.to_be_bytes()) + } + + /// Build a block-with-parent for number `n` with deterministic hash. + fn block(n: u64, parent: B256) -> BlockWithParent { + BlockWithParent::new(parent, NumHash::new(n, b256(n))) + } + + #[tokio::test] + async fn run_inner_and_and_verify_updated_state() { + // --- env/store --- + let dir = TempDir::new().unwrap(); + let store = Arc::new(MdbxProofsStorage::new(dir.path()).expect("env")); + store.set_earliest_block_number(0, B256::ZERO).await.expect("set earliest"); + + // --- entities --- + // accounts + let a1 = B256::from([0xA1; 32]); + let a2 = B256::from([0xA2; 32]); + let a3 = B256::from([0xA3; 32]); // introduced later + + // one storage address with 3 slots + let stor_addr = B256::from([0x10; 32]); + let s1 = B256::from([0xB1; 32]); + let s2 = B256::from([0xB2; 32]); + let s3 = B256::from([0xB3; 32]); + + // account-trie paths (p1 gets removed by block 3; p2 remains; p3 added later) + let p1 = Nibbles::from_nibbles_unchecked([0x01, 0x02]); + let p2 = Nibbles::from_nibbles_unchecked([0x03, 0x04]); + let p3 = Nibbles::from_nibbles_unchecked([0x05, 0x06]); + + let node_p1 = BranchNodeCompact::new(0b1, 0, 0, vec![], Some(B256::from([0x11; 32]))); + let node_p2 = BranchNodeCompact::new(0b10, 0, 0, vec![], Some(B256::from([0x22; 32]))); + let node_p3 = BranchNodeCompact::new(0b11, 0, 0, vec![], Some(B256::from([0x33; 32]))); + + // storage-trie paths (st1 removed by block 3; st2 remains; st3 added later) + let st1 = Nibbles::from_nibbles_unchecked([0x0A]); + let st2 = Nibbles::from_nibbles_unchecked([0x0B]); + let st3 = Nibbles::from_nibbles_unchecked([0x0C]); + + let node_st2 = BranchNodeCompact::new(0b101, 0, 0, vec![], Some(B256::from([0x44; 32]))); + let node_st3 = BranchNodeCompact::new(0b110, 0, 0, vec![], Some(B256::from([0x55; 32]))); + + // --- write 5 blocks manually --- + let mut parent = B256::ZERO; + + // Block 1: add a1,a2; s1=100, s2=200; add p1, st1 + { + let b1 = block(1, parent); + let mut d = BlockStateDiff::default(); + + d.post_state.accounts.insert( + a1, + Some(Account { nonce: 1, balance: U256::from(1_001), ..Default::default() }), + ); + d.post_state.accounts.insert( + a2, + Some(Account { nonce: 1, balance: U256::from(1_002), ..Default::default() }), + ); + + let mut hs = HashedStorage::default(); + hs.storage.insert(s1, U256::from(100)); + hs.storage.insert(s2, U256::from(200)); + d.post_state.storages.insert(stor_addr, hs); + + d.trie_updates.account_nodes.insert(p1, node_p1.clone()); + let e = d.trie_updates.storage_tries.entry(stor_addr).or_default(); + e.storage_nodes.insert(st1, BranchNodeCompact::default()); + + store.store_trie_updates(b1, d).await.expect("b1"); + parent = b256(1); + } + + // Block 2: update a2; add a3; s2=220, s3=300; add p2, st2 + { + let b2 = block(2, parent); + let mut d = BlockStateDiff::default(); + + d.post_state.accounts.insert( + a2, + Some(Account { nonce: 2, balance: U256::from(2_002), ..Default::default() }), + ); + d.post_state.accounts.insert( + a3, + Some(Account { nonce: 1, balance: U256::from(1_003), ..Default::default() }), + ); + + let mut hs = HashedStorage::default(); + hs.storage.insert(s2, U256::from(220)); + hs.storage.insert(s3, U256::from(300)); + d.post_state.storages.insert(stor_addr, hs); + + d.trie_updates.account_nodes.insert(p2, node_p2.clone()); + let e = d.trie_updates.storage_tries.entry(stor_addr).or_default(); + e.storage_nodes.insert(st2, node_st2.clone()); + + store.store_trie_updates(b2, d).await.expect("b2"); + parent = b256(2); + } + + // Block 3: delete a1; leave a2,a3; remove p1; remove st1 (storage-trie) + { + let b3 = block(3, parent); + let mut d = BlockStateDiff::default(); + + // delete a1, keep a2 & a3 values unchanged for this block + d.post_state.accounts.insert(a1, None); + + // remove account trie node p1 + d.trie_updates.removed_nodes.insert(p1); + + // remove storage-trie node st1 + let mut st_upd = StorageTrieUpdates::default(); + st_upd.removed_nodes.insert(st1); + d.trie_updates.storage_tries.insert(stor_addr, st_upd); + + store.store_trie_updates(b3, d).await.expect("b3"); + parent = b256(3); + } + + // Block 4 (kept): update a2; s1=140; add p3, st3 + { + let b4 = block(4, parent); + let mut d = BlockStateDiff::default(); + + d.post_state.accounts.insert( + a2, + Some(Account { nonce: 3, balance: U256::from(3_002), ..Default::default() }), + ); + + let mut hs = HashedStorage::default(); + hs.storage.insert(s1, U256::from(140)); + d.post_state.storages.insert(stor_addr, hs); + + d.trie_updates.account_nodes.insert(p3, node_p3.clone()); + let e = d.trie_updates.storage_tries.entry(stor_addr).or_default(); + e.storage_nodes.insert(st3, node_st3.clone()); + + store.store_trie_updates(b4, d).await.expect("b4"); + parent = b256(4); + } + + // Block 5 (kept): update a3; s3=330 + { + let b5 = block(5, parent); + let mut d = BlockStateDiff::default(); + + d.post_state.accounts.insert( + a3, + Some(Account { nonce: 2, balance: U256::from(2_003), ..Default::default() }), + ); + + let mut hs = HashedStorage::default(); + hs.storage.insert(s3, U256::from(330)); + d.post_state.storages.insert(stor_addr, hs); + + store.store_trie_updates(b5, d).await.expect("b5"); + } + + // sanity: earliest=0, latest=5 + { + let e = store.get_earliest_block_number().await.expect("earliest").expect("some"); + let l = store.get_latest_block_number().await.expect("latest").expect("some"); + assert_eq!(e.0, 0); + assert_eq!(l.0, 5); + } + + // --- prune: remove the first 3 blocks, keep 4 and 5 + // new_earliest = 5-1 = 4 + let mut block_hash_reader = MockBlockHashReader::new(); + block_hash_reader + .expect_block_hash() + .withf(move |block_num| *block_num == 4) + .returning(move |_| Ok(Some(b256(4)))); + + block_hash_reader + .expect_block_hash() + .withf(move |block_num| *block_num == 3) + .returning(move |_| Ok(Some(b256(3)))); + + let pruner = OpProofStoragePruner::new(store.clone(), block_hash_reader, 1); + let out = pruner.run_inner().await.expect("pruner ok"); + assert_eq!(out.start_block, 1); + assert_eq!(out.end_block, 3, "pruned up to 3 (inclusive); new earliest is 4"); + + // proof window moved: earliest=4, latest=5 + { + let e = store.get_earliest_block_number().await.expect("earliest").expect("some"); + let l = store.get_latest_block_number().await.expect("latest").expect("some"); + assert_eq!(e.0, 4); + assert_eq!(e.1, b256(4)); + assert_eq!(l.0, 5); + assert_eq!(l.1, b256(5)); + } + + // --- DB checks + let mut acc_cur = store.account_hashed_cursor(4).expect("acc cur"); + let mut stor_cur = store.storage_hashed_cursor(stor_addr, 4).expect("stor cur"); + let mut acc_trie_cur = store.account_trie_cursor(4).expect("acc trie cur"); + let mut stor_trie_cur = store.storage_trie_cursor(stor_addr, 4).expect("stor trie cur"); + + // Check these histories have been removed + let pruned_hashed_account = a1; + let pruned_trie_accounts = p1; + let pruned_trie_storage = st1; + + assert_ne!( + acc_cur.seek(pruned_hashed_account).expect("seek").unwrap().0, + pruned_hashed_account, + "deleted account must not exist in earliest snapshot" + ); + assert_ne!( + acc_trie_cur.seek(pruned_trie_accounts).expect("seek").unwrap().0, + pruned_trie_accounts, + "deleted account trie must not exist in earliest snapshot" + ); + assert_ne!( + stor_trie_cur.seek(pruned_trie_storage).expect("seek").unwrap().0, + pruned_trie_storage, + "deleted storage trie must not exist in earliest snapshot" + ); + + // Check these histories have been updated - till block 4 + let updated_hashed_accounts = vec![ + (a2, Account { nonce: 3, balance: U256::from(3_002), ..Default::default() }), /* block 4 */ + (a3, Account { nonce: 1, balance: U256::from(1_003), ..Default::default() }), /* block 2 */ + ]; + let updated_hashed_storage = vec![ + (s1, U256::from(140)), // block 4 + (s2, U256::from(220)), // block 2 + (s3, U256::from(300)), // block 2 + ]; + let updated_trie_accounts = vec![ + (p2, node_p2.clone()), // block 2 + (p3, node_p3.clone()), // block 4 + ]; + let updated_trie_storage = vec![ + (st2, node_st2.clone()), // block 2 + (st3, node_st3.clone()), // block 4 + ]; + + for (key, val) in updated_hashed_accounts { + let (k, vv) = acc_cur.seek(key).expect("seek").unwrap(); + assert_eq!(key, k, "key must exist"); + assert_eq!(val, vv, "value must be updated"); + } + + for (key, val) in updated_hashed_storage { + let (k, vv) = stor_cur.seek(key).expect("seek").unwrap(); + assert_eq!(key, k, "key must exist"); + assert_eq!(val, vv, "value must be updated"); + } + + for (key, val) in updated_trie_accounts { + let (k, vv) = acc_trie_cur.seek(key).expect("seek").unwrap(); + assert_eq!(key, k, "key must exist"); + assert_eq!(val, vv, "value must be updated"); + } + for (key, val) in updated_trie_storage { + let (k, vv) = stor_trie_cur.seek(key).expect("seek").unwrap(); + assert_eq!(key, k, "key must exist"); + assert_eq!(val, vv, "value must be updated"); + } + } + + // Both latest and earliest blocks are None -> early return default; DB untouched. + #[tokio::test] + async fn run_inner_where_latest_block_is_none() { + let dir = TempDir::new().unwrap(); + let store = Arc::new(MdbxProofsStorage::new(dir.path()).expect("env")); + + let earliest = store.get_earliest_block_number().await.unwrap(); + let latest = store.get_latest_block_number().await.unwrap(); + println!("{:?} {:?}", earliest, latest); + assert!(earliest.is_none()); + assert!(latest.is_none()); + + let block_hash_reader = MockBlockHashReader::new(); + let pruner = OpProofStoragePruner::new(store, block_hash_reader, 10); + let out = pruner.run_inner().await.expect("ok"); + assert_eq!(out, PrunerOutput::default(), "should early-return default output"); + } + + // The earliest block is None, but the latest block exists -> early return default. + #[tokio::test] + async fn run_inner_earliest_none_real_db() { + use crate::BlockStateDiff; + + let dir = TempDir::new().unwrap(); + let store = Arc::new(MdbxProofsStorage::new(dir.path()).expect("env")); + + // Write a single block to set *latest* only. + store + .store_trie_updates(block(3, B256::ZERO), BlockStateDiff::default()) + .await + .expect("store b1"); + + let earliest = store.get_earliest_block_number().await.unwrap(); + let latest = store.get_latest_block_number().await.unwrap(); + assert!(earliest.is_none(), "earliest must remain None"); + assert_eq!(latest.unwrap().0, 3); + + let block_hash_reader = MockBlockHashReader::new(); + let pruner = OpProofStoragePruner::new(store, block_hash_reader, 1); + let out = pruner.run_inner().await.expect("ok"); + assert_eq!(out, PrunerOutput::default(), "should early-return default output"); + } + + // interval < min_block_interval -> "Nothing to prune" path; default output. + #[tokio::test] + async fn run_inner_interval_too_small_real_db() { + use crate::BlockStateDiff; + + let dir = TempDir::new().unwrap(); + let store = Arc::new(MdbxProofsStorage::new(dir.path()).expect("env")); + + // Set earliest=4 explicitly + let earliest_num = 4u64; + let h4 = b256(4); + store.set_earliest_block_number(earliest_num, h4).await.expect("set earliest"); + + // Set latest=5 by storing block 5 + let b5 = block(5, h4); + store.store_trie_updates(b5, BlockStateDiff::default()).await.expect("store b5"); + + // Sanity: earliest=4, latest=5 => interval=1 + let e = store.get_earliest_block_number().await.unwrap().unwrap(); + let l = store.get_latest_block_number().await.unwrap().unwrap(); + assert_eq!(e.0, 4); + assert_eq!(l.0, 5); + + // Require min_block_interval=2 (or greater) so interval < min + let block_hash_reader = MockBlockHashReader::new(); + let pruner = OpProofStoragePruner::new(store, block_hash_reader, 2); + let out = pruner.run_inner().await.expect("ok"); + assert_eq!(out, PrunerOutput::default(), "no pruning should occur"); + } +} From 615396c344af5ea76ddac92cc2d99e2557df2a1e Mon Sep 17 00:00:00 2001 From: Sadiqur Rahman Date: Tue, 18 Nov 2025 18:36:49 +0600 Subject: [PATCH 2/4] Update crates/optimism/trie/src/db/store.rs Co-authored-by: Emilia Hane --- crates/optimism/trie/src/db/store.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/optimism/trie/src/db/store.rs b/crates/optimism/trie/src/db/store.rs index 0a1c3243023..fe93fc63705 100644 --- a/crates/optimism/trie/src/db/store.rs +++ b/crates/optimism/trie/src/db/store.rs @@ -121,8 +121,8 @@ impl MdbxProofsStorage { return Ok(keys); } // We need hard deletions at the time of pruning where we need to perform these steps: - // Hard delete all the tombstones - // Update new state to block zero (not append) + // - Hard delete all the tombstones + // - Update new state to block zero (not append) let (to_delete, to_append): (Vec<_>, Vec<_>) = pairs.into_iter().partition(|(_, vv)| vv.value.0.is_none()); From f69377726dc26647265f4a9d5afe339c7abb267a Mon Sep 17 00:00:00 2001 From: Sadiqur Rahman Date: Tue, 18 Nov 2025 18:37:09 +0600 Subject: [PATCH 3/4] Update crates/optimism/trie/src/api.rs Co-authored-by: Emilia Hane --- crates/optimism/trie/src/api.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/optimism/trie/src/api.rs b/crates/optimism/trie/src/api.rs index 41bab13c9fd..6894ae7ae2a 100644 --- a/crates/optimism/trie/src/api.rs +++ b/crates/optimism/trie/src/api.rs @@ -58,7 +58,7 @@ pub struct BlockStateDiff { } impl BlockStateDiff { - /// Extend the `BlockStateDiff` from other latest `BlockStateDiff` + /// Extend the [` BlockStateDiff`] from other latest [`BlockStateDiff`] pub fn extend(&mut self, other: Self) { self.trie_updates.extend(other.trie_updates); self.post_state.extend(other.post_state); From 6c25416acf49b6db07dcbe06f31f6a258357c089 Mon Sep 17 00:00:00 2001 From: Sadiqur Rahman Date: Tue, 18 Nov 2025 18:37:29 +0600 Subject: [PATCH 4/4] Update crates/optimism/trie/src/prune/pruner.rs Co-authored-by: Emilia Hane --- crates/optimism/trie/src/prune/pruner.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/optimism/trie/src/prune/pruner.rs b/crates/optimism/trie/src/prune/pruner.rs index dc569e9803e..4b4f19d47ad 100644 --- a/crates/optimism/trie/src/prune/pruner.rs +++ b/crates/optimism/trie/src/prune/pruner.rs @@ -101,7 +101,7 @@ where pub async fn run(self) { let res = self.run_inner().await; if let Err(e) = res { - error!(target: "trie::pruner", "Pruner failed: {:?}", e); + error!(target: "trie::pruner", err=%e, "Pruner failed"); return; } info!(target: "trie::pruner", result = %res.unwrap(), "Finished pruning proof storage");