From 0c9309dc7b1b2ccd1eba6ff09c40ac5a8a2df8a4 Mon Sep 17 00:00:00 2001 From: Arun Dhyani Date: Wed, 15 Oct 2025 18:51:45 +0530 Subject: [PATCH 01/10] feat: store hashed account and storage --- Cargo.lock | 1 + crates/optimism/trie/Cargo.toml | 1 + crates/optimism/trie/src/db/models/mod.rs | 10 +- crates/optimism/trie/src/db/models/storage.rs | 65 ++++++--- crates/optimism/trie/src/db/store.rs | 123 ++++++++++++++++-- 5 files changed, 163 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 477612f8b74..bbac33e6d3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9617,6 +9617,7 @@ dependencies = [ "reth-primitives-traits", "reth-trie", "serde", + "tempfile", "test-case", "thiserror 2.0.16", "tokio", diff --git a/crates/optimism/trie/Cargo.toml b/crates/optimism/trie/Cargo.toml index 3649158d771..667ba94a2d5 100644 --- a/crates/optimism/trie/Cargo.toml +++ b/crates/optimism/trie/Cargo.toml @@ -40,3 +40,4 @@ tokio = { workspace = true, features = ["test-util", "rt-multi-thread", "macros" test-case.workspace = true reth-db = { workspace = true, features = ["test-utils"] } reth-trie = { workspace = true, features = ["test-utils"] } +tempfile.workspace = true \ No newline at end of file diff --git a/crates/optimism/trie/src/db/models/mod.rs b/crates/optimism/trie/src/db/models/mod.rs index d5ea48e7f31..e6d11bcdfbb 100644 --- a/crates/optimism/trie/src/db/models/mod.rs +++ b/crates/optimism/trie/src/db/models/mod.rs @@ -12,7 +12,7 @@ pub use version::*; mod storage; pub use storage::*; -use alloy_primitives::B256; +use alloy_primitives::{B256, U256}; use reth_db::{ table::{DupSort, TableInfo}, tables, TableSet, TableType, TableViewer, @@ -37,7 +37,7 @@ tables! { /// Each entry is identified by a composite key combining the account’s hashed address and the /// compact-encoded trie path. Versions are tracked using block numbers as subkeys. table StorageTrieHistory { - type Key = StorageTrieSubKey; + type Key = StorageTrieKey; type Value = VersionedValue; type SubKey = u64; // block number } @@ -48,7 +48,7 @@ tables! { /// code hash, storage root). table HashedAccountHistory { type Key = B256; - type Value = VersionedValue>; + type Value = VersionedValue; type SubKey = u64; // block number } @@ -57,8 +57,8 @@ tables! { /// Each entry maps a composite key of (hashed address, storage key) to its stored value. /// Used for reconstructing contract storage at any historical block height. table HashedStorageHistory { - type Key = HashedStorageSubKey; - type Value = VersionedValue; + type Key = HashedStorageKey; + type Value = VersionedValue; type SubKey = u64; // block number } diff --git a/crates/optimism/trie/src/db/models/storage.rs b/crates/optimism/trie/src/db/models/storage.rs index 136b474b273..6648466e610 100644 --- a/crates/optimism/trie/src/db/models/storage.rs +++ b/crates/optimism/trie/src/db/models/storage.rs @@ -1,8 +1,9 @@ -use alloy_primitives::B256; +use alloy_primitives::{B256, U256}; use reth_db::{ - table::{Decode, Encode}, + table::{Decode, Encode, Compress, Decompress}, DatabaseError, }; +use derive_more::{Constructor, From, Into}; use reth_trie::StoredNibbles; use serde::{Deserialize, Serialize}; @@ -11,21 +12,21 @@ use serde::{Deserialize, Serialize}; /// Used to efficiently index storage branches by both account address and trie path. /// The encoding ensures lexicographic ordering: first by address, then by path. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -pub struct StorageTrieSubKey { +pub struct StorageTrieKey { /// Hashed account address pub hashed_address: B256, /// Trie path as nibbles pub path: StoredNibbles, } -impl StorageTrieSubKey { +impl StorageTrieKey { /// Create a new storage branch key pub const fn new(hashed_address: B256, path: StoredNibbles) -> Self { Self { hashed_address, path } } } -impl Encode for StorageTrieSubKey { +impl Encode for StorageTrieKey { type Encoded = Vec; fn encode(self) -> Self::Encoded { @@ -38,7 +39,7 @@ impl Encode for StorageTrieSubKey { } } -impl Decode for StorageTrieSubKey { +impl Decode for StorageTrieKey { fn decode(value: &[u8]) -> Result { if value.len() < 32 { return Err(DatabaseError::Decode); @@ -59,21 +60,21 @@ impl Decode for StorageTrieSubKey { /// Used to efficiently index storage values by both account address and storage key. /// The encoding ensures lexicographic ordering: first by address, then by storage key. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -pub struct HashedStorageSubKey { +pub struct HashedStorageKey { /// Hashed account address pub hashed_address: B256, /// Hashed storage key pub hashed_storage_key: B256, } -impl HashedStorageSubKey { +impl HashedStorageKey { /// Create a new hashed storage key pub const fn new(hashed_address: B256, hashed_storage_key: B256) -> Self { Self { hashed_address, hashed_storage_key } } } -impl Encode for HashedStorageSubKey { +impl Encode for HashedStorageKey { type Encoded = [u8; 64]; fn encode(self) -> Self::Encoded { @@ -86,7 +87,7 @@ impl Encode for HashedStorageSubKey { } } -impl Decode for HashedStorageSubKey { +impl Decode for HashedStorageKey { fn decode(value: &[u8]) -> Result { if value.len() != 64 { return Err(DatabaseError::Decode); @@ -99,6 +100,28 @@ impl Decode for HashedStorageSubKey { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, From, Into, Constructor)] +pub struct StorageValue (pub U256); + +impl Compress for StorageValue { + type Compressed = Vec; + + fn compress_to_buf>(&self, buf: &mut B) { + let be: [u8; 32] = self.0.to_be_bytes::<32>(); + buf.put_slice(&be); + } +} + +impl Decompress for StorageValue { + fn decompress(value: &[u8]) -> Result { + if value.len() != 32 { + return Err(DatabaseError::Decode); + } + let bytes: [u8; 32] = value.try_into().map_err(|_| DatabaseError::Decode)?; + Ok(Self(U256::from_be_bytes(bytes))) + } +} + /// Proof Window key for tracking active proof window bounds /// /// Used to store earliest and latest block numbers in the external storage. @@ -138,10 +161,10 @@ mod tests { fn test_storage_branch_subkey_encode_decode() { let addr = B256::from([1u8; 32]); let path = StoredNibbles(Nibbles::from_nibbles_unchecked([1, 2, 3, 4])); - let key = StorageTrieSubKey::new(addr, path.clone()); + let key = StorageTrieKey::new(addr, path.clone()); let encoded = key.clone().encode(); - let decoded = StorageTrieSubKey::decode(&encoded).unwrap(); + let decoded = StorageTrieKey::decode(&encoded).unwrap(); assert_eq!(key, decoded); assert_eq!(decoded.hashed_address, addr); @@ -155,9 +178,9 @@ mod tests { let path1 = StoredNibbles(Nibbles::from_nibbles_unchecked([1, 2])); let path2 = StoredNibbles(Nibbles::from_nibbles_unchecked([1, 3])); - let key1 = StorageTrieSubKey::new(addr1, path1.clone()); - let key2 = StorageTrieSubKey::new(addr1, path2); - let key3 = StorageTrieSubKey::new(addr2, path1); + let key1 = StorageTrieKey::new(addr1, path1.clone()); + let key2 = StorageTrieKey::new(addr1, path2); + let key3 = StorageTrieKey::new(addr2, path1); // Encoded bytes should be sortable: first by address, then by path let enc1 = key1.encode(); @@ -173,10 +196,10 @@ mod tests { fn test_hashed_storage_subkey_encode_decode() { let addr = B256::from([1u8; 32]); let storage_key = B256::from([2u8; 32]); - let key = HashedStorageSubKey::new(addr, storage_key); + let key = HashedStorageKey::new(addr, storage_key); let encoded = key.clone().encode(); - let decoded = HashedStorageSubKey::decode(&encoded).unwrap(); + let decoded = HashedStorageKey::decode(&encoded).unwrap(); assert_eq!(key, decoded); assert_eq!(decoded.hashed_address, addr); @@ -190,9 +213,9 @@ mod tests { let storage1 = B256::from([10u8; 32]); let storage2 = B256::from([20u8; 32]); - let key1 = HashedStorageSubKey::new(addr1, storage1); - let key2 = HashedStorageSubKey::new(addr1, storage2); - let key3 = HashedStorageSubKey::new(addr2, storage1); + let key1 = HashedStorageKey::new(addr1, storage1); + let key2 = HashedStorageKey::new(addr1, storage2); + let key3 = HashedStorageKey::new(addr2, storage1); // Encoded bytes should be sortable: first by address, then by storage key let enc1 = key1.encode(); @@ -208,7 +231,7 @@ mod tests { fn test_hashed_storage_subkey_size() { let addr = B256::from([1u8; 32]); let storage_key = B256::from([2u8; 32]); - let key = HashedStorageSubKey::new(addr, storage_key); + let key = HashedStorageKey::new(addr, storage_key); let encoded = key.encode(); assert_eq!(encoded.len(), 64, "Encoded size should be exactly 64 bytes"); diff --git a/crates/optimism/trie/src/db/store.rs b/crates/optimism/trie/src/db/store.rs index 57d821de982..883c727bf3e 100644 --- a/crates/optimism/trie/src/db/store.rs +++ b/crates/optimism/trie/src/db/store.rs @@ -1,11 +1,15 @@ use crate::{ - db::{MdbxAccountCursor, MdbxStorageCursor, MdbxTrieCursor}, + db::{ + models::{HashedAccountHistory, HashedStorageHistory, HashedStorageKey, MaybeDeleted, VersionedValue, StorageValue}, + MdbxAccountCursor, MdbxStorageCursor, MdbxTrieCursor, + }, BlockStateDiff, OpProofsStorage, OpProofsStorageError, OpProofsStorageResult, }; use alloy_primitives::{map::HashMap, B256, U256}; use reth_db::{ + cursor::DbDupCursorRW, mdbx::{init_db_for, DatabaseArguments}, - DatabaseEnv, + Database, DatabaseEnv, }; use reth_primitives_traits::Account; use reth_trie::{BranchNodeCompact, Nibbles}; @@ -14,7 +18,7 @@ use std::path::Path; /// MDBX implementation of `OpProofsStorage`. #[derive(Debug)] pub struct MdbxProofsStorage { - _env: DatabaseEnv, + env: DatabaseEnv, } impl MdbxProofsStorage { @@ -22,7 +26,7 @@ impl MdbxProofsStorage { pub fn new(path: &Path) -> Result { let env = init_db_for::<_, super::models::Tables>(path, DatabaseArguments::default()) .map_err(OpProofsStorageError::Other)?; - Ok(Self { _env: env }) + Ok(Self { env }) } } @@ -50,19 +54,60 @@ impl OpProofsStorage for MdbxProofsStorage { async fn store_hashed_accounts( &self, - _accounts: Vec<(B256, Option)>, - _block_number: u64, + accounts: Vec<(B256, Option)>, + block_number: u64, ) -> OpProofsStorageResult<()> { - unimplemented!() + let mut accounts = accounts; + if accounts.is_empty() { + return Ok(()); + } + + // sort the accounts by key to ensure insertion is efficient + accounts.sort_by_key(|(key, _)| *key); + + self.env + .update(|tx| { + let mut cursor = tx + .new_cursor::() + .map_err(|err| OpProofsStorageError::Other(err.into()))?; + for (key, account) in accounts { + let vv = VersionedValue { block_number, value: MaybeDeleted(account) }; + cursor.append_dup(key, vv) + .map_err(|err| OpProofsStorageError::Other(err.into()))?; + } + Ok(()) + }) + .map_err(|err| OpProofsStorageError::Other(err.into()))? } async fn store_hashed_storages( &self, - _hashed_address: B256, - _storages: Vec<(B256, U256)>, - _block_number: u64, + hashed_address: B256, + storages: Vec<(B256, U256)>, + block_number: u64, ) -> OpProofsStorageResult<()> { - unimplemented!() + let mut storages = storages; + if storages.is_empty() { + return Ok(()); + } + + // sort the storages by key to ensure insertion is efficient + storages.sort_by_key(|(key, _)| *key); + + self.env + .update(|tx| { + let mut cursor = tx + .new_cursor::() + .map_err(|err| OpProofsStorageError::Other(err.into()))?; + for (key, value) in storages { + let vv = VersionedValue { block_number, value: MaybeDeleted(Some(StorageValue(value))) }; + let storage_key = HashedStorageKey::new(hashed_address, key); + cursor.append_dup(storage_key, vv) + .map_err(|err| OpProofsStorageError::Other(err.into()))?; + } + Ok(()) + }) + .map_err(|err| OpProofsStorageError::Other(err.into()))? } async fn get_earliest_block_number(&self) -> OpProofsStorageResult> { @@ -135,3 +180,59 @@ impl OpProofsStorage for MdbxProofsStorage { unimplemented!() } } + +#[cfg(test)] +mod tests { + use super::*; + use reth_db::cursor::DbDupCursorRO; + use tempfile::TempDir; + + #[tokio::test] + async fn store_hashed_accounts_writes_versioned_values() { + let dir = TempDir::new().unwrap(); + let store = MdbxProofsStorage::new(dir.path()).expect("env"); + + let block_number = 0; + let addr = B256::from([0xAA; 32]); + // Write a deletion (None) and ensure it’s persisted as MaybeDeleted(None) + store + .store_hashed_accounts(vec![(addr, None)], block_number) + .await + .expect("write accounts"); + + // Read back via RO dup cursor + let tx = store.env.tx().expect("ro tx"); + let mut cur = tx.new_cursor::().expect("cursor"); + let vv = cur.seek_by_key_subkey(addr, block_number).expect("seek"); + let vv = vv.expect("entry exists"); + + assert_eq!(vv.block_number, block_number); + assert!(vv.value.0.is_none(), "expected MaybeDeleted(None)"); + } + + #[tokio::test] + async fn store_hashed_storages_writes_versioned_values() { + let dir = TempDir::new().unwrap(); + let store = MdbxProofsStorage::new(dir.path()).expect("env"); + + let block_number = 0; + let addr = B256::from([0x11; 32]); + let slot = B256::from([0x22; 32]); + let val = U256::from(0x1234u64); + + store + .store_hashed_storages(addr, vec![(slot, val)], block_number) + .await + .expect("write storage"); + + let tx = store.env.tx().expect("ro tx"); + let mut cur = tx.new_cursor::().expect("cursor"); + let key = HashedStorageKey::new(addr, slot); + let vv = cur.seek_by_key_subkey(key, block_number).expect("seek"); + let vv = vv.expect("entry exists"); + + assert_eq!(vv.block_number, block_number); + let inner = vv.value.0.as_ref().expect("Some(StorageValue)"); + assert_eq!(inner.0, val); + } +} \ No newline at end of file From 47b141d3c1cb50024f2b199feeb166313a0345e0 Mon Sep 17 00:00:00 2001 From: Arun Dhyani Date: Wed, 15 Oct 2025 19:47:37 +0530 Subject: [PATCH 02/10] improvements --- crates/optimism/trie/src/db/models/mod.rs | 2 +- crates/optimism/trie/src/db/models/storage.rs | 1 + crates/optimism/trie/src/db/store.rs | 9 ++++----- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/optimism/trie/src/db/models/mod.rs b/crates/optimism/trie/src/db/models/mod.rs index e6d11bcdfbb..604d545ac52 100644 --- a/crates/optimism/trie/src/db/models/mod.rs +++ b/crates/optimism/trie/src/db/models/mod.rs @@ -12,7 +12,7 @@ pub use version::*; mod storage; pub use storage::*; -use alloy_primitives::{B256, U256}; +use alloy_primitives::B256; use reth_db::{ table::{DupSort, TableInfo}, tables, TableSet, TableType, TableViewer, diff --git a/crates/optimism/trie/src/db/models/storage.rs b/crates/optimism/trie/src/db/models/storage.rs index 6648466e610..186d6fba4c8 100644 --- a/crates/optimism/trie/src/db/models/storage.rs +++ b/crates/optimism/trie/src/db/models/storage.rs @@ -100,6 +100,7 @@ impl Decode for HashedStorageKey { } } +/// Storage value wrapper for U256 values #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, From, Into, Constructor)] pub struct StorageValue (pub U256); diff --git a/crates/optimism/trie/src/db/store.rs b/crates/optimism/trie/src/db/store.rs index 883c727bf3e..36168972219 100644 --- a/crates/optimism/trie/src/db/store.rs +++ b/crates/optimism/trie/src/db/store.rs @@ -194,20 +194,19 @@ mod tests { let block_number = 0; let addr = B256::from([0xAA; 32]); - // Write a deletion (None) and ensure it’s persisted as MaybeDeleted(None) + let account = Account::default(); store - .store_hashed_accounts(vec![(addr, None)], block_number) + .store_hashed_accounts(vec![(addr, Some(account))], block_number) .await .expect("write accounts"); - // Read back via RO dup cursor let tx = store.env.tx().expect("ro tx"); let mut cur = tx.new_cursor::().expect("cursor"); let vv = cur.seek_by_key_subkey(addr, block_number).expect("seek"); let vv = vv.expect("entry exists"); assert_eq!(vv.block_number, block_number); - assert!(vv.value.0.is_none(), "expected MaybeDeleted(None)"); + assert_eq!(vv.value.0, Some(account)); } #[tokio::test] @@ -235,4 +234,4 @@ mod tests { let inner = vv.value.0.as_ref().expect("Some(StorageValue)"); assert_eq!(inner.0, val); } -} \ No newline at end of file +} From d84a4fdeaf18d14bf3dbf25db7c35843a5e45250 Mon Sep 17 00:00:00 2001 From: Arun Dhyani Date: Wed, 15 Oct 2025 20:18:39 +0530 Subject: [PATCH 03/10] test cases added --- crates/optimism/trie/Cargo.toml | 2 +- crates/optimism/trie/src/db/models/storage.rs | 6 +- crates/optimism/trie/src/db/store.rs | 101 ++++++++++++++---- 3 files changed, 87 insertions(+), 22 deletions(-) diff --git a/crates/optimism/trie/Cargo.toml b/crates/optimism/trie/Cargo.toml index 318a07e9a9a..574deb1ad0c 100644 --- a/crates/optimism/trie/Cargo.toml +++ b/crates/optimism/trie/Cargo.toml @@ -41,4 +41,4 @@ test-case.workspace = true reth-db = { workspace = true, features = ["test-utils"] } reth-db-api = { workspace = true, features = ["test-utils"] } reth-trie = { workspace = true, features = ["test-utils"] } -tempfile.workspace = true \ No newline at end of file +tempfile.workspace = true diff --git a/crates/optimism/trie/src/db/models/storage.rs b/crates/optimism/trie/src/db/models/storage.rs index 186d6fba4c8..d573681d42c 100644 --- a/crates/optimism/trie/src/db/models/storage.rs +++ b/crates/optimism/trie/src/db/models/storage.rs @@ -1,9 +1,9 @@ use alloy_primitives::{B256, U256}; +use derive_more::{Constructor, From, Into}; use reth_db::{ - table::{Decode, Encode, Compress, Decompress}, + table::{Compress, Decode, Decompress, Encode}, DatabaseError, }; -use derive_more::{Constructor, From, Into}; use reth_trie::StoredNibbles; use serde::{Deserialize, Serialize}; @@ -102,7 +102,7 @@ impl Decode for HashedStorageKey { /// Storage value wrapper for U256 values #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, From, Into, Constructor)] -pub struct StorageValue (pub U256); +pub struct StorageValue(pub U256); impl Compress for StorageValue { type Compressed = Vec; diff --git a/crates/optimism/trie/src/db/store.rs b/crates/optimism/trie/src/db/store.rs index 36168972219..5e735bd38a8 100644 --- a/crates/optimism/trie/src/db/store.rs +++ b/crates/optimism/trie/src/db/store.rs @@ -1,6 +1,9 @@ use crate::{ db::{ - models::{HashedAccountHistory, HashedStorageHistory, HashedStorageKey, MaybeDeleted, VersionedValue, StorageValue}, + models::{ + HashedAccountHistory, HashedStorageHistory, HashedStorageKey, MaybeDeleted, + StorageValue, VersionedValue, + }, MdbxAccountCursor, MdbxStorageCursor, MdbxTrieCursor, }, BlockStateDiff, OpProofsStorage, OpProofsStorageError, OpProofsStorageResult, @@ -72,7 +75,8 @@ impl OpProofsStorage for MdbxProofsStorage { .map_err(|err| OpProofsStorageError::Other(err.into()))?; for (key, account) in accounts { let vv = VersionedValue { block_number, value: MaybeDeleted(account) }; - cursor.append_dup(key, vv) + cursor + .append_dup(key, vv) .map_err(|err| OpProofsStorageError::Other(err.into()))?; } Ok(()) @@ -100,9 +104,13 @@ impl OpProofsStorage for MdbxProofsStorage { .new_cursor::() .map_err(|err| OpProofsStorageError::Other(err.into()))?; for (key, value) in storages { - let vv = VersionedValue { block_number, value: MaybeDeleted(Some(StorageValue(value))) }; + let vv = VersionedValue { + block_number, + value: MaybeDeleted(Some(StorageValue(value))), + }; let storage_key = HashedStorageKey::new(hashed_address, key); - cursor.append_dup(storage_key, vv) + cursor + .append_dup(storage_key, vv) .map_err(|err| OpProofsStorageError::Other(err.into()))?; } Ok(()) @@ -187,51 +195,108 @@ mod tests { use reth_db::cursor::DbDupCursorRO; use tempfile::TempDir; + const B0: u64 = 0; + #[tokio::test] async fn store_hashed_accounts_writes_versioned_values() { let dir = TempDir::new().unwrap(); let store = MdbxProofsStorage::new(dir.path()).expect("env"); - let block_number = 0; let addr = B256::from([0xAA; 32]); let account = Account::default(); - store - .store_hashed_accounts(vec![(addr, Some(account))], block_number) - .await - .expect("write accounts"); + store.store_hashed_accounts(vec![(addr, Some(account))], B0).await.expect("write accounts"); let tx = store.env.tx().expect("ro tx"); let mut cur = tx.new_cursor::().expect("cursor"); - let vv = cur.seek_by_key_subkey(addr, block_number).expect("seek"); + let vv = cur.seek_by_key_subkey(addr, B0).expect("seek"); let vv = vv.expect("entry exists"); - assert_eq!(vv.block_number, block_number); + assert_eq!(vv.block_number, B0); assert_eq!(vv.value.0, Some(account)); } + #[tokio::test] + async fn store_hashed_accounts_multiple_items_unsorted() { + let dir = TempDir::new().unwrap(); + let store = MdbxProofsStorage::new(dir.path()).expect("env"); + + // Unsorted input, mixed Some/None (deletion) + let a1 = B256::from([0x01; 32]); + let a2 = B256::from([0x02; 32]); + let a3 = B256::from([0x03; 32]); + let acc1 = Account { nonce: 2, balance: U256::from(1000u64), ..Default::default() }; + let acc3 = Account { nonce: 1, balance: U256::from(10000u64), ..Default::default() }; + + store + .store_hashed_accounts(vec![(a2, None), (a1, Some(acc1)), (a3, Some(acc3))], B0) + .await + .expect("write"); + + let tx = store.env.tx().expect("ro tx"); + let mut cur = tx.new_cursor::().expect("cursor"); + + let v1 = cur.seek_by_key_subkey(a1, B0).expect("seek a1").expect("exists a1"); + assert_eq!(v1.block_number, B0); + assert_eq!(v1.value.0, Some(acc1)); + + let v2 = cur.seek_by_key_subkey(a2, B0).expect("seek a2").expect("exists a2"); + assert_eq!(v2.block_number, B0); + assert!(v2.value.0.is_none(), "a2 is none"); + + let v3 = cur.seek_by_key_subkey(a3, B0).expect("seek a3").expect("exists a3"); + assert_eq!(v3.block_number, B0); + assert_eq!(v3.value.0, Some(acc3)); + } + #[tokio::test] async fn store_hashed_storages_writes_versioned_values() { let dir = TempDir::new().unwrap(); let store = MdbxProofsStorage::new(dir.path()).expect("env"); - let block_number = 0; let addr = B256::from([0x11; 32]); let slot = B256::from([0x22; 32]); let val = U256::from(0x1234u64); - store - .store_hashed_storages(addr, vec![(slot, val)], block_number) - .await - .expect("write storage"); + store.store_hashed_storages(addr, vec![(slot, val)], B0).await.expect("write storage"); let tx = store.env.tx().expect("ro tx"); let mut cur = tx.new_cursor::().expect("cursor"); let key = HashedStorageKey::new(addr, slot); - let vv = cur.seek_by_key_subkey(key, block_number).expect("seek"); + let vv = cur.seek_by_key_subkey(key, B0).expect("seek"); let vv = vv.expect("entry exists"); - assert_eq!(vv.block_number, block_number); + assert_eq!(vv.block_number, B0); let inner = vv.value.0.as_ref().expect("Some(StorageValue)"); assert_eq!(inner.0, val); } + + #[tokio::test] + async fn store_hashed_storages_multiple_slots_unsorted() { + let dir = TempDir::new().unwrap(); + let store = MdbxProofsStorage::new(dir.path()).expect("env"); + + let addr = B256::from([0x11; 32]); + let s1 = B256::from([0x01; 32]); + let v1 = U256::from(1u64); + let s2 = B256::from([0x02; 32]); + let v2 = U256::from(2u64); + let s3 = B256::from([0x03; 32]); + let v3 = U256::from(3u64); + + store + .store_hashed_storages(addr, vec![(s2, v2), (s1, v1), (s3, v3)], B0) + .await + .expect("write"); + + let tx = store.env.tx().expect("ro tx"); + let mut cur = tx.new_cursor::().expect("cursor"); + + for (slot, expected) in [(s1, v1), (s2, v2), (s3, v3)] { + let key = HashedStorageKey::new(addr, slot); + let vv = cur.seek_by_key_subkey(key, B0).expect("seek").expect("exists"); + assert_eq!(vv.block_number, B0); + let inner = vv.value.0.as_ref().expect("Some(StorageValue)"); + assert_eq!(inner.0, expected); + } + } } From 6af2ac861f223b049c62cc522463bef0cd18dbea Mon Sep 17 00:00:00 2001 From: Arun Dhyani Date: Thu, 16 Oct 2025 12:29:48 +0530 Subject: [PATCH 04/10] added more test --- crates/optimism/trie/src/db/store.rs | 110 +++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/crates/optimism/trie/src/db/store.rs b/crates/optimism/trie/src/db/store.rs index 5e735bd38a8..0df4f45d870 100644 --- a/crates/optimism/trie/src/db/store.rs +++ b/crates/optimism/trie/src/db/store.rs @@ -248,6 +248,64 @@ mod tests { assert_eq!(v3.value.0, Some(acc3)); } + #[tokio::test] + async fn store_hashed_accounts_multiple_calls() { + let dir = TempDir::new().unwrap(); + let store = MdbxProofsStorage::new(dir.path()).expect("env"); + + // Unsorted input, mixed Some/None (deletion) + let a1 = B256::from([0x01; 32]); + let a2 = B256::from([0x02; 32]); + let a3 = B256::from([0x03; 32]); + let a4 = B256::from([0x04; 32]); + let a5 = B256::from([0x05; 32]); + let acc1 = Account { nonce: 2, balance: U256::from(1000u64), ..Default::default() }; + let acc3 = Account { nonce: 1, balance: U256::from(10000u64), ..Default::default() }; + let acc4 = Account { nonce: 5, balance: U256::from(5000u64), ..Default::default() }; + let acc5 = Account { nonce: 10, balance: U256::from(20000u64), ..Default::default() }; + + { + store + .store_hashed_accounts(vec![(a2, None), (a1, Some(acc1)), (a4, Some(acc4))], B0) + .await + .expect("write"); + + let tx = store.env.tx().expect("ro tx"); + let mut cur = tx.new_cursor::().expect("cursor"); + + let v1 = cur.seek_by_key_subkey(a1, B0).expect("seek a1").expect("exists a1"); + assert_eq!(v1.block_number, B0); + assert_eq!(v1.value.0, Some(acc1)); + + let v2 = cur.seek_by_key_subkey(a2, B0).expect("seek a2").expect("exists a2"); + assert_eq!(v2.block_number, B0); + assert!(v2.value.0.is_none(), "a2 is none"); + + let v4 = cur.seek_by_key_subkey(a4, B0).expect("seek a4").expect("exists a4"); + assert_eq!(v4.block_number, B0); + assert_eq!(v4.value.0, Some(acc4)); + } + + { + // Second call + store + .store_hashed_accounts(vec![(a5, Some(acc5)), (a3, Some(acc3))], B0) + .await + .expect("write"); + + let tx = store.env.tx().expect("ro tx"); + let mut cur = tx.new_cursor::().expect("cursor"); + + let v3 = cur.seek_by_key_subkey(a3, B0).expect("seek a3").expect("exists a3"); + assert_eq!(v3.block_number, B0); + assert_eq!(v3.value.0, Some(acc3)); + + let v5 = cur.seek_by_key_subkey(a5, B0).expect("seek a5").expect("exists a5"); + assert_eq!(v5.block_number, B0); + assert_eq!(v5.value.0, Some(acc5)); + } + } + #[tokio::test] async fn store_hashed_storages_writes_versioned_values() { let dir = TempDir::new().unwrap(); @@ -299,4 +357,56 @@ mod tests { assert_eq!(inner.0, expected); } } + + #[tokio::test] + async fn store_hashed_storages_multiple_calls() { + let dir = TempDir::new().unwrap(); + let store = MdbxProofsStorage::new(dir.path()).expect("env"); + + let addr = B256::from([0x11; 32]); + let s1 = B256::from([0x01; 32]); + let v1 = U256::from(1u64); + let s2 = B256::from([0x02; 32]); + let v2 = U256::from(2u64); + let s3 = B256::from([0x03; 32]); + let v3 = U256::from(3u64); + let s4 = B256::from([0x04; 32]); + let v4 = U256::from(4u64); + let s5 = B256::from([0x05; 32]); + let v5 = U256::from(5u64); + + { + store + .store_hashed_storages(addr, vec![(s2, v2), (s1, v1), (s5, v5)], B0) + .await + .expect("write"); + + let tx = store.env.tx().expect("ro tx"); + let mut cur = tx.new_cursor::().expect("cursor"); + + for (slot, expected) in [(s1, v1), (s2, v2), (s5, v5)] { + let key = HashedStorageKey::new(addr, slot); + let vv = cur.seek_by_key_subkey(key, B0).expect("seek").expect("exists"); + assert_eq!(vv.block_number, B0); + let inner = vv.value.0.as_ref().expect("Some(StorageValue)"); + assert_eq!(inner.0, expected); + } + } + + { + // Second call + store.store_hashed_storages(addr, vec![(s4, v4), (s3, v3)], B0).await.expect("write"); + + let tx = store.env.tx().expect("ro tx"); + let mut cur = tx.new_cursor::().expect("cursor"); + + for (slot, expected) in [(s4, v4), (s3, v3)] { + let key = HashedStorageKey::new(addr, slot); + let vv = cur.seek_by_key_subkey(key, B0).expect("seek").expect("exists"); + assert_eq!(vv.block_number, B0); + let inner = vv.value.0.as_ref().expect("Some(StorageValue)"); + assert_eq!(inner.0, expected); + } + } + } } From 292129c2c20ce4161251388ad59756668bcd6499 Mon Sep 17 00:00:00 2001 From: Arun Dhyani Date: Thu, 16 Oct 2025 13:30:57 +0530 Subject: [PATCH 05/10] error handling --- crates/optimism/trie/Cargo.toml | 2 +- crates/optimism/trie/src/api.rs | 5 +++ crates/optimism/trie/src/db/store.rs | 50 ++++++++++------------------ 3 files changed, 24 insertions(+), 33 deletions(-) diff --git a/crates/optimism/trie/Cargo.toml b/crates/optimism/trie/Cargo.toml index 574deb1ad0c..54a44b8544c 100644 --- a/crates/optimism/trie/Cargo.toml +++ b/crates/optimism/trie/Cargo.toml @@ -16,6 +16,7 @@ workspace = true reth-trie = { workspace = true, features = ["serde"] } reth-primitives-traits = { workspace = true, features = ["op"] } reth-db = { workspace = true, features = ["mdbx", "op"] } +reth-db-api = { workspace = true } # ethereum alloy-primitives.workspace = true @@ -39,6 +40,5 @@ reth-codecs = { workspace = true, features = ["test-utils"] } tokio = { workspace = true, features = ["test-util", "rt-multi-thread", "macros"] } test-case.workspace = true reth-db = { workspace = true, features = ["test-utils"] } -reth-db-api = { workspace = true, features = ["test-utils"] } reth-trie = { workspace = true, features = ["test-utils"] } tempfile.workspace = true diff --git a/crates/optimism/trie/src/api.rs b/crates/optimism/trie/src/api.rs index 8dbfbc2ac69..a1d886a3abd 100644 --- a/crates/optimism/trie/src/api.rs +++ b/crates/optimism/trie/src/api.rs @@ -2,6 +2,7 @@ use alloy_primitives::{map::HashMap, B256, U256}; use auto_impl::auto_impl; +use reth_db_api::DatabaseError; use reth_primitives_traits::Account; use reth_trie::{updates::TrieUpdates, BranchNodeCompact, HashedPostState, Nibbles}; use std::fmt::Debug; @@ -11,6 +12,10 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum OpProofsStorageError { // TODO: add more errors once we know what they are + /// Error occurred while interacting with the database. + #[error(transparent)] + DatabaseError(#[from] DatabaseError), + /// Other error #[error("Other error: {0}")] Other(eyre::Error), diff --git a/crates/optimism/trie/src/db/store.rs b/crates/optimism/trie/src/db/store.rs index 0df4f45d870..2b02d0bb03c 100644 --- a/crates/optimism/trie/src/db/store.rs +++ b/crates/optimism/trie/src/db/store.rs @@ -68,20 +68,14 @@ impl OpProofsStorage for MdbxProofsStorage { // sort the accounts by key to ensure insertion is efficient accounts.sort_by_key(|(key, _)| *key); - self.env - .update(|tx| { - let mut cursor = tx - .new_cursor::() - .map_err(|err| OpProofsStorageError::Other(err.into()))?; - for (key, account) in accounts { - let vv = VersionedValue { block_number, value: MaybeDeleted(account) }; - cursor - .append_dup(key, vv) - .map_err(|err| OpProofsStorageError::Other(err.into()))?; - } - Ok(()) - }) - .map_err(|err| OpProofsStorageError::Other(err.into()))? + self.env.update(|tx| { + let mut cursor = tx.new_cursor::()?; + for (key, account) in accounts { + let vv = VersionedValue { block_number, value: MaybeDeleted(account) }; + cursor.append_dup(key, vv)?; + } + Ok(()) + })? } async fn store_hashed_storages( @@ -98,24 +92,16 @@ impl OpProofsStorage for MdbxProofsStorage { // sort the storages by key to ensure insertion is efficient storages.sort_by_key(|(key, _)| *key); - self.env - .update(|tx| { - let mut cursor = tx - .new_cursor::() - .map_err(|err| OpProofsStorageError::Other(err.into()))?; - for (key, value) in storages { - let vv = VersionedValue { - block_number, - value: MaybeDeleted(Some(StorageValue(value))), - }; - let storage_key = HashedStorageKey::new(hashed_address, key); - cursor - .append_dup(storage_key, vv) - .map_err(|err| OpProofsStorageError::Other(err.into()))?; - } - Ok(()) - }) - .map_err(|err| OpProofsStorageError::Other(err.into()))? + self.env.update(|tx| { + let mut cursor = tx.new_cursor::()?; + for (key, value) in storages { + let vv = + VersionedValue { block_number, value: MaybeDeleted(Some(StorageValue(value))) }; + let storage_key = HashedStorageKey::new(hashed_address, key); + cursor.append_dup(storage_key, vv)?; + } + Ok(()) + })? } async fn get_earliest_block_number(&self) -> OpProofsStorageResult> { From 6651c404b4e61d4df8f995030c70b9949e4db823 Mon Sep 17 00:00:00 2001 From: Arun Dhyani Date: Thu, 16 Oct 2025 18:02:37 +0530 Subject: [PATCH 06/10] initial commit --- crates/optimism/trie/src/db/store.rs | 53 ++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/crates/optimism/trie/src/db/store.rs b/crates/optimism/trie/src/db/store.rs index da0c6cda445..67a745687bc 100644 --- a/crates/optimism/trie/src/db/store.rs +++ b/crates/optimism/trie/src/db/store.rs @@ -145,10 +145,57 @@ impl OpProofsStorage for MdbxProofsStorage { async fn store_trie_updates( &self, - _block_number: u64, - _block_state_diff: BlockStateDiff, + block_number: u64, + block_state_diff: BlockStateDiff, ) -> OpProofsStorageResult<()> { - unimplemented!() + + // Todo: save trie updates + let mut trie_updates = block_state_diff.trie_updates; + let sorted_trie_updates = trie_updates.into_sorted(); + let sorted_account_nodes = sorted_trie_updates.account_nodes; + + let mut sorted_storage_nodes = Vec::new(); + for (hashed_address, nodes) in sorted_trie_updates.storage_tries { + sorted_storage_nodes.push((hashed_address, nodes)); + } + sorted_storage_nodes.sort_by_key(|(hashed_address, _)| *hashed_address); + + let sorted_post_state = block_state_diff.post_state.into_sorted(); + let sorted_accounts = sorted_post_state + .accounts() + .accounts_sorted() + .collect::>(); + + // convert to sorted vec of (hashed_address, Vec<(storage_key, storage_value)>) + let mut sorted_storage = Vec::new(); + for (hashed_address, storage) in sorted_post_state.account_storages() { + sorted_storage.push((hashed_address, storage)); + } + sorted_storage.sort_by_key(|(hashed_address, _)| *hashed_address); + + self.env.update(|tx| { + let mut account_cursor = tx.new_cursor::()?; + for (hashed_address, account) in sorted_accounts { + let vv = VersionedValue { block_number, value: MaybeDeleted(account) }; + account_cursor.append_dup(hashed_address, vv)?; + } + + let mut storage_cursor = tx.new_cursor::()?; + for (hashed_address, storage) in sorted_storage { + // todo: handle wiped storage scenario + let storage_items = storage.storage_slots_sorted().collect::>(); + for (storage_key, storage_value) in storage_items { + let vv = VersionedValue { + block_number, + value: MaybeDeleted(Some(StorageValue(storage_value))), + }; + let key = HashedStorageKey::new(*hashed_address, storage_key); + storage_cursor.append_dup(key, vv)?; + } + } + + Ok(()) + })? } async fn fetch_trie_updates( From 40886ae5c84e2771a6e3f1e636066764780ec8d2 Mon Sep 17 00:00:00 2001 From: Arun Dhyani Date: Thu, 16 Oct 2025 21:39:20 +0530 Subject: [PATCH 07/10] trie udpate handled --- crates/optimism/trie/src/db/store.rs | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/crates/optimism/trie/src/db/store.rs b/crates/optimism/trie/src/db/store.rs index 67a745687bc..7ab04bb5d61 100644 --- a/crates/optimism/trie/src/db/store.rs +++ b/crates/optimism/trie/src/db/store.rs @@ -1,7 +1,8 @@ use crate::{ db::{ models::{ - HashedAccountHistory, HashedStorageHistory, HashedStorageKey, MaybeDeleted, + AccountTrieHistory, StorageTrieHistory, HashedAccountHistory, + HashedStorageHistory, StorageTrieKey, HashedStorageKey, MaybeDeleted, StorageValue, VersionedValue, }, MdbxAccountCursor, MdbxStorageCursor, MdbxTrieCursor, @@ -14,7 +15,7 @@ use reth_db::{ mdbx::{init_db_for, DatabaseArguments}, Database, DatabaseEnv, }; -use reth_primitives_traits::Account; +use reth_primitives_traits::{Account}; use reth_trie::{BranchNodeCompact, Nibbles}; use std::path::Path; @@ -148,10 +149,7 @@ impl OpProofsStorage for MdbxProofsStorage { block_number: u64, block_state_diff: BlockStateDiff, ) -> OpProofsStorageResult<()> { - - // Todo: save trie updates - let mut trie_updates = block_state_diff.trie_updates; - let sorted_trie_updates = trie_updates.into_sorted(); + let sorted_trie_updates = block_state_diff.trie_updates.into_sorted(); let sorted_account_nodes = sorted_trie_updates.account_nodes; let mut sorted_storage_nodes = Vec::new(); @@ -174,6 +172,22 @@ impl OpProofsStorage for MdbxProofsStorage { sorted_storage.sort_by_key(|(hashed_address, _)| *hashed_address); self.env.update(|tx| { + let mut account_trie_cursor = tx.new_cursor::()?; + for (path, node) in sorted_account_nodes { + let vv = VersionedValue { block_number, value: MaybeDeleted(node) }; + account_trie_cursor.append_dup(path.into(), vv)?; + } + + let mut storage_trie_cursor = tx.new_cursor::()?; + for (hashed_address, nodes) in sorted_storage_nodes { + // todo: handle is_deleted scenario + for (path, node) in nodes.storage_nodes { + let key = StorageTrieKey::new(hashed_address, path.into()); + let vv = VersionedValue { block_number, value: MaybeDeleted(node) }; + storage_trie_cursor.append_dup(key, vv)?; + } + } + let mut account_cursor = tx.new_cursor::()?; for (hashed_address, account) in sorted_accounts { let vv = VersionedValue { block_number, value: MaybeDeleted(account) }; From 99ca572ee06c9851c6efbbd87a5b25ae24bfe1f3 Mon Sep 17 00:00:00 2001 From: Arun Dhyani Date: Fri, 17 Oct 2025 13:20:55 +0530 Subject: [PATCH 08/10] test cases added --- crates/optimism/trie/src/api.rs | 2 +- crates/optimism/trie/src/db/store.rs | 191 +++++++++++++++++++++++++-- 2 files changed, 184 insertions(+), 9 deletions(-) diff --git a/crates/optimism/trie/src/api.rs b/crates/optimism/trie/src/api.rs index 81bbe1c3246..3879c884138 100644 --- a/crates/optimism/trie/src/api.rs +++ b/crates/optimism/trie/src/api.rs @@ -65,7 +65,7 @@ pub trait OpProofsHashedCursor: Send + Sync { } /// Diff of trie updates and post state for a block. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct BlockStateDiff { /// Trie updates for branch nodes pub trie_updates: TrieUpdates, diff --git a/crates/optimism/trie/src/db/store.rs b/crates/optimism/trie/src/db/store.rs index 7ab04bb5d61..13050bcdb40 100644 --- a/crates/optimism/trie/src/db/store.rs +++ b/crates/optimism/trie/src/db/store.rs @@ -1,9 +1,8 @@ use crate::{ db::{ models::{ - AccountTrieHistory, StorageTrieHistory, HashedAccountHistory, - HashedStorageHistory, StorageTrieKey, HashedStorageKey, MaybeDeleted, - StorageValue, VersionedValue, + AccountTrieHistory, HashedAccountHistory, HashedStorageHistory, HashedStorageKey, + MaybeDeleted, StorageTrieHistory, StorageTrieKey, StorageValue, VersionedValue, }, MdbxAccountCursor, MdbxStorageCursor, MdbxTrieCursor, }, @@ -15,7 +14,7 @@ use reth_db::{ mdbx::{init_db_for, DatabaseArguments}, Database, DatabaseEnv, }; -use reth_primitives_traits::{Account}; +use reth_primitives_traits::Account; use reth_trie::{BranchNodeCompact, Nibbles}; use std::path::Path; @@ -159,10 +158,7 @@ impl OpProofsStorage for MdbxProofsStorage { sorted_storage_nodes.sort_by_key(|(hashed_address, _)| *hashed_address); let sorted_post_state = block_state_diff.post_state.into_sorted(); - let sorted_accounts = sorted_post_state - .accounts() - .accounts_sorted() - .collect::>(); + let sorted_accounts = sorted_post_state.accounts().accounts_sorted().collect::>(); // convert to sorted vec of (hashed_address, Vec<(storage_key, storage_value)>) let mut sorted_storage = Vec::new(); @@ -248,6 +244,7 @@ impl OpProofsStorage for MdbxProofsStorage { mod tests { use super::*; use reth_db::cursor::DbDupCursorRO; + use reth_trie::{updates::StorageTrieUpdates, HashedStorage}; use tempfile::TempDir; const B0: u64 = 0; @@ -464,4 +461,182 @@ mod tests { } } } + + #[tokio::test] + async fn test_store_trie_updates_comprehensive() { + let dir = TempDir::new().unwrap(); + let store = MdbxProofsStorage::new(dir.path()).expect("env"); + + // Sample block number + const BLOCK: u64 = 42; + + // Sample addresses and keys + let addr1 = B256::from([0x11; 32]); + let addr2 = B256::from([0x22; 32]); + let slot1 = B256::from([0xA1; 32]); + let slot2 = B256::from([0xA2; 32]); + + // Sample accounts + let acc1 = Account { nonce: 1, balance: U256::from(100), ..Default::default() }; + + // Sample storage values + let val1 = U256::from(1234u64); + let val2 = U256::from(5678u64); + + // Sample trie paths + let account_path1 = Nibbles::from_nibbles_unchecked(vec![0, 1, 2, 3]); + let account_path2 = Nibbles::from_nibbles_unchecked(vec![4, 5, 6, 7]); + let removed_account_path = Nibbles::from_nibbles_unchecked(vec![7, 8, 9]); + + let account_node1 = BranchNodeCompact::default(); + let account_node2 = BranchNodeCompact::default(); + + let storage_path1 = Nibbles::from_nibbles_unchecked(vec![1, 2, 3, 4]); + let storage_path2 = Nibbles::from_nibbles_unchecked(vec![8, 9, 0, 1]); + + let storage_node1 = BranchNodeCompact::default(); + let storage_node2 = BranchNodeCompact::default(); + + // Construct test BlockStateDiff + let mut block_state_diff = BlockStateDiff::default(); + + // Add account trie nodes + block_state_diff.trie_updates.account_nodes.insert(account_path1, account_node1.clone()); + block_state_diff.trie_updates.account_nodes.insert(account_path2, account_node2.clone()); + block_state_diff.trie_updates.removed_nodes.insert(removed_account_path); + + // Add storage trie nodes for two addresses + let mut storage_nodes1 = StorageTrieUpdates::default(); + storage_nodes1.storage_nodes.insert(storage_path1, storage_node1.clone()); + block_state_diff.trie_updates.storage_tries.insert(addr1, storage_nodes1); + + let mut storage_nodes2 = StorageTrieUpdates::default(); + storage_nodes2.storage_nodes.insert(storage_path2, storage_node2.clone()); + block_state_diff.trie_updates.storage_tries.insert(addr2, storage_nodes2); + + // Add hashed accounts (one Some, one None) + block_state_diff.post_state.accounts.insert(addr1, Some(acc1)); + block_state_diff.post_state.accounts.insert(addr2, None); // Deletion + + // Add storage slots for both addresses + let mut storage1 = HashedStorage::default(); + storage1.storage.insert(slot1, val1); + block_state_diff.post_state.storages.insert(addr1, storage1); + + let mut storage2 = HashedStorage::default(); + storage2.storage.insert(slot2, val2); + block_state_diff.post_state.storages.insert(addr2, storage2); + + // Store everything + store.store_trie_updates(BLOCK, block_state_diff).await.expect("store"); + + // Verify account trie nodes + { + let tx = store.env.tx().expect("tx"); + let mut cur = tx.new_cursor::().expect("cursor"); + + // Check first node + let vv1 = + cur.seek_by_key_subkey(account_path1.into(), BLOCK).expect("seek").expect("exists"); + assert_eq!(vv1.block_number, BLOCK); + assert!(vv1.value.0.is_some()); + + // Check second node + let vv2 = + cur.seek_by_key_subkey(account_path2.into(), BLOCK).expect("seek").expect("exists"); + assert_eq!(vv2.block_number, BLOCK); + assert!(vv2.value.0.is_some()); + + // Check removed node + let vv3 = cur + .seek_by_key_subkey(removed_account_path.into(), BLOCK) + .expect("seek") + .expect("exists"); + assert_eq!(vv3.block_number, BLOCK); + assert!(vv3.value.0.is_none(), "Expected node deletion"); + } + + // Verify storage trie nodes + { + let tx = store.env.tx().expect("tx"); + let mut cur = tx.new_cursor::().expect("cursor"); + + // Check node for addr1 + let key1 = StorageTrieKey::new(addr1, storage_path1.into()); + let vv1 = cur.seek_by_key_subkey(key1, BLOCK).expect("seek").expect("exists"); + assert_eq!(vv1.block_number, BLOCK); + assert!(vv1.value.0.is_some()); + + // Check node for addr2 + let key2 = StorageTrieKey::new(addr2, storage_path2.into()); + let vv2 = cur.seek_by_key_subkey(key2, BLOCK).expect("seek").expect("exists"); + assert_eq!(vv2.block_number, BLOCK); + assert!(vv2.value.0.is_some()); + } + + // Verify hashed accounts + { + let tx = store.env.tx().expect("tx"); + let mut cur = tx.new_cursor::().expect("cursor"); + + // Check account1 (exists) + let vv1 = cur.seek_by_key_subkey(addr1, BLOCK).expect("seek").expect("exists"); + assert_eq!(vv1.block_number, BLOCK); + assert_eq!(vv1.value.0, Some(acc1)); + + // Check account2 (deletion) + let vv2 = cur.seek_by_key_subkey(addr2, BLOCK).expect("seek").expect("exists"); + assert_eq!(vv2.block_number, BLOCK); + assert!(vv2.value.0.is_none(), "Expected account deletion"); + } + + // Verify hashed storages + { + let tx = store.env.tx().expect("tx"); + let mut cur = tx.new_cursor::().expect("cursor"); + + // Check storage for addr1 + let key1 = HashedStorageKey::new(addr1, slot1); + let vv1 = cur.seek_by_key_subkey(key1, BLOCK).expect("seek").expect("exists"); + assert_eq!(vv1.block_number, BLOCK); + let inner1 = vv1.value.0.as_ref().expect("Some(StorageValue)"); + assert_eq!(inner1.0, val1); + + // Check storage for addr2 + let key2 = HashedStorageKey::new(addr2, slot2); + let vv2 = cur.seek_by_key_subkey(key2, BLOCK).expect("seek").expect("exists"); + assert_eq!(vv2.block_number, BLOCK); + let inner2 = vv2.value.0.as_ref().expect("Some(StorageValue)"); + assert_eq!(inner2.0, val2); + } + } + + #[tokio::test] + async fn test_store_trie_updates_empty_collections() { + let dir = TempDir::new().unwrap(); + let store = MdbxProofsStorage::new(dir.path()).expect("env"); + + const BLOCK: u64 = 42; + + // Create BlockStateDiff with empty collections + let block_state_diff = BlockStateDiff::default(); + + // This should work without errors + store.store_trie_updates(BLOCK, block_state_diff).await.expect("store"); + + // Verify nothing was written (should be empty) + let tx = store.env.tx().expect("tx"); + + let mut cur1 = tx.new_cursor::().expect("cursor"); + assert!(cur1.next_dup_val().expect("first").is_none(), "Account trie should be empty"); + + let mut cur2 = tx.new_cursor::().expect("cursor"); + assert!(cur2.next_dup_val().expect("first").is_none(), "Storage trie should be empty"); + + let mut cur3 = tx.new_cursor::().expect("cursor"); + assert!(cur3.next_dup_val().expect("first").is_none(), "Hashed accounts should be empty"); + + let mut cur4 = tx.new_cursor::().expect("cursor"); + assert!(cur4.next_dup_val().expect("first").is_none(), "Hashed storage should be empty"); + } } From f00ce0a527dc152efb3150b1a6af5fc0627cba47 Mon Sep 17 00:00:00 2001 From: Arun Dhyani Date: Fri, 17 Oct 2025 13:41:24 +0530 Subject: [PATCH 09/10] improvements --- crates/optimism/trie/src/db/store.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/optimism/trie/src/db/store.rs b/crates/optimism/trie/src/db/store.rs index 13050bcdb40..81d4949a7de 100644 --- a/crates/optimism/trie/src/db/store.rs +++ b/crates/optimism/trie/src/db/store.rs @@ -158,7 +158,7 @@ impl OpProofsStorage for MdbxProofsStorage { sorted_storage_nodes.sort_by_key(|(hashed_address, _)| *hashed_address); let sorted_post_state = block_state_diff.post_state.into_sorted(); - let sorted_accounts = sorted_post_state.accounts().accounts_sorted().collect::>(); + let sorted_accounts = sorted_post_state.accounts().accounts_sorted(); // convert to sorted vec of (hashed_address, Vec<(storage_key, storage_value)>) let mut sorted_storage = Vec::new(); From 8d887ef09ccc660c9797b8c841f54fae98fe8cac Mon Sep 17 00:00:00 2001 From: Arun Dhyani Date: Fri, 17 Oct 2025 17:52:24 +0530 Subject: [PATCH 10/10] review fixes --- Cargo.lock | 1 + crates/optimism/trie/Cargo.toml | 1 + crates/optimism/trie/src/db/store.rs | 22 +++++++++++----------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 10c3d8dfc97..c2605e4caa2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9617,6 +9617,7 @@ dependencies = [ "bytes", "derive_more", "eyre", + "itertools 0.14.0", "metrics", "reth-codecs", "reth-db", diff --git a/crates/optimism/trie/Cargo.toml b/crates/optimism/trie/Cargo.toml index d73ea537dfa..e4a51cb9e28 100644 --- a/crates/optimism/trie/Cargo.toml +++ b/crates/optimism/trie/Cargo.toml @@ -44,6 +44,7 @@ eyre.workspace = true strum.workspace = true tracing.workspace = true derive_more.workspace = true +itertools.workspace = true [dev-dependencies] reth-codecs = { workspace = true, features = ["test-utils"] } diff --git a/crates/optimism/trie/src/db/store.rs b/crates/optimism/trie/src/db/store.rs index ad39a8f2e42..83c2a6246c8 100644 --- a/crates/optimism/trie/src/db/store.rs +++ b/crates/optimism/trie/src/db/store.rs @@ -9,6 +9,7 @@ use crate::{ BlockStateDiff, OpProofsStorage, OpProofsStorageError, OpProofsStorageResult, }; use alloy_primitives::{map::HashMap, B256, U256}; +use itertools::Itertools; use reth_db::{ cursor::DbDupCursorRW, mdbx::{init_db_for, DatabaseArguments}, @@ -178,21 +179,20 @@ impl OpProofsStorage for MdbxProofsStorage { let sorted_trie_updates = block_state_diff.trie_updates.into_sorted(); let sorted_account_nodes = sorted_trie_updates.account_nodes; - let mut sorted_storage_nodes = Vec::new(); - for (hashed_address, nodes) in sorted_trie_updates.storage_tries { - sorted_storage_nodes.push((hashed_address, nodes)); - } - sorted_storage_nodes.sort_by_key(|(hashed_address, _)| *hashed_address); + let sorted_storage_nodes = sorted_trie_updates + .storage_tries + .into_iter() + .sorted_by_key(|(hashed_address, _)| *hashed_address) + .collect::>(); let sorted_post_state = block_state_diff.post_state.into_sorted(); let sorted_accounts = sorted_post_state.accounts().accounts_sorted(); - // convert to sorted vec of (hashed_address, Vec<(storage_key, storage_value)>) - let mut sorted_storage = Vec::new(); - for (hashed_address, storage) in sorted_post_state.account_storages() { - sorted_storage.push((hashed_address, storage)); - } - sorted_storage.sort_by_key(|(hashed_address, _)| *hashed_address); + let sorted_storage = sorted_post_state + .account_storages() + .iter() + .sorted_by_key(|(hashed_address, _)| *hashed_address) + .collect::>(); self.env.update(|tx| { let mut account_trie_cursor = tx.new_cursor::()?;