From 8e970c86fd8e14867519e0bf6d25b22333d50478 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 29 Jan 2026 12:06:48 -0500 Subject: [PATCH 1/6] chore: remove get_many to reconsider the API --- crates/hot-mdbx/src/test_utils.rs | 15 ------ crates/hot/src/conformance.rs | 29 ------------ crates/hot/src/mem.rs | 16 ------- crates/hot/src/model/revm.rs | 26 +---------- crates/hot/src/model/traits.rs | 77 +------------------------------ crates/hot/src/model/traverse.rs | 3 +- 6 files changed, 4 insertions(+), 162 deletions(-) diff --git a/crates/hot-mdbx/src/test_utils.rs b/crates/hot-mdbx/src/test_utils.rs index 22face7..0bba8ad 100644 --- a/crates/hot-mdbx/src/test_utils.rs +++ b/crates/hot-mdbx/src/test_utils.rs @@ -329,21 +329,6 @@ mod tests { assert_eq!(read_account.as_ref(), Some(expected_account)); } } - - // Test batch get_many - { - let reader: Tx = db.reader().unwrap(); - let addresses: Vec
= accounts.iter().map(|(addr, _)| *addr).collect(); - let read_accounts: Vec<(_, Option)> = reader - .get_many::(addresses.iter()) - .into_iter() - .collect::, _>>() - .unwrap(); - - for (i, (_, expected_account)) in accounts.iter().enumerate() { - assert_eq!(read_accounts[i].1.as_ref(), Some(expected_account)); - } - } } #[test] diff --git a/crates/hot/src/conformance.rs b/crates/hot/src/conformance.rs index 7f393ac..a3aa4c1 100644 --- a/crates/hot/src/conformance.rs +++ b/crates/hot/src/conformance.rs @@ -1959,7 +1959,6 @@ pub fn test_get_many(hot_kv: &T) { let addr1 = address!("0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee01"); let addr2 = address!("0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee02"); let addr3 = address!("0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee03"); - let addr4 = address!("0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee04"); // non-existent let acc1 = Account { nonce: 1, balance: U256::from(100), bytecode_hash: None }; let acc2 = Account { nonce: 2, balance: U256::from(200), bytecode_hash: None }; @@ -1973,34 +1972,6 @@ pub fn test_get_many(hot_kv: &T) { writer.put_account(&addr3, &acc3).unwrap(); writer.commit().unwrap(); } - - // Batch retrieve - { - let reader = hot_kv.reader().unwrap(); - let keys = [addr1, addr2, addr3, addr4]; - let results = reader - .get_many::(&keys) - .into_iter() - .collect::, _>>() - .unwrap(); - - assert_eq!(results.len(), 4); - - // Build a map for easier checking (order not guaranteed) - let result_map: HashMap<&Address, Option> = - results.iter().map(|(k, v)| (*k, *v)).collect(); - - assert!(result_map[&addr1].is_some()); - assert_eq!(result_map[&addr1].as_ref().unwrap().nonce, 1); - - assert!(result_map[&addr2].is_some()); - assert_eq!(result_map[&addr2].as_ref().unwrap().nonce, 2); - - assert!(result_map[&addr3].is_some()); - assert_eq!(result_map[&addr3].as_ref().unwrap().nonce, 3); - - assert!(result_map[&addr4].is_none(), "Non-existent key should return None"); - } } /// Test queue_put_many batch writes. diff --git a/crates/hot/src/mem.rs b/crates/hot/src/mem.rs index 25a908c..4969a24 100644 --- a/crates/hot/src/mem.rs +++ b/crates/hot/src/mem.rs @@ -1519,22 +1519,6 @@ mod tests { writer.queue_put_many::(entry_refs).unwrap(); writer.raw_commit().unwrap(); } - - // Read batch - { - let reader = store.reader().unwrap(); - let keys: Vec<_> = entries.iter().map(|(k, _)| k).collect(); - let values = reader - .get_many::(keys) - .into_iter() - .collect::, _>>() - .unwrap(); - - assert_eq!(values.len(), 3); - assert_eq!(values[0], (&1u64, Some(Bytes::from_static(b"first")))); - assert_eq!(values[1], (&2u64, Some(Bytes::from_static(b"second")))); - assert_eq!(values[2], (&3u64, Some(Bytes::from_static(b"third")))); - } } #[test] diff --git a/crates/hot/src/model/revm.rs b/crates/hot/src/model/revm.rs index 5a47d1a..45ff7f3 100644 --- a/crates/hot/src/model/revm.rs +++ b/crates/hot/src/model/revm.rs @@ -1,5 +1,5 @@ use crate::{ - model::{GetManyItem, HotKvError, HotKvRead, HotKvWrite}, + model::{HotKvError, HotKvRead, HotKvWrite}, tables::{self, Bytecodes, DualKey, PlainAccountState, SingleKey, Table}, }; use alloy::primitives::{Address, B256, KECCAK256_EMPTY}; @@ -74,18 +74,6 @@ impl HotKvRead for RevmRead { ) -> Result, Self::Error> { self.reader.get_dual::(key1, key2) } - - fn get_many<'a, T, I>( - &self, - keys: I, - ) -> impl IntoIterator, Self::Error>> - where - T::Key: 'a, - T: SingleKey, - I: IntoIterator, - { - self.reader.get_many::(keys) - } } /// Read-write REVM database adapter. This adapter allows committing changes. @@ -155,18 +143,6 @@ impl HotKvRead for RevmWrite { ) -> Result, Self::Error> { self.writer.get_dual::(key1, key2) } - - fn get_many<'a, T, I>( - &self, - keys: I, - ) -> impl IntoIterator, Self::Error>> - where - T::Key: 'a, - T: SingleKey, - I: IntoIterator, - { - self.writer.get_many::(keys) - } } impl HotKvWrite for RevmWrite { diff --git a/crates/hot/src/model/traits.rs b/crates/hot/src/model/traits.rs index 5e85206..e228c09 100644 --- a/crates/hot/src/model/traits.rs +++ b/crates/hot/src/model/traits.rs @@ -1,7 +1,7 @@ use crate::{ model::{ - DualKeyTraverse, DualKeyValue, DualTableCursor, GetManyDualItem, GetManyItem, HotKvError, - HotKvReadError, KeyValue, KvTraverse, KvTraverseMut, TableCursor, + DualKeyTraverse, DualKeyValue, DualTableCursor, HotKvError, HotKvReadError, KeyValue, + KvTraverse, KvTraverseMut, TableCursor, revm::{RevmRead, RevmWrite}, }, ser::{KeySer, MAX_KEY_SIZE, ValSer}, @@ -175,79 +175,6 @@ pub trait HotKvRead { }; T::Value::decode_value(&value_bytes).map(Some).map_err(Into::into) } - - /// Get many values from a specific table. - /// - /// # Arguments - /// - /// * `keys` - An iterator over keys to retrieve. - /// - /// # Returns - /// - /// An iterator of [`KeyValue`] where each element corresponds to the value - /// for the respective key in the input iterator. If a key does not exist - /// in the table, the corresponding element will be `None`. - /// - /// Implementations ARE NOT required to preserve the order of the input - /// keys in the output iterator. Users should not rely on any specific - /// ordering. - /// - /// If any error occurs during retrieval or deserialization, the entire - /// operation will return an error. - fn get_many<'a, T, I>( - &self, - keys: I, - ) -> impl IntoIterator, Self::Error>> - where - T::Key: 'a, - T: SingleKey, - I: IntoIterator, - { - let mut key_buf = [0u8; MAX_KEY_SIZE]; - - keys.into_iter() - .map(move |key| (key, self.raw_get(T::NAME, key.encode_key(&mut key_buf)))) - .map(|(key, maybe_val)| { - maybe_val - .and_then(|val| { - ::maybe_decode_value(val.as_deref()).map_err(Into::into) - }) - .map(|res| (key, res)) - }) - } - - /// Get many values from a specific dual-keyed table. - /// - /// # Arguments - /// * `keys` - An iterator over tuples of (key1, key2) to retrieve. - /// - /// # Returns - fn get_many_dual<'a, T, I>( - &self, - keys: I, - ) -> impl IntoIterator, Self::Error>> - where - T: DualKey, - T::Key: 'a, - T::Key2: 'a, - I: IntoIterator, - { - let mut key_buf = [0u8; MAX_KEY_SIZE]; - let mut key2_buf = [0u8; MAX_KEY_SIZE]; - keys.into_iter().map(move |(key1, key2)| { - let key1_bytes = key1.encode_key(&mut key_buf); - let key2_bytes = key2.encode_key(&mut key2_buf); - - let maybe_val = self.raw_get_dual(T::NAME, key1_bytes, key2_bytes)?; - - let decoded_val = match maybe_val { - Some(val) => Some(::decode_value(&val)?), - None => None, - }; - - Ok((key1, key2, decoded_val)) - }) - } } /// Trait for hot storage write transactions. diff --git a/crates/hot/src/model/traverse.rs b/crates/hot/src/model/traverse.rs index 3d414e6..fc303d1 100644 --- a/crates/hot/src/model/traverse.rs +++ b/crates/hot/src/model/traverse.rs @@ -5,6 +5,7 @@ use crate::{ ser::{KeySer, MAX_KEY_SIZE}, tables::{DualKey, SingleKey}, }; +use core::marker::PhantomData; use std::ops::Range; /// Trait for traversing key-value pairs in the database. @@ -449,8 +450,6 @@ where // Wrapper Structs // ============================================================================ -use core::marker::PhantomData; - /// A wrapper struct for typed table traversal. /// /// This struct wraps a raw cursor and provides type-safe access to table From 26722da0453cc7d2cacf2ff68e9acaf6097853bd Mon Sep 17 00:00:00 2001 From: James Date: Thu, 29 Jan 2026 16:49:28 -0500 Subject: [PATCH 2/6] wip: improve traversal --- Cargo.toml | 3 +- crates/hot-mdbx/src/cursor.rs | 28 +- crates/hot-mdbx/src/test_utils.rs | 430 ++++ crates/hot-mdbx/src/tx.rs | 43 +- crates/hot/Cargo.toml | 1 + crates/hot/src/conformance.rs | 2534 ---------------------- crates/hot/src/conformance/cursor.rs | 218 ++ crates/hot/src/conformance/edge_cases.rs | 256 +++ crates/hot/src/conformance/history.rs | 456 ++++ crates/hot/src/conformance/mod.rs | 42 + crates/hot/src/conformance/range.rs | 492 +++++ crates/hot/src/conformance/roundtrip.rs | 291 +++ crates/hot/src/conformance/unwind.rs | 485 +++++ crates/hot/src/db/consistent.rs | 184 +- crates/hot/src/db/inconsistent.rs | 302 +-- crates/hot/src/mem.rs | 108 +- crates/hot/src/model/mod.rs | 4 +- crates/hot/src/model/traits.rs | 129 +- crates/hot/src/model/traverse.rs | 803 +++++-- 19 files changed, 3680 insertions(+), 3129 deletions(-) delete mode 100644 crates/hot/src/conformance.rs create mode 100644 crates/hot/src/conformance/cursor.rs create mode 100644 crates/hot/src/conformance/edge_cases.rs create mode 100644 crates/hot/src/conformance/history.rs create mode 100644 crates/hot/src/conformance/mod.rs create mode 100644 crates/hot/src/conformance/range.rs create mode 100644 crates/hot/src/conformance/roundtrip.rs create mode 100644 crates/hot/src/conformance/unwind.rs diff --git a/Cargo.toml b/Cargo.toml index 81f6f09..0144a41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,7 @@ signet-storage = { version = "0.0.1", path = "./crates/storage" } signet-storage-types = { version = "0.0.1", path = "./crates/types" } # External, in-house -signet-libmdbx = "0.6.0" +signet-libmdbx = { version = "0.7.0" } signet-zenith = "0.16.0-rc.5" @@ -64,4 +64,5 @@ tempfile = "3.20.0" thiserror = "2.0.18" tokio = { version = "1.45.0", features = ["full"] } tokio-util = { version = "0.7", features = ["rt"] } +itertools = "0.14" tracing = "0.1.44" diff --git a/crates/hot-mdbx/src/cursor.rs b/crates/hot-mdbx/src/cursor.rs index a68b52e..e126962 100644 --- a/crates/hot-mdbx/src/cursor.rs +++ b/crates/hot-mdbx/src/cursor.rs @@ -3,7 +3,10 @@ use crate::{FixedSizeInfo, MdbxError}; use signet_hot::{ MAX_FIXED_VAL_SIZE, MAX_KEY_SIZE, - model::{DualKeyTraverse, KvTraverse, KvTraverseMut, RawDualKeyValue, RawKeyValue, RawValue}, + model::{ + DualKeyTraverse, DualKeyTraverseMut, KvTraverse, KvTraverseMut, RawDualKeyValue, + RawKeyValue, RawValue, + }, }; use signet_libmdbx::{Ro, Rw, RwSync, TransactionKind, tx::WriteMarker}; use std::{ @@ -104,7 +107,7 @@ where impl KvTraverseMut for Cursor<'_, K> { fn delete_current(&mut self) -> Result<(), MdbxError> { - self.inner.del(Default::default()).map_err(MdbxError::Mdbx) + self.inner.del().map_err(MdbxError::Mdbx) } } @@ -502,3 +505,24 @@ where } } } + +impl DualKeyTraverseMut for Cursor<'_, K> { + fn delete_current(&mut self) -> Result<(), MdbxError> { + // For DUPSORT tables, del() deletes only the current duplicate + self.inner.del().map_err(MdbxError::Mdbx) + } + + fn clear_k1(&mut self, key1: &[u8]) -> Result<(), MdbxError> { + if !self.fsi.is_dupsort() { + return Err(MdbxError::NotDupSort); + } + + // Position at the K1 - if it doesn't exist, nothing to delete + if self.inner.set::<()>(key1)?.is_none() { + return Ok(()); + } + // Delete all K2 entries for this K1 + self.inner.del_all_dups()?; + Ok(()) + } +} diff --git a/crates/hot-mdbx/src/test_utils.rs b/crates/hot-mdbx/src/test_utils.rs index 0bba8ad..88a75ab 100644 --- a/crates/hot-mdbx/src/test_utils.rs +++ b/crates/hot-mdbx/src/test_utils.rs @@ -1097,6 +1097,292 @@ mod tests { test_unwind_conformance(&db_a, &db_b); } + #[test] + #[serial] + fn mdbx_range_clear() { + let (_dir, db) = create_test_rw_db(); + signet_hot::conformance::test_clear_range(&db); + } + + #[test] + #[serial] + fn mdbx_range_take() { + let (_dir, db) = create_test_rw_db(); + signet_hot::conformance::test_take_range(&db); + } + + #[test] + #[serial] + fn mdbx_range_clear_dual() { + let (_dir, db) = create_test_rw_db(); + signet_hot::conformance::test_clear_range_dual(&db); + } + + #[test] + #[serial] + fn mdbx_range_take_dual() { + let (_dir, db) = create_test_rw_db(); + signet_hot::conformance::test_take_range_dual(&db); + } + + /// Debug test to trace StorageChangeSets iteration during unwind + /// This test mirrors the full unwind_conformance test to debug the MDBX failure + #[test] + #[serial] + fn debug_storage_changesets_iteration() { + use alloy::{ + consensus::{Header, Sealable}, + primitives::{B256, U256, address}, + }; + use signet_hot::{ + conformance::make_bundle_state, + db::{HistoryWrite, HotDbRead}, + model::{DualTableTraverse, HotKvRead}, + tables, + }; + + // Use same addresses as the full test + let addr1 = address!("0x1111111111111111111111111111111111111111"); + let addr2 = address!("0x2222222222222222222222222222222222222222"); + let addr3 = address!("0x3333333333333333333333333333333333333333"); + let addr4 = address!("0x4444444444444444444444444444444444444444"); + + let slot1 = U256::from(1); + let slot2 = U256::from(2); + let slot3 = U256::from(3); + + let (_dir, db) = create_test_rw_db(); + + // Create 5 blocks matching the full test + let mut blocks = Vec::new(); + let mut prev_hash = B256::ZERO; + + // Block 0: Create addr1.slot1 = 10 + { + let header = Header { + number: 0, + parent_hash: prev_hash, + gas_limit: 1_000_000, + ..Default::default() + }; + let sealed = header.seal_slow(); + prev_hash = sealed.hash(); + let bundle = make_bundle_state( + vec![], + vec![(addr1, vec![(slot1, U256::ZERO, U256::from(10))])], + vec![], + ); + blocks.push((sealed, bundle)); + } + + // Block 1: addr1.slot1 = 20, addr2.slot1 = 100 + { + let header = Header { + number: 1, + parent_hash: prev_hash, + gas_limit: 1_000_000, + ..Default::default() + }; + let sealed = header.seal_slow(); + prev_hash = sealed.hash(); + let bundle = make_bundle_state( + vec![], + vec![ + (addr1, vec![(slot1, U256::from(10), U256::from(20))]), + (addr2, vec![(slot1, U256::ZERO, U256::from(100))]), + ], + vec![], + ); + blocks.push((sealed, bundle)); + } + + // Block 2: addr3.slot1 = 1000 + { + let header = Header { + number: 2, + parent_hash: prev_hash, + gas_limit: 1_000_000, + ..Default::default() + }; + let sealed = header.seal_slow(); + prev_hash = sealed.hash(); + let bundle = make_bundle_state( + vec![], + vec![(addr3, vec![(slot1, U256::ZERO, U256::from(1000))])], + vec![], + ); + blocks.push((sealed, bundle)); + } + + // Block 3: addr1.slot1 = 30, addr1.slot2 = 50, addr4.slot1 = 500 + { + let header = Header { + number: 3, + parent_hash: prev_hash, + gas_limit: 1_000_000, + ..Default::default() + }; + let sealed = header.seal_slow(); + prev_hash = sealed.hash(); + let bundle = make_bundle_state( + vec![], + vec![ + ( + addr1, + vec![ + (slot1, U256::from(20), U256::from(30)), + (slot2, U256::ZERO, U256::from(50)), + ], + ), + (addr4, vec![(slot1, U256::ZERO, U256::from(500))]), + ], + vec![], + ); + blocks.push((sealed, bundle)); + } + + // Block 4: addr1.slot1 = 40, addr1.slot3 = 60, addr2.slot1 = 150, addr2.slot2 = 200 + { + let header = Header { + number: 4, + parent_hash: prev_hash, + gas_limit: 1_000_000, + ..Default::default() + }; + let sealed = header.seal_slow(); + let bundle = make_bundle_state( + vec![], + vec![ + ( + addr1, + vec![ + (slot1, U256::from(30), U256::from(40)), + (slot3, U256::ZERO, U256::from(60)), + ], + ), + ( + addr2, + vec![ + (slot1, U256::from(100), U256::from(150)), + (slot2, U256::ZERO, U256::from(200)), + ], + ), + ], + vec![], + ); + blocks.push((sealed, bundle)); + } + + // Append all blocks + { + let writer = db.writer().unwrap(); + writer.append_blocks(&blocks).unwrap(); + writer.commit().unwrap(); + } + + // Verify StorageChangeSets contains expected entries + { + let reader = db.reader().unwrap(); + let mut cursor = reader.traverse_dual::().unwrap(); + + let mut entries = Vec::new(); + if let Some(first) = + DualTableTraverse::::first(&mut *cursor.inner_mut()) + .unwrap() + { + entries.push(first); + while let Some(next) = DualTableTraverse::::read_next( + &mut *cursor.inner_mut(), + ) + .unwrap() + { + entries.push(next); + } + } + + eprintln!("\nStorageChangeSets entries ({} total):", entries.len()); + for ((bn, addr), slot, val) in &entries { + eprintln!(" block={}, addr={:?}, slot={}, old_value={}", bn, addr, slot, val); + } + } + + // Count PlainStorageState entries before unwind + { + let reader = db.reader().unwrap(); + let mut cursor = reader.traverse_dual::().unwrap(); + + let mut entries = Vec::new(); + if let Some(first) = + DualTableTraverse::::first(&mut *cursor.inner_mut()) + .unwrap() + { + entries.push(first); + while let Some(next) = DualTableTraverse::::read_next( + &mut *cursor.inner_mut(), + ) + .unwrap() + { + entries.push(next); + } + } + + eprintln!("\nPlainStorageState BEFORE unwind ({} total):", entries.len()); + for (addr, slot, val) in &entries { + eprintln!(" addr={:?}, slot={}, value={}", addr, slot, val); + } + assert_eq!(entries.len(), 7, "Expected 7 storage entries before unwind"); + } + + // Unwind to block 1 + { + let writer = db.writer().unwrap(); + writer.unwind_above(1).unwrap(); + writer.commit().unwrap(); + } + + // Count PlainStorageState entries after unwind + { + let reader = db.reader().unwrap(); + let mut cursor = reader.traverse_dual::().unwrap(); + + let mut entries = Vec::new(); + if let Some(first) = + DualTableTraverse::::first(&mut *cursor.inner_mut()) + .unwrap() + { + entries.push(first); + while let Some(next) = DualTableTraverse::::read_next( + &mut *cursor.inner_mut(), + ) + .unwrap() + { + entries.push(next); + } + } + + eprintln!("\nPlainStorageState AFTER unwind ({} total):", entries.len()); + for (addr, slot, val) in &entries { + eprintln!(" addr={:?}, slot={}, value={}", addr, slot, val); + } + + // Expected: addr1.slot1=20, addr2.slot1=100 (2 entries) + assert_eq!(entries.len(), 2, "Expected 2 storage entries after unwind"); + + // Verify specific values + let reader = db.reader().unwrap(); + assert_eq!( + reader.get_storage(&addr1, &slot1).unwrap(), + Some(U256::from(20)), + "addr1.slot1 should be 20" + ); + assert_eq!( + reader.get_storage(&addr2, &slot1).unwrap(), + Some(U256::from(100)), + "addr2.slot1 should be 100" + ); + } + } + // ======================================================================== // put_multiple Tests // ======================================================================== @@ -1660,4 +1946,148 @@ mod tests { assert_eq!(value, U256::from(max_entries_per_page * 7 + 42)); } } + + #[test] + #[serial] + fn test_clear_k1() { + run_test(test_clear_k1_inner) + } + + fn test_clear_k1_inner(db: &DatabaseEnv) { + let addr1 = Address::from_slice(&[0x11; 20]); + let addr2 = Address::from_slice(&[0x22; 20]); + let addr3 = Address::from_slice(&[0x33; 20]); + + // Setup: Write storage entries for multiple addresses + { + let writer = db.writer().unwrap(); + // addr1: slots 1, 2, 3 + writer + .queue_put_dual::( + &addr1, + &U256::from(1), + &U256::from(10), + ) + .unwrap(); + writer + .queue_put_dual::( + &addr1, + &U256::from(2), + &U256::from(20), + ) + .unwrap(); + writer + .queue_put_dual::( + &addr1, + &U256::from(3), + &U256::from(30), + ) + .unwrap(); + // addr2: slots 10, 20 + writer + .queue_put_dual::( + &addr2, + &U256::from(10), + &U256::from(100), + ) + .unwrap(); + writer + .queue_put_dual::( + &addr2, + &U256::from(20), + &U256::from(200), + ) + .unwrap(); + // addr3: slot 100 + writer + .queue_put_dual::( + &addr3, + &U256::from(100), + &U256::from(1000), + ) + .unwrap(); + writer.commit().unwrap(); + } + + // Clear all entries for addr2 + { + let writer = db.writer().unwrap(); + { + let mut cursor = writer.traverse_dual_mut::().unwrap(); + cursor.clear_k1(&addr2).unwrap(); + } + writer.commit().unwrap(); + } + + // Verify addr2 entries are deleted, others remain + { + let reader: Tx = db.reader().unwrap(); + // addr1 should exist + assert!( + reader + .get_dual::(&addr1, &U256::from(1)) + .unwrap() + .is_some() + ); + assert!( + reader + .get_dual::(&addr1, &U256::from(2)) + .unwrap() + .is_some() + ); + assert!( + reader + .get_dual::(&addr1, &U256::from(3)) + .unwrap() + .is_some() + ); + // addr2 should be deleted + assert!( + reader + .get_dual::(&addr2, &U256::from(10)) + .unwrap() + .is_none() + ); + assert!( + reader + .get_dual::(&addr2, &U256::from(20)) + .unwrap() + .is_none() + ); + // addr3 should exist + assert!( + reader + .get_dual::(&addr3, &U256::from(100)) + .unwrap() + .is_some() + ); + } + + // Test idempotent - clearing already deleted K1 should be no-op + { + let writer = db.writer().unwrap(); + { + let mut cursor = writer.traverse_dual_mut::().unwrap(); + cursor.clear_k1(&addr2).unwrap(); + } + writer.commit().unwrap(); + } + + // Verify state unchanged + { + let reader: Tx = db.reader().unwrap(); + assert!( + reader + .get_dual::(&addr1, &U256::from(1)) + .unwrap() + .is_some() + ); + assert!( + reader + .get_dual::(&addr3, &U256::from(100)) + .unwrap() + .is_some() + ); + } + } } diff --git a/crates/hot-mdbx/src/tx.rs b/crates/hot-mdbx/src/tx.rs index 3c7ec3f..a248089 100644 --- a/crates/hot-mdbx/src/tx.rs +++ b/crates/hot-mdbx/src/tx.rs @@ -225,7 +225,7 @@ macro_rules! impl_hot_kv_write { && found_val.starts_with(key2) // Check if found value starts with our key2 { - cursor.del(Default::default()).map_err(MdbxError::Mdbx)?; + cursor.del().map_err(MdbxError::Mdbx)?; } } @@ -269,18 +269,37 @@ macro_rules! impl_hot_kv_write { let db = self.inner.open_db(Some(table))?; let fsi = self.get_fsi(table)?; - // For DUPSORT tables, the "value" is key2 concatenated with the actual - // value. If the table is ALSO dupfixed, we need to pad key2 to the - // fixed size - if let Some(total_size) = fsi.total_size() { - // Copy key2 to scratch buffer and zero-pad to total fixed size - let mut buffer = [0u8; TX_BUFFER_SIZE]; - buffer[..key2.len()].copy_from_slice(key2); - buffer[key2.len()..total_size].fill(0); - let k2 = &buffer[..total_size]; - - self.inner.del(db, key1, Some(k2)).map(drop).map_err(MdbxError::Mdbx) + // For DUPSORT tables, the "value" is key2 || actual_value. + // For DUP_FIXED tables, we cannot use del() with a partial value + // because MDBX requires an exact match. We must use a cursor to + // find and delete the entry. + if fsi.is_dupsort() { + // Prepare search value (key2, optionally padded for DUP_FIXED) + let mut search_buf = [0u8; TX_BUFFER_SIZE]; + let search_val = if let Some(ts) = fsi.total_size() { + search_buf[..key2.len()].copy_from_slice(key2); + search_buf[key2.len()..ts].fill(0); + &search_buf[..ts] + } else { + key2 + }; + + // Use cursor to find and delete the entry + let mut cursor = self.inner.cursor(db).map_err(MdbxError::Mdbx)?; + + // get_both_range finds entry where key=key1 and value >= search_val + // If found and the key2 portion matches, delete it + if let Some(found_val) = cursor + .get_both_range::>(key1, search_val) + .map_err(MdbxError::from)? + && found_val.starts_with(key2) + { + cursor.del().map_err(MdbxError::Mdbx)?; + } + + Ok(()) } else { + // Non-DUPSORT table - just delete by key1 self.inner.del(db, key1, Some(key2)).map(drop).map_err(MdbxError::Mdbx) } } diff --git a/crates/hot/Cargo.toml b/crates/hot/Cargo.toml index c8a22e3..c29b8e6 100644 --- a/crates/hot/Cargo.toml +++ b/crates/hot/Cargo.toml @@ -17,6 +17,7 @@ alloy.workspace = true auto_impl.workspace = true bytes.workspace = true +itertools.workspace = true thiserror.workspace = true [dev-dependencies] diff --git a/crates/hot/src/conformance.rs b/crates/hot/src/conformance.rs deleted file mode 100644 index a3aa4c1..0000000 --- a/crates/hot/src/conformance.rs +++ /dev/null @@ -1,2534 +0,0 @@ -#![allow(dead_code)] - -use crate::{ - db::{HistoryError, HistoryRead, HistoryWrite, HotDbRead, UnsafeDbWrite, UnsafeHistoryWrite}, - model::{ - DualKeyValue, DualTableTraverse, HotKv, HotKvRead, HotKvWrite, KeyValue, TableTraverse, - }, - tables::{self, DualKey, SingleKey}, -}; -use alloy::{ - consensus::{Header, Sealable}, - primitives::{Address, B256, Bytes, U256, address, b256}, -}; -use signet_storage_types::{Account, BlockNumberList, SealedHeader, ShardedKey}; -use std::collections::HashMap; -use std::fmt::Debug; -use trevm::revm::{ - bytecode::Bytecode, - database::{ - AccountStatus, BundleAccount, BundleState, - states::{ - StorageSlot, - reverts::{AccountInfoRevert, AccountRevert, RevertToSlot, Reverts}, - }, - }, - primitives::map::DefaultHashBuilder, - state::AccountInfo, -}; - -/// Run all conformance tests against a [`HotKv`] implementation. -pub fn conformance(hot_kv: &T) { - test_header_roundtrip(hot_kv); - test_account_roundtrip(hot_kv); - test_storage_roundtrip(hot_kv); - test_storage_update_replaces(hot_kv); - test_bytecode_roundtrip(hot_kv); - test_account_history(hot_kv); - test_storage_history(hot_kv); - test_account_changes(hot_kv); - test_storage_changes(hot_kv); - test_missing_reads(hot_kv); -} - -// /// Run append and unwind conformance tests. -// /// -// /// This test requires a fresh database (no prior state) to properly test -// /// the append/unwind functionality. -// pub fn conformance_append_unwind(hot_kv: &T) { -// test_append_and_unwind_blocks(hot_kv); -// } - -/// Test writing and reading headers via HotDbWrite/HotDbRead -fn test_header_roundtrip(hot_kv: &T) { - let header = Header { number: 42, gas_limit: 1_000_000, ..Default::default() }; - let sealed = SealedHeader::new(header.clone()); - let hash = sealed.hash(); - - // Write header - { - let writer = hot_kv.writer().unwrap(); - writer.put_header(&sealed).unwrap(); - writer.commit().unwrap(); - } - - // Read header by number - { - let reader = hot_kv.reader().unwrap(); - let read_header = reader.get_header(42).unwrap(); - assert!(read_header.is_some()); - assert_eq!(read_header.unwrap().number, 42); - } - - // Read header number by hash - { - let reader = hot_kv.reader().unwrap(); - let read_number = reader.get_header_number(&hash).unwrap(); - assert!(read_number.is_some()); - assert_eq!(read_number.unwrap(), 42); - } - - // Read header by hash - { - let reader = hot_kv.reader().unwrap(); - let read_header = reader.header_by_hash(&hash).unwrap(); - assert!(read_header.is_some()); - assert_eq!(read_header.unwrap().number, 42); - } -} - -/// Test writing and reading accounts via HotDbWrite/HotDbRead -fn test_account_roundtrip(hot_kv: &T) { - let addr = address!("0x1234567890123456789012345678901234567890"); - let account = Account { nonce: 5, balance: U256::from(1000), bytecode_hash: Some(B256::ZERO) }; - - // Write account - { - let writer = hot_kv.writer().unwrap(); - writer.put_account(&addr, &account).unwrap(); - writer.commit().unwrap(); - } - - // Read account - { - let reader = hot_kv.reader().unwrap(); - let read_account = reader.get_account(&addr).unwrap(); - assert!(read_account.is_some()); - let read_account = read_account.unwrap(); - assert_eq!(read_account.nonce, 5); - assert_eq!(read_account.balance, U256::from(1000)); - } -} - -/// Test writing and reading storage via HotDbWrite/HotDbRead -fn test_storage_roundtrip(hot_kv: &T) { - let addr = address!("0xabcdef0123456789abcdef0123456789abcdef01"); - let slot = U256::from(42); - let value = U256::from(999); - - // Write storage - { - let writer = hot_kv.writer().unwrap(); - writer.put_storage(&addr, &slot, &value).unwrap(); - writer.commit().unwrap(); - } - - // Read storage - { - let reader = hot_kv.reader().unwrap(); - let read_value = reader.get_storage(&addr, &slot).unwrap(); - assert!(read_value.is_some()); - assert_eq!(read_value.unwrap(), U256::from(999)); - } -} - -/// Test that updating a storage slot replaces the value (no duplicates). -/// -/// This test verifies that DUPSORT tables properly handle updates by deleting -/// existing entries before inserting new ones. -fn test_storage_update_replaces(hot_kv: &T) { - let addr = address!("0x2222222222222222222222222222222222222222"); - let slot = U256::from(1); - - // Write initial value - { - let writer = hot_kv.writer().unwrap(); - writer.put_storage(&addr, &slot, &U256::from(10)).unwrap(); - writer.commit().unwrap(); - } - - // Update to new value - { - let writer = hot_kv.writer().unwrap(); - writer.put_storage(&addr, &slot, &U256::from(20)).unwrap(); - writer.commit().unwrap(); - } - - // Verify: only ONE entry exists with the NEW value - let reader = hot_kv.reader().unwrap(); - let mut cursor = reader.traverse_dual::().unwrap(); - - let mut count = 0; - let mut found_value = None; - while let Some((k, k2, v)) = cursor.read_next().unwrap() { - if k == addr && k2 == slot { - count += 1; - found_value = Some(v); - } - } - - assert_eq!(count, 1, "Should have exactly one entry, not duplicates"); - assert_eq!(found_value, Some(U256::from(20)), "Value should be 20"); -} - -/// Test writing and reading bytecode via HotDbWrite/HotDbRead -fn test_bytecode_roundtrip(hot_kv: &T) { - let code = Bytes::from_static(&[0x60, 0x00, 0x60, 0x00, 0xf3]); // Simple EVM bytecode - let bytecode = Bytecode::new_raw(code); - let code_hash = bytecode.hash_slow(); - - // Write bytecode - { - let writer = hot_kv.writer().unwrap(); - writer.put_bytecode(&code_hash, &bytecode).unwrap(); - writer.commit().unwrap(); - } - - // Read bytecode - { - let reader = hot_kv.reader().unwrap(); - let read_bytecode = reader.get_bytecode(&code_hash).unwrap(); - assert!(read_bytecode.is_some()); - } -} - -/// Test account history via HotHistoryWrite/HotHistoryRead -fn test_account_history(hot_kv: &T) { - let addr = address!("0x1111111111111111111111111111111111111111"); - let touched_blocks = BlockNumberList::new([10, 20, 30]).unwrap(); - let latest_height = 100u64; - - // Write account history - { - let writer = hot_kv.writer().unwrap(); - writer.write_account_history(&addr, latest_height, &touched_blocks).unwrap(); - writer.commit().unwrap(); - } - - // Read account history - { - let reader = hot_kv.reader().unwrap(); - let read_history = reader.get_account_history(&addr, latest_height).unwrap(); - assert!(read_history.is_some()); - let history = read_history.unwrap(); - assert_eq!(history.iter().collect::>(), vec![10, 20, 30]); - } -} - -/// Test storage history via HotHistoryWrite/HotHistoryRead -fn test_storage_history(hot_kv: &T) { - let addr = address!("0x2222222222222222222222222222222222222222"); - let slot = U256::from(42); - let touched_blocks = BlockNumberList::new([5, 15, 25]).unwrap(); - let highest_block = 50u64; - - // Write storage history - { - let writer = hot_kv.writer().unwrap(); - writer.write_storage_history(&addr, slot, highest_block, &touched_blocks).unwrap(); - writer.commit().unwrap(); - } - - // Read storage history - { - let reader = hot_kv.reader().unwrap(); - let read_history = reader.get_storage_history(&addr, slot, highest_block).unwrap(); - assert!(read_history.is_some()); - let history = read_history.unwrap(); - assert_eq!(history.iter().collect::>(), vec![5, 15, 25]); - } -} - -/// Test account change sets via HotHistoryWrite/HotHistoryRead -fn test_account_changes(hot_kv: &T) { - let addr = address!("0x3333333333333333333333333333333333333333"); - let pre_state = Account { nonce: 10, balance: U256::from(5000), bytecode_hash: None }; - let block_number = 100u64; - - // Write account change - { - let writer = hot_kv.writer().unwrap(); - writer.write_account_prestate(block_number, addr, &pre_state).unwrap(); - writer.commit().unwrap(); - } - - // Read account change - { - let reader = hot_kv.reader().unwrap(); - - let read_change = reader.get_account_change(block_number, &addr).unwrap(); - - assert!(read_change.is_some()); - let change = read_change.unwrap(); - assert_eq!(change.nonce, 10); - assert_eq!(change.balance, U256::from(5000)); - } -} - -/// Test storage change sets via HotHistoryWrite/HotHistoryRead -fn test_storage_changes(hot_kv: &T) { - let addr = address!("0x4444444444444444444444444444444444444444"); - let slot = U256::from(153); - let pre_value = U256::from(12345); - let block_number = 200u64; - - // Write storage change - { - let writer = hot_kv.writer().unwrap(); - writer.write_storage_prestate(block_number, addr, &slot, &pre_value).unwrap(); - writer.commit().unwrap(); - } - - // Read storage change - { - let reader = hot_kv.reader().unwrap(); - let read_change = reader.get_storage_change(block_number, &addr, &slot).unwrap(); - assert!(read_change.is_some()); - assert_eq!(read_change.unwrap(), U256::from(12345)); - } -} - -/// Test that missing reads return None -fn test_missing_reads(hot_kv: &T) { - let missing_addr = address!("0x9999999999999999999999999999999999999999"); - let missing_hash = b256!("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); - let missing_slot = U256::from(99999); - - let reader = hot_kv.reader().unwrap(); - - // Missing header - assert!(reader.get_header(999999).unwrap().is_none()); - - // Missing header number - assert!(reader.get_header_number(&missing_hash).unwrap().is_none()); - - // Missing account - assert!(reader.get_account(&missing_addr).unwrap().is_none()); - - // Missing storage - assert!(reader.get_storage(&missing_addr, &missing_slot).unwrap().is_none()); - - // Missing bytecode - assert!(reader.get_bytecode(&missing_hash).unwrap().is_none()); - - // Missing header by hash - assert!(reader.header_by_hash(&missing_hash).unwrap().is_none()); - - // Missing account history - assert!(reader.get_account_history(&missing_addr, 1000).unwrap().is_none()); - - // Missing storage history - assert!(reader.get_storage_history(&missing_addr, missing_slot, 1000).unwrap().is_none()); - - // Missing account change - assert!(reader.get_account_change(999999, &missing_addr).unwrap().is_none()); - - // Missing storage change - assert!(reader.get_storage_change(999999, &missing_addr, &missing_slot).unwrap().is_none()); -} - -/// Helper to create a sealed header at a given height with specific parent -fn make_header(number: u64, parent_hash: B256) -> SealedHeader { - let header = Header { number, parent_hash, gas_limit: 1_000_000, ..Default::default() }; - header.seal_slow() -} - -/// Test update_history_indices_inconsistent for account history. -/// -/// This test verifies that: -/// 1. Account change sets are correctly indexed into account history -/// 2. Appending to existing history works correctly -/// 3. Old shards are deleted when appending -pub fn test_update_history_indices_account(hot_kv: &T) { - let addr1 = address!("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); - let addr2 = address!("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); - - // Phase 1: Write account change sets for blocks 1-3 - { - let writer = hot_kv.writer().unwrap(); - - // Block 1: addr1 changed - let pre_acc = Account::default(); - writer.write_account_prestate(1, addr1, &pre_acc).unwrap(); - - // Block 2: addr1 and addr2 changed - let acc1 = Account { nonce: 1, balance: U256::from(100), bytecode_hash: None }; - writer.write_account_prestate(2, addr1, &acc1).unwrap(); - writer.write_account_prestate(2, addr2, &pre_acc).unwrap(); - - // Block 3: addr2 changed - let acc2 = Account { nonce: 1, balance: U256::from(200), bytecode_hash: None }; - writer.write_account_prestate(3, addr2, &acc2).unwrap(); - - writer.commit().unwrap(); - } - - // Phase 2: Run update_history_indices_inconsistent for blocks 1-3 - { - let writer = hot_kv.writer().unwrap(); - writer.update_history_indices_inconsistent(1..=3).unwrap(); - writer.commit().unwrap(); - } - - // Phase 3: Verify account history was created correctly - { - let reader = hot_kv.reader().unwrap(); - - // addr1 should have history at blocks 1, 2 - let (_, history1) = - reader.last_account_history(addr1).unwrap().expect("addr1 should have history"); - let blocks1: Vec = history1.iter().collect(); - assert_eq!(blocks1, vec![1, 2], "addr1 history mismatch"); - - // addr2 should have history at blocks 2, 3 - let (_, history2) = - reader.last_account_history(addr2).unwrap().expect("addr2 should have history"); - let blocks2: Vec = history2.iter().collect(); - assert_eq!(blocks2, vec![2, 3], "addr2 history mismatch"); - } - - // Phase 4: Write more change sets for blocks 4-5 - { - let writer = hot_kv.writer().unwrap(); - - // Block 4: addr1 changed - let acc1 = Account { nonce: 2, balance: U256::from(300), bytecode_hash: None }; - writer.write_account_prestate(4, addr1, &acc1).unwrap(); - - // Block 5: addr1 changed again - let acc1_v2 = Account { nonce: 3, balance: U256::from(400), bytecode_hash: None }; - writer.write_account_prestate(5, addr1, &acc1_v2).unwrap(); - - writer.commit().unwrap(); - } - - // Phase 5: Run update_history_indices_inconsistent for blocks 4-5 - { - let writer = hot_kv.writer().unwrap(); - writer.update_history_indices_inconsistent(4..=5).unwrap(); - writer.commit().unwrap(); - } - - // Phase 6: Verify history was appended correctly - { - let reader = hot_kv.reader().unwrap(); - - // addr1 should now have history at blocks 1, 2, 4, 5 - let (_, history1) = - reader.last_account_history(addr1).unwrap().expect("addr1 should have history"); - let blocks1: Vec = history1.iter().collect(); - assert_eq!(blocks1, vec![1, 2, 4, 5], "addr1 history mismatch after append"); - - // addr2 should still have history at blocks 2, 3 (unchanged) - let (_, history2) = - reader.last_account_history(addr2).unwrap().expect("addr2 should have history"); - let blocks2: Vec = history2.iter().collect(); - assert_eq!(blocks2, vec![2, 3], "addr2 history should be unchanged"); - } -} - -/// Test update_history_indices_inconsistent for storage history. -/// -/// This test verifies that: -/// 1. Storage change sets are correctly indexed into storage history -/// 2. Appending to existing history works correctly -/// 3. Old shards are deleted when appending -/// 4. Different slots for the same address are tracked separately -pub fn test_update_history_indices_storage(hot_kv: &T) { - let addr1 = address!("0xcccccccccccccccccccccccccccccccccccccccc"); - let slot1 = U256::from(1); - let slot2 = U256::from(2); - - // Phase 1: Write storage change sets for blocks 1-3 - { - let writer = hot_kv.writer().unwrap(); - - // Block 1: addr1.slot1 changed - writer.write_storage_prestate(1, addr1, &slot1, &U256::ZERO).unwrap(); - - // Block 2: addr1.slot1 and addr1.slot2 changed - writer.write_storage_prestate(2, addr1, &slot1, &U256::from(100)).unwrap(); - writer.write_storage_prestate(2, addr1, &slot2, &U256::ZERO).unwrap(); - - // Block 3: addr1.slot2 changed - writer.write_storage_prestate(3, addr1, &slot2, &U256::from(200)).unwrap(); - - writer.commit().unwrap(); - } - - // Phase 2: Run update_history_indices_inconsistent for blocks 1-3 - { - let writer = hot_kv.writer().unwrap(); - writer.update_history_indices_inconsistent(1..=3).unwrap(); - writer.commit().unwrap(); - } - - // Phase 3: Verify storage history was created correctly - { - let reader = hot_kv.reader().unwrap(); - - // addr1.slot1 should have history at blocks 1, 2 - let (_, history1) = reader - .last_storage_history(&addr1, &slot1) - .unwrap() - .expect("addr1.slot1 should have history"); - let blocks1: Vec = history1.iter().collect(); - assert_eq!(blocks1, vec![1, 2], "addr1.slot1 history mismatch"); - - // addr1.slot2 should have history at blocks 2, 3 - let (_, history2) = reader - .last_storage_history(&addr1, &slot2) - .unwrap() - .expect("addr1.slot2 should have history"); - let blocks2: Vec = history2.iter().collect(); - assert_eq!(blocks2, vec![2, 3], "addr1.slot2 history mismatch"); - } - - // Phase 4: Write more change sets for blocks 4-5 - { - let writer = hot_kv.writer().unwrap(); - - // Block 4: addr1.slot1 changed - writer.write_storage_prestate(4, addr1, &slot1, &U256::from(300)).unwrap(); - - // Block 5: addr1.slot1 changed again - writer.write_storage_prestate(5, addr1, &slot1, &U256::from(400)).unwrap(); - - writer.commit().unwrap(); - } - - // Phase 5: Run update_history_indices_inconsistent for blocks 4-5 - { - let writer = hot_kv.writer().unwrap(); - writer.update_history_indices_inconsistent(4..=5).unwrap(); - writer.commit().unwrap(); - } - - // Phase 6: Verify history was appended correctly - { - let reader = hot_kv.reader().unwrap(); - - // addr1.slot1 should now have history at blocks 1, 2, 4, 5 - let (_, history1) = reader - .last_storage_history(&addr1, &slot1) - .unwrap() - .expect("addr1.slot1 should have history"); - let blocks1: Vec = history1.iter().collect(); - assert_eq!(blocks1, vec![1, 2, 4, 5], "addr1.slot1 history mismatch after append"); - - // addr1.slot2 should still have history at blocks 2, 3 (unchanged) - let (_, history2) = reader - .last_storage_history(&addr1, &slot2) - .unwrap() - .expect("addr1.slot2 should have history"); - let blocks2: Vec = history2.iter().collect(); - assert_eq!(blocks2, vec![2, 3], "addr1.slot2 history should be unchanged"); - } -} - -/// Test that appending to history correctly removes old entries at same k1,k2. -/// -/// This test specifically verifies that when we append new indices to an existing -/// shard, the old shard is properly deleted so we don't end up with duplicate data. -pub fn test_history_append_removes_old_entries(hot_kv: &T) { - let addr = address!("0xdddddddddddddddddddddddddddddddddddddddd"); - - // Phase 1: Manually write account history - { - let writer = hot_kv.writer().unwrap(); - let initial_history = BlockNumberList::new([10, 20, 30]).unwrap(); - writer.write_account_history(&addr, u64::MAX, &initial_history).unwrap(); - writer.commit().unwrap(); - } - - // Verify initial state - { - let reader = hot_kv.reader().unwrap(); - let (key, history) = - reader.last_account_history(addr).unwrap().expect("should have history"); - assert_eq!(key, u64::MAX); - let blocks: Vec = history.iter().collect(); - assert_eq!(blocks, vec![10, 20, 30]); - } - - // Phase 2: Write account change set for block 40 - { - let writer = hot_kv.writer().unwrap(); - let acc = Account { nonce: 1, balance: U256::from(100), bytecode_hash: None }; - writer.write_account_prestate(40, addr, &acc).unwrap(); - writer.commit().unwrap(); - } - - // Phase 3: Run update_history_indices_inconsistent - { - let writer = hot_kv.writer().unwrap(); - writer.update_history_indices_inconsistent(40..=40).unwrap(); - writer.commit().unwrap(); - } - - // Phase 4: Verify history was correctly appended - { - let reader = hot_kv.reader().unwrap(); - let (key, history) = - reader.last_account_history(addr).unwrap().expect("should have history"); - assert_eq!(key, u64::MAX, "key should still be u64::MAX"); - let blocks: Vec = history.iter().collect(); - assert_eq!(blocks, vec![10, 20, 30, 40], "history should include appended block"); - } -} - -/// Test deleting dual-keyed account history entries. -/// -/// This test verifies that: -/// 1. Writing dual-keyed entries works correctly -/// 2. Deleting specific dual-keyed entries removes only that entry -/// 3. Other entries for the same k1 remain intact -/// 4. Traversal after deletion shows the entry is gone -pub fn test_delete_dual_account_history(hot_kv: &T) { - let addr1 = address!("0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"); - let addr2 = address!("0xffffffffffffffffffffffffffffffffffffffff"); - - // Phase 1: Write account history entries for multiple addresses - { - let writer = hot_kv.writer().unwrap(); - - // Write history for addr1 at two different shard keys - let history1_a = BlockNumberList::new([1, 2, 3]).unwrap(); - let history1_b = BlockNumberList::new([4, 5, 6]).unwrap(); - writer.write_account_history(&addr1, 100, &history1_a).unwrap(); - writer.write_account_history(&addr1, u64::MAX, &history1_b).unwrap(); - - // Write history for addr2 - let history2 = BlockNumberList::new([10, 20, 30]).unwrap(); - writer.write_account_history(&addr2, u64::MAX, &history2).unwrap(); - - writer.commit().unwrap(); - } - - // Phase 2: Verify all entries exist - { - let reader = hot_kv.reader().unwrap(); - - // Check addr1 entries - let hist1_a = reader.get_account_history(&addr1, 100).unwrap(); - assert!(hist1_a.is_some(), "addr1 shard 100 should exist"); - assert_eq!(hist1_a.unwrap().iter().collect::>(), vec![1, 2, 3]); - - let hist1_b = reader.get_account_history(&addr1, u64::MAX).unwrap(); - assert!(hist1_b.is_some(), "addr1 shard u64::MAX should exist"); - assert_eq!(hist1_b.unwrap().iter().collect::>(), vec![4, 5, 6]); - - // Check addr2 entry - let hist2 = reader.get_account_history(&addr2, u64::MAX).unwrap(); - assert!(hist2.is_some(), "addr2 should exist"); - assert_eq!(hist2.unwrap().iter().collect::>(), vec![10, 20, 30]); - } - - // Phase 3: Delete addr1's u64::MAX entry - { - let writer = hot_kv.writer().unwrap(); - writer.queue_delete_dual::(&addr1, &u64::MAX).unwrap(); - writer.commit().unwrap(); - } - - // Phase 4: Verify only the deleted entry is gone - { - let reader = hot_kv.reader().unwrap(); - - // addr1 shard 100 should still exist - let hist1_a = reader.get_account_history(&addr1, 100).unwrap(); - assert!(hist1_a.is_some(), "addr1 shard 100 should still exist after delete"); - assert_eq!(hist1_a.unwrap().iter().collect::>(), vec![1, 2, 3]); - - // addr1 shard u64::MAX should be gone - let hist1_b = reader.get_account_history(&addr1, u64::MAX).unwrap(); - assert!(hist1_b.is_none(), "addr1 shard u64::MAX should be deleted"); - - // addr2 should be unaffected - let hist2 = reader.get_account_history(&addr2, u64::MAX).unwrap(); - assert!(hist2.is_some(), "addr2 should be unaffected by delete"); - assert_eq!(hist2.unwrap().iter().collect::>(), vec![10, 20, 30]); - - // Verify last_account_history now returns shard 100 for addr1 - let (key, _) = - reader.last_account_history(addr1).unwrap().expect("addr1 should still have history"); - assert_eq!(key, 100, "last shard for addr1 should now be 100"); - } -} - -/// Test deleting dual-keyed storage history entries. -/// -/// This test verifies that: -/// 1. Writing storage history entries works correctly -/// 2. Deleting specific (address, slot, shard) entries removes only that entry -/// 3. Other slots for the same address remain intact -/// 4. Traversal after deletion shows the entry is gone -pub fn test_delete_dual_storage_history(hot_kv: &T) { - use signet_storage_types::ShardedKey; - - let addr = address!("0x1111111111111111111111111111111111111111"); - let slot1 = U256::from(100); - let slot2 = U256::from(200); - - // Phase 1: Write storage history entries for multiple slots - { - let writer = hot_kv.writer().unwrap(); - - // Write history for slot1 - let history1 = BlockNumberList::new([1, 2, 3]).unwrap(); - writer.write_storage_history(&addr, slot1, u64::MAX, &history1).unwrap(); - - // Write history for slot2 - let history2 = BlockNumberList::new([10, 20, 30]).unwrap(); - writer.write_storage_history(&addr, slot2, u64::MAX, &history2).unwrap(); - - writer.commit().unwrap(); - } - - // Phase 2: Verify both entries exist - { - let reader = hot_kv.reader().unwrap(); - - let hist1 = reader.get_storage_history(&addr, slot1, u64::MAX).unwrap(); - assert!(hist1.is_some(), "slot1 should exist"); - assert_eq!(hist1.unwrap().iter().collect::>(), vec![1, 2, 3]); - - let hist2 = reader.get_storage_history(&addr, slot2, u64::MAX).unwrap(); - assert!(hist2.is_some(), "slot2 should exist"); - assert_eq!(hist2.unwrap().iter().collect::>(), vec![10, 20, 30]); - } - - // Phase 3: Delete slot1's entry - { - let writer = hot_kv.writer().unwrap(); - let key_to_delete = ShardedKey::new(slot1, u64::MAX); - writer.queue_delete_dual::(&addr, &key_to_delete).unwrap(); - writer.commit().unwrap(); - } - - // Phase 4: Verify only slot1 is gone - { - let reader = hot_kv.reader().unwrap(); - - // slot1 should be gone - let hist1 = reader.get_storage_history(&addr, slot1, u64::MAX).unwrap(); - assert!(hist1.is_none(), "slot1 should be deleted"); - - // slot2 should be unaffected - let hist2 = reader.get_storage_history(&addr, slot2, u64::MAX).unwrap(); - assert!(hist2.is_some(), "slot2 should be unaffected"); - assert_eq!(hist2.unwrap().iter().collect::>(), vec![10, 20, 30]); - - // last_storage_history for slot1 should return None - let last1 = reader.last_storage_history(&addr, &slot1).unwrap(); - assert!(last1.is_none(), "last_storage_history for slot1 should return None"); - - // last_storage_history for slot2 should still work - let last2 = reader.last_storage_history(&addr, &slot2).unwrap(); - assert!(last2.is_some(), "last_storage_history for slot2 should still work"); - } -} - -/// Test deleting and re-adding dual-keyed entries. -/// -/// This test verifies that after deleting an entry, we can write a new entry -/// with the same key and it works correctly. -pub fn test_delete_and_rewrite_dual(hot_kv: &T) { - let addr = address!("0x2222222222222222222222222222222222222222"); - - // Phase 1: Write initial entry - { - let writer = hot_kv.writer().unwrap(); - let history = BlockNumberList::new([1, 2, 3]).unwrap(); - writer.write_account_history(&addr, u64::MAX, &history).unwrap(); - writer.commit().unwrap(); - } - - // Verify initial state - { - let reader = hot_kv.reader().unwrap(); - let hist = reader.get_account_history(&addr, u64::MAX).unwrap(); - assert_eq!(hist.unwrap().iter().collect::>(), vec![1, 2, 3]); - } - - // Phase 2: Delete the entry - { - let writer = hot_kv.writer().unwrap(); - writer.queue_delete_dual::(&addr, &u64::MAX).unwrap(); - writer.commit().unwrap(); - } - - // Verify deleted - { - let reader = hot_kv.reader().unwrap(); - let hist = reader.get_account_history(&addr, u64::MAX).unwrap(); - assert!(hist.is_none(), "entry should be deleted"); - } - - // Phase 3: Write new entry with same key but different value - { - let writer = hot_kv.writer().unwrap(); - let new_history = BlockNumberList::new([100, 200, 300]).unwrap(); - writer.write_account_history(&addr, u64::MAX, &new_history).unwrap(); - writer.commit().unwrap(); - } - - // Verify new value - { - let reader = hot_kv.reader().unwrap(); - let hist = reader.get_account_history(&addr, u64::MAX).unwrap(); - assert!(hist.is_some(), "new entry should exist"); - assert_eq!(hist.unwrap().iter().collect::>(), vec![100, 200, 300]); - } -} - -/// Test clear_range on a single-keyed table. -/// -/// This test verifies that: -/// 1. Keys within the range are deleted -/// 2. Keys outside the range remain intact -/// 3. Edge cases like adjacent keys and boundary conditions work correctly -pub fn test_clear_range(hot_kv: &T) { - // Phase 1: Write 15 headers with block numbers 0-14 - { - let writer = hot_kv.writer().unwrap(); - for i in 0u64..15 { - let header = Header { number: i, gas_limit: 1_000_000, ..Default::default() }; - writer.put_header_inconsistent(&header).unwrap(); - } - writer.commit().unwrap(); - } - - // Verify all headers exist - { - let reader = hot_kv.reader().unwrap(); - for i in 0u64..15 { - assert!(reader.get_header(i).unwrap().is_some(), "header {} should exist", i); - } - } - - // Phase 2: Clear range 5..=9 (middle range) - { - let writer = hot_kv.writer().unwrap(); - writer.clear_range::(5..=9).unwrap(); - writer.commit().unwrap(); - } - - // Verify: 0-4 and 10-14 should exist, 5-9 should be gone - { - let reader = hot_kv.reader().unwrap(); - - // Keys before range should exist - for i in 0u64..5 { - assert!(reader.get_header(i).unwrap().is_some(), "header {} should still exist", i); - } - - // Keys in range should be deleted - for i in 5u64..10 { - assert!(reader.get_header(i).unwrap().is_none(), "header {} should be deleted", i); - } - - // Keys after range should exist - for i in 10u64..15 { - assert!(reader.get_header(i).unwrap().is_some(), "header {} should still exist", i); - } - } - - // Phase 3: Test corner case - clear adjacent keys at the boundary - { - let writer = hot_kv.writer().unwrap(); - // Clear keys 3 and 4 (adjacent to the already cleared range) - writer.clear_range::(3..=4).unwrap(); - writer.commit().unwrap(); - } - - // Verify: 0-2 and 10-14 should exist, 3-9 should be gone - { - let reader = hot_kv.reader().unwrap(); - - // Keys 0-2 should exist - for i in 0u64..3 { - assert!(reader.get_header(i).unwrap().is_some(), "header {} should still exist", i); - } - - // Keys 3-9 should all be deleted now - for i in 3u64..10 { - assert!(reader.get_header(i).unwrap().is_none(), "header {} should be deleted", i); - } - - // Keys 10-14 should exist - for i in 10u64..15 { - assert!(reader.get_header(i).unwrap().is_some(), "header {} should still exist", i); - } - } - - // Phase 4: Test clearing a range that includes the first key - { - let writer = hot_kv.writer().unwrap(); - writer.clear_range::(0..=1).unwrap(); - writer.commit().unwrap(); - } - - { - let reader = hot_kv.reader().unwrap(); - assert!(reader.get_header(0).unwrap().is_none(), "header 0 should be deleted"); - assert!(reader.get_header(1).unwrap().is_none(), "header 1 should be deleted"); - assert!(reader.get_header(2).unwrap().is_some(), "header 2 should still exist"); - } - - // Phase 5: Test clearing a range that includes the last key - { - let writer = hot_kv.writer().unwrap(); - writer.clear_range::(13..=14).unwrap(); - writer.commit().unwrap(); - } - - { - let reader = hot_kv.reader().unwrap(); - assert!(reader.get_header(12).unwrap().is_some(), "header 12 should still exist"); - assert!(reader.get_header(13).unwrap().is_none(), "header 13 should be deleted"); - assert!(reader.get_header(14).unwrap().is_none(), "header 14 should be deleted"); - } - - // Phase 6: Test clearing a single key - { - let writer = hot_kv.writer().unwrap(); - writer.clear_range::(11..=11).unwrap(); - writer.commit().unwrap(); - } - - { - let reader = hot_kv.reader().unwrap(); - assert!(reader.get_header(10).unwrap().is_some(), "header 10 should still exist"); - assert!(reader.get_header(11).unwrap().is_none(), "header 11 should be deleted"); - assert!(reader.get_header(12).unwrap().is_some(), "header 12 should still exist"); - } - - // Phase 7: Test clearing a range where nothing exists (should be no-op) - { - let writer = hot_kv.writer().unwrap(); - writer.clear_range::(100..=200).unwrap(); - writer.commit().unwrap(); - } - - // Verify remaining keys are still intact - { - let reader = hot_kv.reader().unwrap(); - assert!(reader.get_header(2).unwrap().is_some(), "header 2 should still exist"); - assert!(reader.get_header(10).unwrap().is_some(), "header 10 should still exist"); - assert!(reader.get_header(12).unwrap().is_some(), "header 12 should still exist"); - } -} - -/// Test take_range on a single-keyed table. -/// -/// Similar to clear_range but also returns the removed keys. -pub fn test_take_range(hot_kv: &T) { - let headers = (0..10u64) - .map(|i| Header { number: i, gas_limit: 1_000_000, ..Default::default() }) - .collect::>(); - - // Phase 1: Write 10 headers with block numbers 0-9 - { - let writer = hot_kv.writer().unwrap(); - for header in headers.iter() { - writer.put_header_inconsistent(header).unwrap(); - } - writer.commit().unwrap(); - } - - // Phase 2: Take range 3..=6 and verify returned keys - { - let writer = hot_kv.writer().unwrap(); - let removed = writer.take_range::(3..=6).unwrap(); - writer.commit().unwrap(); - - // Should return keys 3, 4, 5, 6 in order - assert_eq!(removed.len(), 4); - - for i in 0..4 { - assert_eq!(removed[i].0, (i as u64) + 3); - assert_eq!(&removed[i].1, &headers[i + 3]); - } - } - - // Verify the keys are actually removed - { - let reader = hot_kv.reader().unwrap(); - for i in 0u64..3 { - assert!(reader.get_header(i).unwrap().is_some(), "header {} should exist", i); - } - for i in 3u64..7 { - assert!(reader.get_header(i).unwrap().is_none(), "header {} should be gone", i); - } - for i in 7u64..10 { - assert!(reader.get_header(i).unwrap().is_some(), "header {} should exist", i); - } - } - - // Phase 3: Take empty range (nothing to remove) - { - let writer = hot_kv.writer().unwrap(); - let removed = writer.take_range::(100..=200).unwrap(); - writer.commit().unwrap(); - - assert!(removed.is_empty(), "should return empty vec for non-existent range"); - } - - // Phase 4: Take single key - { - let writer = hot_kv.writer().unwrap(); - let removed = writer.take_range::(8..=8).unwrap(); - writer.commit().unwrap(); - - assert_eq!(removed.len(), 1); - assert_eq!(removed[0].0, 8); - assert_eq!(&removed[0].1, &headers[8]); - } - - { - let reader = hot_kv.reader().unwrap(); - assert!(reader.get_header(7).unwrap().is_some()); - assert!(reader.get_header(8).unwrap().is_none()); - assert!(reader.get_header(9).unwrap().is_some()); - } -} - -/// Test clear_range_dual on a dual-keyed table. -/// -/// This test verifies that: -/// 1. All k2 entries for k1 values within the range are deleted -/// 2. k1 values outside the range remain intact -/// 3. Edge cases work correctly -pub fn test_clear_range_dual(hot_kv: &T) { - let addr1 = address!("0x1000000000000000000000000000000000000001"); - let addr2 = address!("0x2000000000000000000000000000000000000002"); - let addr3 = address!("0x3000000000000000000000000000000000000003"); - let addr4 = address!("0x4000000000000000000000000000000000000004"); - let addr5 = address!("0x5000000000000000000000000000000000000005"); - - // Phase 1: Write account history entries for multiple addresses with multiple shards - { - let writer = hot_kv.writer().unwrap(); - - // addr1: two shards - let history1_a = BlockNumberList::new([1, 2, 3]).unwrap(); - let history1_b = BlockNumberList::new([4, 5, 6]).unwrap(); - writer.write_account_history(&addr1, 100, &history1_a).unwrap(); - writer.write_account_history(&addr1, u64::MAX, &history1_b).unwrap(); - - // addr2: one shard - let history2 = BlockNumberList::new([10, 20]).unwrap(); - writer.write_account_history(&addr2, u64::MAX, &history2).unwrap(); - - // addr3: one shard - let history3 = BlockNumberList::new([30, 40]).unwrap(); - writer.write_account_history(&addr3, u64::MAX, &history3).unwrap(); - - // addr4: two shards - let history4_a = BlockNumberList::new([50, 60]).unwrap(); - let history4_b = BlockNumberList::new([70, 80]).unwrap(); - writer.write_account_history(&addr4, 200, &history4_a).unwrap(); - writer.write_account_history(&addr4, u64::MAX, &history4_b).unwrap(); - - // addr5: one shard - let history5 = BlockNumberList::new([90, 100]).unwrap(); - writer.write_account_history(&addr5, u64::MAX, &history5).unwrap(); - - writer.commit().unwrap(); - } - - // Verify all entries exist - { - let reader = hot_kv.reader().unwrap(); - assert!(reader.get_account_history(&addr1, 100).unwrap().is_some()); - assert!(reader.get_account_history(&addr1, u64::MAX).unwrap().is_some()); - assert!(reader.get_account_history(&addr2, u64::MAX).unwrap().is_some()); - assert!(reader.get_account_history(&addr3, u64::MAX).unwrap().is_some()); - assert!(reader.get_account_history(&addr4, 200).unwrap().is_some()); - assert!(reader.get_account_history(&addr4, u64::MAX).unwrap().is_some()); - assert!(reader.get_account_history(&addr5, u64::MAX).unwrap().is_some()); - } - - // Phase 2: Clear range addr2..=addr3 (middle range) - { - let writer = hot_kv.writer().unwrap(); - writer.clear_range_dual::((addr2, 0)..=(addr3, u64::MAX)).unwrap(); - writer.commit().unwrap(); - } - - // Verify: addr1 and addr4, addr5 should exist, addr2 and addr3 should be gone - { - let reader = hot_kv.reader().unwrap(); - - // addr1 entries should still exist - assert!( - reader.get_account_history(&addr1, 100).unwrap().is_some(), - "addr1 shard 100 should exist" - ); - assert!( - reader.get_account_history(&addr1, u64::MAX).unwrap().is_some(), - "addr1 shard max should exist" - ); - - // addr2 and addr3 should be deleted - assert!( - reader.get_account_history(&addr2, u64::MAX).unwrap().is_none(), - "addr2 should be deleted" - ); - assert!( - reader.get_account_history(&addr3, u64::MAX).unwrap().is_none(), - "addr3 should be deleted" - ); - - // addr4 and addr5 entries should still exist - assert!( - reader.get_account_history(&addr4, 200).unwrap().is_some(), - "addr4 shard 200 should exist" - ); - assert!( - reader.get_account_history(&addr4, u64::MAX).unwrap().is_some(), - "addr4 shard max should exist" - ); - assert!( - reader.get_account_history(&addr5, u64::MAX).unwrap().is_some(), - "addr5 should exist" - ); - } -} - -/// Test take_range_dual on a dual-keyed table. -/// -/// Similar to clear_range_dual but also returns the removed (k1, k2) pairs. -pub fn test_take_range_dual(hot_kv: &T) { - let addr1 = address!("0xa000000000000000000000000000000000000001"); - let addr2 = address!("0xb000000000000000000000000000000000000002"); - let addr3 = address!("0xc000000000000000000000000000000000000003"); - - // Phase 1: Write account history entries - { - let writer = hot_kv.writer().unwrap(); - - // addr1: two shards - let history1_a = BlockNumberList::new([1, 2]).unwrap(); - let history1_b = BlockNumberList::new([3, 4]).unwrap(); - writer.write_account_history(&addr1, 50, &history1_a).unwrap(); - writer.write_account_history(&addr1, u64::MAX, &history1_b).unwrap(); - - // addr2: one shard - let history2 = BlockNumberList::new([10, 20]).unwrap(); - writer.write_account_history(&addr2, u64::MAX, &history2).unwrap(); - - // addr3: one shard - let history3 = BlockNumberList::new([30, 40]).unwrap(); - writer.write_account_history(&addr3, u64::MAX, &history3).unwrap(); - - writer.commit().unwrap(); - } - - // Phase 2: Take range addr1..=addr2 and verify returned pairs - { - let writer = hot_kv.writer().unwrap(); - let removed = writer - .take_range_dual::((addr1, 0)..=(addr2, u64::MAX)) - .unwrap(); - writer.commit().unwrap(); - - // Should return (addr1, 50), (addr1, max), (addr2, max) - assert_eq!(removed.len(), 3, "should have removed 3 entries"); - assert_eq!(removed[0].0, addr1); - assert_eq!(removed[0].1, 50); - assert_eq!(removed[1].0, addr1); - assert_eq!(removed[1].1, u64::MAX); - assert_eq!(removed[2].0, addr2); - assert_eq!(removed[2].1, u64::MAX); - } - - // Verify only addr3 remains - { - let reader = hot_kv.reader().unwrap(); - assert!(reader.get_account_history(&addr1, 50).unwrap().is_none()); - assert!(reader.get_account_history(&addr1, u64::MAX).unwrap().is_none()); - assert!(reader.get_account_history(&addr2, u64::MAX).unwrap().is_none()); - assert!(reader.get_account_history(&addr3, u64::MAX).unwrap().is_some()); - } - - // Phase 3: Take empty range - { - let writer = hot_kv.writer().unwrap(); - let removed = writer - .take_range_dual::( - (address!("0xf000000000000000000000000000000000000000"), 0) - ..=(address!("0xff00000000000000000000000000000000000000"), u64::MAX), - ) - .unwrap(); - writer.commit().unwrap(); - - assert!(removed.is_empty(), "should return empty vec for non-existent range"); - } -} - -// ============================================================================ -// Unwind Conformance Test -// ============================================================================ - -/// Collect all entries from a single-keyed table. -fn collect_single_table(reader: &R) -> Vec> -where - T: SingleKey, - T::Key: Ord, - R: HotKvRead, -{ - let mut cursor = reader.traverse::().unwrap(); - let mut entries = Vec::new(); - if let Some(first) = TableTraverse::::first(&mut *cursor.inner_mut()).unwrap() { - entries.push(first); - while let Some(next) = TableTraverse::::read_next(&mut *cursor.inner_mut()).unwrap() { - entries.push(next); - } - } - entries.sort_by(|a, b| a.0.cmp(&b.0)); - entries -} - -/// Collect all entries from a dual-keyed table. -fn collect_dual_table(reader: &R) -> Vec> -where - T: DualKey, - T::Key: Ord, - T::Key2: Ord, - R: HotKvRead, -{ - let mut cursor = reader.traverse_dual::().unwrap(); - let mut entries = Vec::new(); - if let Some(first) = DualTableTraverse::::first(&mut *cursor.inner_mut()).unwrap() { - entries.push(first); - while let Some(next) = - DualTableTraverse::::read_next(&mut *cursor.inner_mut()).unwrap() - { - entries.push(next); - } - } - entries.sort_by(|a, b| (&a.0, &a.1).cmp(&(&b.0, &b.1))); - entries -} - -/// Assert two single-keyed table contents are equal. -fn assert_single_tables_equal(table_name: &str, a: Vec>, b: Vec>) -where - T: SingleKey, - T::Key: Debug + PartialEq, - T::Value: Debug + PartialEq, -{ - assert_eq!( - a.len(), - b.len(), - "{} table entry count mismatch: {} vs {}", - table_name, - a.len(), - b.len() - ); - for (i, (entry_a, entry_b)) in a.iter().zip(b.iter()).enumerate() { - assert_eq!( - entry_a, entry_b, - "{} table entry {} mismatch:\n A: {:?}\n B: {:?}", - table_name, i, entry_a, entry_b - ); - } -} - -/// Assert two dual-keyed table contents are equal. -fn assert_dual_tables_equal(table_name: &str, a: Vec>, b: Vec>) -where - T: DualKey, - T::Key: Debug + PartialEq, - T::Key2: Debug + PartialEq, - T::Value: Debug + PartialEq, -{ - assert_eq!( - a.len(), - b.len(), - "{} table entry count mismatch: {} vs {}", - table_name, - a.len(), - b.len() - ); - for (i, (entry_a, entry_b)) in a.iter().zip(b.iter()).enumerate() { - assert_eq!( - entry_a, entry_b, - "{} table entry {} mismatch:\n A: {:?}\n B: {:?}", - table_name, i, entry_a, entry_b - ); - } -} - -/// Create a BundleState with account and storage changes. -/// -/// This function creates a proper BundleState with reverts populated so that -/// `to_plain_state_and_reverts` will produce the expected output. -#[allow(clippy::type_complexity)] -fn make_bundle_state( - accounts: Vec<(Address, Option, Option)>, - storage: Vec<(Address, Vec<(U256, U256, U256)>)>, // (addr, [(slot, old, new)]) - _contracts: Vec<(B256, Bytecode)>, -) -> BundleState { - let mut state: HashMap = Default::default(); - - // Build account reverts for this block - let mut block_reverts: Vec<(Address, AccountRevert)> = Vec::new(); - - for (addr, original, info) in &accounts { - let account_storage: HashMap = Default::default(); - state.insert( - *addr, - BundleAccount { - info: info.clone(), - original_info: original.clone(), - storage: account_storage, - status: AccountStatus::Changed, - }, - ); - - // Create account revert - this stores what to restore to when unwinding - let account_info_revert = match original { - Some(orig) => AccountInfoRevert::RevertTo(orig.clone()), - None => AccountInfoRevert::DeleteIt, - }; - - block_reverts.push(( - *addr, - AccountRevert { - account: account_info_revert, - storage: Default::default(), // Storage reverts added below - previous_status: AccountStatus::Changed, - wipe_storage: false, - }, - )); - } - - // Process storage changes - for (addr, slots) in &storage { - let account = state.entry(*addr).or_insert_with(|| BundleAccount { - info: None, - original_info: None, - storage: Default::default(), - status: AccountStatus::Changed, - }); - - // Find or create the account revert entry - let revert_entry = block_reverts.iter_mut().find(|(a, _)| a == addr); - let account_revert = if let Some((_, revert)) = revert_entry { - revert - } else { - block_reverts.push(( - *addr, - AccountRevert { - account: AccountInfoRevert::DoNothing, - storage: Default::default(), - previous_status: AccountStatus::Changed, - wipe_storage: false, - }, - )); - &mut block_reverts.last_mut().unwrap().1 - }; - - for (slot, old_value, new_value) in slots { - account.storage.insert( - *slot, - StorageSlot { previous_or_original_value: *old_value, present_value: *new_value }, - ); - - // Add storage revert entry - account_revert.storage.insert(*slot, RevertToSlot::Some(*old_value)); - } - } - - // Create Reverts with one block's worth of reverts - let reverts = Reverts::new(vec![block_reverts]); - - BundleState { state, contracts: Default::default(), reverts, state_size: 0, reverts_size: 0 } -} - -/// Create a simple AccountInfo for testing. -fn make_account_info(nonce: u64, balance: U256, code_hash: Option) -> AccountInfo { - AccountInfo { nonce, balance, code_hash: code_hash.unwrap_or(B256::ZERO), code: None } -} - -/// Test that unwinding produces the exact same state as never having appended. -/// -/// This test: -/// 1. Creates 5 blocks with complex state changes -/// 2. Appends all 5 blocks to store_a, then unwinds to block 1 (keeping blocks 0, 1) -/// 3. Appends only blocks 0, 1 to store_b -/// 4. Compares ALL tables between the two stores - they must be exactly equal -/// -/// This proves that `unwind_above` correctly reverses all state changes including: -/// - Plain account state -/// - Plain storage state -/// - Headers and header number mappings -/// - Account and storage change sets -/// - Account and storage history indices -pub fn test_unwind_conformance(store_a: &Kv, store_b: &Kv) { - // Test addresses - let addr1 = address!("0x1111111111111111111111111111111111111111"); - let addr2 = address!("0x2222222222222222222222222222222222222222"); - let addr3 = address!("0x3333333333333333333333333333333333333333"); - let addr4 = address!("0x4444444444444444444444444444444444444444"); - - // Storage slots - let slot1 = U256::from(1); - let slot2 = U256::from(2); - let slot3 = U256::from(3); - - // Create bytecode - let code = Bytes::from_static(&[0x60, 0x00, 0x60, 0x00, 0xf3]); - let bytecode = Bytecode::new_raw(code); - let code_hash = bytecode.hash_slow(); - - // Create 5 blocks with complex state - let mut blocks: Vec<(SealedHeader, BundleState)> = Vec::new(); - let mut prev_hash = B256::ZERO; - - // Block 0: Create addr1, addr2, addr3 with different states - { - let header = Header { - number: 0, - parent_hash: prev_hash, - gas_limit: 1_000_000, - ..Default::default() - }; - let sealed = header.seal_slow(); - prev_hash = sealed.hash(); - - let bundle = make_bundle_state( - vec![ - (addr1, None, Some(make_account_info(1, U256::from(100), None))), - (addr2, None, Some(make_account_info(1, U256::from(200), None))), - (addr3, None, Some(make_account_info(1, U256::from(300), None))), - ], - vec![(addr1, vec![(slot1, U256::ZERO, U256::from(10))])], - vec![], - ); - blocks.push((sealed, bundle)); - } - - // Block 1: Update addr1, addr2; add storage to addr2 - { - let header = Header { - number: 1, - parent_hash: prev_hash, - gas_limit: 1_000_000, - ..Default::default() - }; - let sealed = header.seal_slow(); - prev_hash = sealed.hash(); - - let bundle = make_bundle_state( - vec![ - ( - addr1, - Some(make_account_info(1, U256::from(100), None)), - Some(make_account_info(2, U256::from(150), None)), - ), - ( - addr2, - Some(make_account_info(1, U256::from(200), None)), - Some(make_account_info(2, U256::from(250), None)), - ), - ], - vec![ - (addr1, vec![(slot1, U256::from(10), U256::from(20))]), - (addr2, vec![(slot1, U256::ZERO, U256::from(100))]), - ], - vec![], - ); - blocks.push((sealed, bundle)); - } - - // Block 2: Update addr3, add bytecode (this is the boundary - will be unwound) - { - let header = Header { - number: 2, - parent_hash: prev_hash, - gas_limit: 1_000_000, - ..Default::default() - }; - let sealed = header.seal_slow(); - prev_hash = sealed.hash(); - - let bundle = make_bundle_state( - vec![( - addr3, - Some(make_account_info(1, U256::from(300), None)), - Some(make_account_info(2, U256::from(350), Some(code_hash))), - )], - vec![(addr3, vec![(slot1, U256::ZERO, U256::from(1000))])], - vec![(code_hash, bytecode.clone())], - ); - blocks.push((sealed, bundle)); - } - - // Block 3: Create addr4, update existing storage - { - let header = Header { - number: 3, - parent_hash: prev_hash, - gas_limit: 1_000_000, - ..Default::default() - }; - let sealed = header.seal_slow(); - prev_hash = sealed.hash(); - - let bundle = make_bundle_state( - vec![ - (addr4, None, Some(make_account_info(1, U256::from(400), None))), - ( - addr1, - Some(make_account_info(2, U256::from(150), None)), - Some(make_account_info(3, U256::from(175), None)), - ), - ], - vec![ - ( - addr1, - vec![ - (slot1, U256::from(20), U256::from(30)), - (slot2, U256::ZERO, U256::from(50)), - ], - ), - (addr4, vec![(slot1, U256::ZERO, U256::from(500))]), - ], - vec![], - ); - blocks.push((sealed, bundle)); - } - - // Block 4: Update multiple addresses and storage - { - let header = Header { - number: 4, - parent_hash: prev_hash, - gas_limit: 1_000_000, - ..Default::default() - }; - let sealed = header.seal_slow(); - - let bundle = make_bundle_state( - vec![ - ( - addr1, - Some(make_account_info(3, U256::from(175), None)), - Some(make_account_info(4, U256::from(200), None)), - ), - ( - addr2, - Some(make_account_info(2, U256::from(250), None)), - Some(make_account_info(3, U256::from(275), None)), - ), - ( - addr4, - Some(make_account_info(1, U256::from(400), None)), - Some(make_account_info(2, U256::from(450), None)), - ), - ], - vec![ - ( - addr1, - vec![ - (slot1, U256::from(30), U256::from(40)), - (slot3, U256::ZERO, U256::from(60)), - ], - ), - ( - addr2, - vec![ - (slot1, U256::from(100), U256::from(150)), - (slot2, U256::ZERO, U256::from(200)), - ], - ), - ], - vec![], - ); - blocks.push((sealed, bundle)); - } - - // Store A: Append all 5 blocks, then unwind to block 1 - { - let writer = store_a.writer().unwrap(); - writer.append_blocks(&blocks).unwrap(); - writer.commit().unwrap(); - } - { - let writer = store_a.writer().unwrap(); - writer.unwind_above(1).unwrap(); - writer.commit().unwrap(); - } - - // Store B: Append only blocks 0, 1 - { - let writer = store_b.writer().unwrap(); - writer.append_blocks(&blocks[0..2]).unwrap(); - writer.commit().unwrap(); - } - - // Compare all tables - let reader_a = store_a.reader().unwrap(); - let reader_b = store_b.reader().unwrap(); - - // Single-keyed tables - assert_single_tables_equal::( - "Headers", - collect_single_table::(&reader_a), - collect_single_table::(&reader_b), - ); - - assert_single_tables_equal::( - "HeaderNumbers", - collect_single_table::(&reader_a), - collect_single_table::(&reader_b), - ); - - assert_single_tables_equal::( - "PlainAccountState", - collect_single_table::(&reader_a), - collect_single_table::(&reader_b), - ); - - // Note: Bytecodes are not removed on unwind (they're content-addressed), - // so store_a may have more bytecodes than store_b. We skip this comparison. - // assert_single_tables_equal::(...) - - // Dual-keyed tables - assert_dual_tables_equal::( - "PlainStorageState", - collect_dual_table::(&reader_a), - collect_dual_table::(&reader_b), - ); - - assert_dual_tables_equal::( - "AccountChangeSets", - collect_dual_table::(&reader_a), - collect_dual_table::(&reader_b), - ); - - assert_dual_tables_equal::( - "StorageChangeSets", - collect_dual_table::(&reader_a), - collect_dual_table::(&reader_b), - ); - - assert_dual_tables_equal::( - "AccountsHistory", - collect_dual_table::(&reader_a), - collect_dual_table::(&reader_b), - ); - - assert_dual_tables_equal::( - "StorageHistory", - collect_dual_table::(&reader_a), - collect_dual_table::(&reader_b), - ); -} - -// ============================================================================ -// Value Edge Case Tests -// ============================================================================ - -/// Test that zero storage values are correctly stored and retrieved. -/// -/// This verifies that U256::ZERO is not confused with "not set" or deleted. -pub fn test_zero_storage_value(hot_kv: &T) { - let addr = address!("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1"); - let slot = U256::from(1); - - // Write zero value - { - let writer = hot_kv.writer().unwrap(); - writer.put_storage(&addr, &slot, &U256::ZERO).unwrap(); - writer.commit().unwrap(); - } - - // Read zero value - should return Some(ZERO), not None - { - let reader = hot_kv.reader().unwrap(); - let value = reader.get_storage(&addr, &slot).unwrap(); - assert!(value.is_some(), "Zero storage value should be Some, not None"); - assert_eq!(value.unwrap(), U256::ZERO, "Zero storage value should be U256::ZERO"); - } - - // Verify via traversal that the entry exists - { - let reader = hot_kv.reader().unwrap(); - let mut cursor = reader.traverse_dual::().unwrap(); - let mut found = false; - while let Some((k1, k2, v)) = cursor.read_next().unwrap() { - if k1 == addr && k2 == slot { - found = true; - assert_eq!(v, U256::ZERO); - } - } - assert!(found, "Zero value entry should exist in table"); - } -} - -/// Test that empty accounts (all zero fields) are correctly stored and retrieved. -/// -/// This verifies that an account with nonce=0, balance=0, no code is not -/// confused with a non-existent account. -pub fn test_empty_account(hot_kv: &T) { - let addr = address!("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa2"); - let empty_account = Account { nonce: 0, balance: U256::ZERO, bytecode_hash: None }; - - // Write empty account - { - let writer = hot_kv.writer().unwrap(); - writer.put_account(&addr, &empty_account).unwrap(); - writer.commit().unwrap(); - } - - // Read empty account - should return Some, not None - { - let reader = hot_kv.reader().unwrap(); - let account = reader.get_account(&addr).unwrap(); - assert!(account.is_some(), "Empty account should be Some, not None"); - let account = account.unwrap(); - assert_eq!(account.nonce, 0); - assert_eq!(account.balance, U256::ZERO); - assert!(account.bytecode_hash.is_none()); - } -} - -/// Test that maximum storage values (U256::MAX) are correctly stored and retrieved. -pub fn test_max_storage_value(hot_kv: &T) { - let addr = address!("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa3"); - let slot = U256::from(1); - - // Write max value - { - let writer = hot_kv.writer().unwrap(); - writer.put_storage(&addr, &slot, &U256::MAX).unwrap(); - writer.commit().unwrap(); - } - - // Read max value - { - let reader = hot_kv.reader().unwrap(); - let value = reader.get_storage(&addr, &slot).unwrap(); - assert!(value.is_some()); - assert_eq!(value.unwrap(), U256::MAX, "Max storage value should be preserved"); - } -} - -/// Test that maximum block numbers (u64::MAX) work correctly in headers. -pub fn test_max_block_number(hot_kv: &T) { - let header = Header { number: u64::MAX, gas_limit: 1_000_000, ..Default::default() }; - let sealed = header.seal_slow(); - - // Write header at max block number - { - let writer = hot_kv.writer().unwrap(); - writer.put_header(&sealed).unwrap(); - writer.commit().unwrap(); - } - - // Read header - { - let reader = hot_kv.reader().unwrap(); - let read_header = reader.get_header(u64::MAX).unwrap(); - assert!(read_header.is_some()); - assert_eq!(read_header.unwrap().number, u64::MAX); - } -} - -// ============================================================================ -// Cursor Operation Tests -// ============================================================================ - -/// Test cursor operations on an empty table. -/// -/// Verifies that first(), last(), exact(), lower_bound() return None on empty tables. -pub fn test_cursor_empty_table(hot_kv: &T) { - // Use a table that we haven't written to in this test - // We'll use HeaderNumbers which should be empty if we haven't written headers with hashes - let reader = hot_kv.reader().unwrap(); - - // Create a fresh address that definitely doesn't exist - let missing_addr = address!("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb01"); - - // Test single-key cursor on PlainAccountState for a non-existent key - { - let mut cursor = reader.traverse::().unwrap(); - - // exact() for non-existent key should return None - let exact_result = cursor.exact(&missing_addr).unwrap(); - assert!(exact_result.is_none(), "exact() on non-existent key should return None"); - - // lower_bound for a key beyond all existing should return None - let lb_result = - cursor.lower_bound(&address!("0xffffffffffffffffffffffffffffffffffffff99")).unwrap(); - // This might return something if there are entries, but for a truly empty table it would be None - // We're mainly testing that it doesn't panic - let _ = lb_result; - } - - // Test dual-key cursor - { - let mut cursor = reader.traverse_dual::().unwrap(); - - // exact_dual for non-existent keys should return None - let exact_result = cursor.exact_dual(&missing_addr, &U256::from(999)).unwrap(); - assert!(exact_result.is_none(), "exact_dual() on non-existent key should return None"); - } -} - -/// Test cursor exact() match semantics. -/// -/// Verifies that exact() returns only exact matches, not lower_bound semantics. -pub fn test_cursor_exact_match(hot_kv: &T) { - // Write headers at block numbers 10, 20, 30 - { - let writer = hot_kv.writer().unwrap(); - for i in [10u64, 20, 30] { - let header = Header { number: i, gas_limit: 1_000_000, ..Default::default() }; - writer.put_header_inconsistent(&header).unwrap(); - } - writer.commit().unwrap(); - } - - let reader = hot_kv.reader().unwrap(); - let mut cursor = reader.traverse::().unwrap(); - - // exact() for existing key should return value - let exact_10 = cursor.exact(&10u64).unwrap(); - assert!(exact_10.is_some(), "exact(10) should find the header"); - assert_eq!(exact_10.unwrap().number, 10); - - // exact() for non-existing key should return None, not the next key - let exact_15 = cursor.exact(&15u64).unwrap(); - assert!(exact_15.is_none(), "exact(15) should return None, not header 20"); - - // Verify lower_bound would have found something at 15 - let lb_15 = cursor.lower_bound(&15u64).unwrap(); - assert!(lb_15.is_some(), "lower_bound(15) should find header 20"); - assert_eq!(lb_15.unwrap().0, 20); -} - -/// Test cursor backward iteration with read_prev(). -pub fn test_cursor_backward_iteration(hot_kv: &T) { - // Write headers at block numbers 100, 101, 102, 103, 104 - { - let writer = hot_kv.writer().unwrap(); - for i in 100u64..105 { - let header = Header { number: i, gas_limit: 1_000_000, ..Default::default() }; - writer.put_header_inconsistent(&header).unwrap(); - } - writer.commit().unwrap(); - } - - let reader = hot_kv.reader().unwrap(); - let mut cursor = reader.traverse::().unwrap(); - - // Position at last entry - let last = cursor.last().unwrap(); - assert!(last.is_some()); - let (num, _) = last.unwrap(); - assert_eq!(num, 104); - - // Iterate backward - let prev1 = cursor.read_prev().unwrap(); - assert!(prev1.is_some()); - assert_eq!(prev1.unwrap().0, 103); - - let prev2 = cursor.read_prev().unwrap(); - assert!(prev2.is_some()); - assert_eq!(prev2.unwrap().0, 102); - - let prev3 = cursor.read_prev().unwrap(); - assert!(prev3.is_some()); - assert_eq!(prev3.unwrap().0, 101); - - let prev4 = cursor.read_prev().unwrap(); - assert!(prev4.is_some()); - assert_eq!(prev4.unwrap().0, 100); - - // Should hit beginning - let prev5 = cursor.read_prev().unwrap(); - assert!(prev5.is_none(), "read_prev() past beginning should return None"); -} - -/// Test dual-key cursor navigation between k1 values. -pub fn test_cursor_dual_navigation(hot_kv: &T) { - let addr1 = address!("0xcccccccccccccccccccccccccccccccccccccc01"); - let addr2 = address!("0xcccccccccccccccccccccccccccccccccccccc02"); - let addr3 = address!("0xcccccccccccccccccccccccccccccccccccccc03"); - - // Write storage for multiple addresses with multiple slots - { - let writer = hot_kv.writer().unwrap(); - - // addr1: slots 1, 2, 3 - writer.put_storage(&addr1, &U256::from(1), &U256::from(10)).unwrap(); - writer.put_storage(&addr1, &U256::from(2), &U256::from(20)).unwrap(); - writer.put_storage(&addr1, &U256::from(3), &U256::from(30)).unwrap(); - - // addr2: slots 1, 2 - writer.put_storage(&addr2, &U256::from(1), &U256::from(100)).unwrap(); - writer.put_storage(&addr2, &U256::from(2), &U256::from(200)).unwrap(); - - // addr3: slot 1 - writer.put_storage(&addr3, &U256::from(1), &U256::from(1000)).unwrap(); - - writer.commit().unwrap(); - } - - let reader = hot_kv.reader().unwrap(); - let mut cursor = reader.traverse_dual::().unwrap(); - - // Position at first entry - let first = - DualTableTraverse::::first(&mut *cursor.inner_mut()).unwrap(); - assert!(first.is_some()); - let (k1, k2, _) = first.unwrap(); - assert_eq!(k1, addr1); - assert_eq!(k2, U256::from(1)); - - // next_k1() should jump to addr2 - let next_addr = cursor.next_k1().unwrap(); - assert!(next_addr.is_some()); - let (k1, k2, _) = next_addr.unwrap(); - assert_eq!(k1, addr2, "next_k1() should jump to addr2"); - assert_eq!(k2, U256::from(1), "Should be at first slot of addr2"); - - // next_k1() again should jump to addr3 - let next_addr = cursor.next_k1().unwrap(); - assert!(next_addr.is_some()); - let (k1, _, _) = next_addr.unwrap(); - assert_eq!(k1, addr3, "next_k1() should jump to addr3"); - - // next_k1() again should return None (no more k1 values) - let next_addr = cursor.next_k1().unwrap(); - assert!(next_addr.is_none(), "next_k1() at end should return None"); - - // Test previous_k1() - // First position at addr3 - cursor.last_of_k1(&addr3).unwrap(); - let prev_addr = cursor.previous_k1().unwrap(); - assert!(prev_addr.is_some()); - let (k1, _, _) = prev_addr.unwrap(); - assert_eq!(k1, addr2, "previous_k1() from addr3 should go to addr2"); -} - -/// Test cursor on table with single entry. -pub fn test_cursor_single_entry(hot_kv: &T) { - let addr = address!("0xdddddddddddddddddddddddddddddddddddddd01"); - let account = Account { nonce: 42, balance: U256::from(1000), bytecode_hash: None }; - - // Write single account - { - let writer = hot_kv.writer().unwrap(); - writer.put_account(&addr, &account).unwrap(); - writer.commit().unwrap(); - } - - let reader = hot_kv.reader().unwrap(); - let mut cursor = reader.traverse::().unwrap(); - - // first() and last() should return the same entry - let first = cursor.first().unwrap(); - assert!(first.is_some()); - let (first_addr, _) = first.unwrap(); - - let last = cursor.last().unwrap(); - assert!(last.is_some()); - let (last_addr, _) = last.unwrap(); - - assert_eq!(first_addr, last_addr, "first() and last() should be same for single entry"); - - // read_next() after first() should return None - cursor.first().unwrap(); - let next = cursor.read_next().unwrap(); - assert!(next.is_none(), "read_next() after first() on single entry should return None"); -} - -// ============================================================================ -// Batch Operation Tests -// ============================================================================ - -/// Test get_many batch retrieval. -pub fn test_get_many(hot_kv: &T) { - let addr1 = address!("0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee01"); - let addr2 = address!("0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee02"); - let addr3 = address!("0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee03"); - - let acc1 = Account { nonce: 1, balance: U256::from(100), bytecode_hash: None }; - let acc2 = Account { nonce: 2, balance: U256::from(200), bytecode_hash: None }; - let acc3 = Account { nonce: 3, balance: U256::from(300), bytecode_hash: None }; - - // Write accounts - { - let writer = hot_kv.writer().unwrap(); - writer.put_account(&addr1, &acc1).unwrap(); - writer.put_account(&addr2, &acc2).unwrap(); - writer.put_account(&addr3, &acc3).unwrap(); - writer.commit().unwrap(); - } -} - -/// Test queue_put_many batch writes. -pub fn test_queue_put_many(hot_kv: &T) { - let entries: Vec<(u64, Header)> = (200u64..210) - .map(|i| (i, Header { number: i, gas_limit: 1_000_000, ..Default::default() })) - .collect(); - - // Batch write using queue_put_many - { - let writer = hot_kv.writer().unwrap(); - let refs: Vec<(&u64, &Header)> = entries.iter().map(|(k, v)| (k, v)).collect(); - writer.queue_put_many::(refs).unwrap(); - writer.commit().unwrap(); - } - - // Verify all entries exist - { - let reader = hot_kv.reader().unwrap(); - for i in 200u64..210 { - let header = reader.get_header(i).unwrap(); - assert!(header.is_some(), "Header {} should exist after batch write", i); - assert_eq!(header.unwrap().number, i); - } - } -} - -/// Test queue_clear clears all entries in a table. -pub fn test_queue_clear(hot_kv: &T) { - // Write some headers - { - let writer = hot_kv.writer().unwrap(); - for i in 300u64..310 { - let header = Header { number: i, gas_limit: 1_000_000, ..Default::default() }; - writer.put_header_inconsistent(&header).unwrap(); - } - writer.commit().unwrap(); - } - - // Verify entries exist - { - let reader = hot_kv.reader().unwrap(); - for i in 300u64..310 { - assert!(reader.get_header(i).unwrap().is_some()); - } - } - - // Clear the table - { - let writer = hot_kv.writer().unwrap(); - writer.queue_clear::().unwrap(); - writer.commit().unwrap(); - } - - // Verify all entries are gone - { - let reader = hot_kv.reader().unwrap(); - for i in 300u64..310 { - assert!( - reader.get_header(i).unwrap().is_none(), - "Header {} should be gone after clear", - i - ); - } - } -} - -// ============================================================================ -// Transaction Ordering Tests -// ============================================================================ - -/// Test that put-then-delete in the same transaction results in deletion. -pub fn test_put_then_delete_same_key(hot_kv: &T) { - let addr = address!("0xffffffffffffffffffffffffffffffffffff0001"); - let account = Account { nonce: 99, balance: U256::from(9999), bytecode_hash: None }; - - // In a single transaction: put then delete - { - let writer = hot_kv.writer().unwrap(); - writer.put_account(&addr, &account).unwrap(); - writer.queue_delete::(&addr).unwrap(); - writer.commit().unwrap(); - } - - // Account should not exist - { - let reader = hot_kv.reader().unwrap(); - let result = reader.get_account(&addr).unwrap(); - assert!(result.is_none(), "Put-then-delete should result in no entry"); - } -} - -/// Test that delete-then-put in the same transaction results in the put value. -pub fn test_delete_then_put_same_key(hot_kv: &T) { - let addr = address!("0xffffffffffffffffffffffffffffffffffff0002"); - let old_account = Account { nonce: 1, balance: U256::from(100), bytecode_hash: None }; - let new_account = Account { nonce: 2, balance: U256::from(200), bytecode_hash: None }; - - // First, write an account - { - let writer = hot_kv.writer().unwrap(); - writer.put_account(&addr, &old_account).unwrap(); - writer.commit().unwrap(); - } - - // In a single transaction: delete then put new value - { - let writer = hot_kv.writer().unwrap(); - writer.queue_delete::(&addr).unwrap(); - writer.put_account(&addr, &new_account).unwrap(); - writer.commit().unwrap(); - } - - // Should have the new value - { - let reader = hot_kv.reader().unwrap(); - let result = reader.get_account(&addr).unwrap(); - assert!(result.is_some(), "Delete-then-put should result in entry existing"); - let account = result.unwrap(); - assert_eq!(account.nonce, 2, "Should have the new nonce"); - assert_eq!(account.balance, U256::from(200), "Should have the new balance"); - } -} - -/// Test that multiple puts to the same key in one transaction use last value. -pub fn test_multiple_puts_same_key(hot_kv: &T) { - let addr = address!("0xffffffffffffffffffffffffffffffffffff0003"); - - // In a single transaction: put three different values - { - let writer = hot_kv.writer().unwrap(); - writer - .put_account( - &addr, - &Account { nonce: 1, balance: U256::from(100), bytecode_hash: None }, - ) - .unwrap(); - writer - .put_account( - &addr, - &Account { nonce: 2, balance: U256::from(200), bytecode_hash: None }, - ) - .unwrap(); - writer - .put_account( - &addr, - &Account { nonce: 3, balance: U256::from(300), bytecode_hash: None }, - ) - .unwrap(); - writer.commit().unwrap(); - } - - // Should have the last value - { - let reader = hot_kv.reader().unwrap(); - let result = reader.get_account(&addr).unwrap(); - assert!(result.is_some()); - let account = result.unwrap(); - assert_eq!(account.nonce, 3, "Should have the last nonce (3)"); - assert_eq!(account.balance, U256::from(300), "Should have the last balance (300)"); - } -} - -/// Test that abandoned transaction (dropped without commit) makes no changes. -pub fn test_abandoned_transaction(hot_kv: &T) { - let addr = address!("0xffffffffffffffffffffffffffffffffffff0004"); - let account = Account { nonce: 42, balance: U256::from(4200), bytecode_hash: None }; - - // Start a transaction, write, but don't commit (drop it) - { - let writer = hot_kv.writer().unwrap(); - writer.put_account(&addr, &account).unwrap(); - // writer is dropped here without commit - } - - // Account should not exist - { - let reader = hot_kv.reader().unwrap(); - let result = reader.get_account(&addr).unwrap(); - assert!(result.is_none(), "Abandoned transaction should not persist changes"); - } -} - -// ============================================================================ -// Chain Validation Error Tests -// ============================================================================ - -/// Test that validate_chain_extension rejects non-contiguous blocks. -pub fn test_validate_noncontiguous_blocks(hot_kv: &Kv) { - // First, append a genesis block - let genesis = make_header(0, B256::ZERO); - { - let writer = hot_kv.writer().unwrap(); - let bundle = make_bundle_state(vec![], vec![], vec![]); - writer.append_blocks(&[(genesis.clone(), bundle)]).unwrap(); - writer.commit().unwrap(); - } - - // Try to append block 2 (skipping block 1) - let block2 = make_header(2, genesis.hash()); - { - let writer = hot_kv.writer().unwrap(); - let bundle = make_bundle_state(vec![], vec![], vec![]); - let result = writer.append_blocks(&[(block2, bundle)]); - - match result { - Err(HistoryError::NonContiguousBlock { expected, got }) => { - assert_eq!(expected, 1, "Expected block should be 1"); - assert_eq!(got, 2, "Got block should be 2"); - } - Err(e) => panic!("Expected NonContiguousBlock error, got: {:?}", e), - Ok(_) => panic!("Expected error for non-contiguous blocks"), - } - } -} - -/// Test that validate_chain_extension rejects wrong parent hash. -pub fn test_validate_parent_hash_mismatch(hot_kv: &Kv) { - // Append genesis block - let genesis = make_header(0, B256::ZERO); - { - let writer = hot_kv.writer().unwrap(); - let bundle = make_bundle_state(vec![], vec![], vec![]); - writer.append_blocks(&[(genesis.clone(), bundle)]).unwrap(); - writer.commit().unwrap(); - } - - // Try to append block 1 with wrong parent hash - let wrong_parent = b256!("0x1111111111111111111111111111111111111111111111111111111111111111"); - let block1 = make_header(1, wrong_parent); - { - let writer = hot_kv.writer().unwrap(); - let bundle = make_bundle_state(vec![], vec![], vec![]); - let result = writer.append_blocks(&[(block1, bundle)]); - - match result { - Err(HistoryError::ParentHashMismatch { expected, got }) => { - assert_eq!(expected, genesis.hash(), "Expected parent should be genesis hash"); - assert_eq!(got, wrong_parent, "Got parent should be wrong_parent"); - } - Err(e) => panic!("Expected ParentHashMismatch error, got: {:?}", e), - Ok(_) => panic!("Expected error for parent hash mismatch"), - } - } -} - -/// Test appending genesis block (block 0) to empty database. -pub fn test_append_genesis_block(hot_kv: &Kv) { - let addr = address!("0x0000000000000000000000000000000000000001"); - - // Create genesis block with initial state - let genesis = make_header(0, B256::ZERO); - let bundle = make_bundle_state( - vec![(addr, None, Some(make_account_info(0, U256::from(1_000_000), None)))], - vec![], - vec![], - ); - - // Append genesis - { - let writer = hot_kv.writer().unwrap(); - writer.append_blocks(&[(genesis.clone(), bundle)]).unwrap(); - writer.commit().unwrap(); - } - - // Verify genesis exists - { - let reader = hot_kv.reader().unwrap(); - let header = reader.get_header(0).unwrap(); - assert!(header.is_some(), "Genesis header should exist"); - assert_eq!(header.unwrap().number, 0); - - // Verify chain tip - let tip = reader.get_chain_tip().unwrap(); - assert!(tip.is_some()); - let (num, hash) = tip.unwrap(); - assert_eq!(num, 0); - assert_eq!(hash, genesis.hash()); - } -} - -/// Test unwinding to block 0 (keeping only genesis). -pub fn test_unwind_to_zero(hot_kv: &Kv) { - let addr = address!("0x1111111111111111111111111111111111111111"); - - // Build a chain of 5 blocks - let mut blocks = Vec::new(); - let mut prev_hash = B256::ZERO; - - for i in 0u64..5 { - let header = make_header(i, prev_hash); - prev_hash = header.hash(); - - let bundle = make_bundle_state( - vec![( - addr, - if i == 0 { - None - } else { - Some(make_account_info(i - 1, U256::from(i * 100), None)) - }, - Some(make_account_info(i, U256::from((i + 1) * 100), None)), - )], - vec![], - vec![], - ); - blocks.push((header, bundle)); - } - - // Append all blocks - { - let writer = hot_kv.writer().unwrap(); - writer.append_blocks(&blocks).unwrap(); - writer.commit().unwrap(); - } - - // Verify chain tip is at block 4 - { - let reader = hot_kv.reader().unwrap(); - let tip = reader.last_block_number().unwrap(); - assert_eq!(tip, Some(4)); - } - - // Unwind to block 0 (keep only genesis) - { - let writer = hot_kv.writer().unwrap(); - writer.unwind_above(0).unwrap(); - writer.commit().unwrap(); - } - - // Verify only genesis remains - { - let reader = hot_kv.reader().unwrap(); - let tip = reader.last_block_number().unwrap(); - assert_eq!(tip, Some(0), "Only genesis should remain after unwind to 0"); - - // Verify blocks 1-4 are gone - for i in 1u64..5 { - assert!(reader.get_header(i).unwrap().is_none(), "Block {} should be gone", i); - } - - // Verify genesis account state (nonce=0 from block 0) - let account = reader.get_account(&addr).unwrap(); - assert!(account.is_some()); - assert_eq!(account.unwrap().nonce, 0, "Account should have genesis state"); - } -} - -// ============================================================================ -// History Sharding Tests -// ============================================================================ - -/// Test history at exactly the shard boundary. -/// -/// NUM_OF_INDICES_IN_SHARD is typically 1000. This test writes exactly that many -/// entries to verify boundary handling. -pub fn test_history_shard_boundary(hot_kv: &T) { - let addr = address!("0xaaaabbbbccccddddeeeeffffaaaabbbbccccdddd"); - let shard_size = signet_storage_types::ShardedKey::SHARD_COUNT; - - // Write exactly shard_size account changes - { - let writer = hot_kv.writer().unwrap(); - for i in 1..=shard_size { - let acc = Account { nonce: i as u64, balance: U256::from(i), bytecode_hash: None }; - writer.write_account_prestate(i as u64, addr, &acc).unwrap(); - } - writer.commit().unwrap(); - } - - // Build history indices - { - let writer = hot_kv.writer().unwrap(); - writer.update_history_indices_inconsistent(1..=(shard_size as u64)).unwrap(); - writer.commit().unwrap(); - } - - // Verify history - should fit in exactly one shard - { - let reader = hot_kv.reader().unwrap(); - let (key, history) = - reader.last_account_history(addr).unwrap().expect("Should have history"); - - // With exactly shard_size entries, it should be stored with key = u64::MAX - assert_eq!(key, u64::MAX, "Shard key should be u64::MAX for single full shard"); - - let blocks: Vec = history.iter().collect(); - assert_eq!(blocks.len(), shard_size, "Should have exactly {} blocks", shard_size); - } -} - -/// Test history overflow into multiple shards. -pub fn test_history_multi_shard(hot_kv: &T) { - let addr = address!("0xbbbbccccddddeeeeffffaaaabbbbccccddddeee1"); - let shard_size = ShardedKey::SHARD_COUNT; - let total_entries = shard_size + 100; // Overflow into second shard - - // Write more than shard_size account changes - { - let writer = hot_kv.writer().unwrap(); - for i in 1..=total_entries { - let acc = Account { nonce: i as u64, balance: U256::from(i), bytecode_hash: None }; - writer.write_account_prestate(i as u64, addr, &acc).unwrap(); - } - writer.commit().unwrap(); - } - - // Build history indices - { - let writer = hot_kv.writer().unwrap(); - writer.update_history_indices_inconsistent(1..=(total_entries as u64)).unwrap(); - writer.commit().unwrap(); - } - - // Verify we have multiple shards - { - let reader = hot_kv.reader().unwrap(); - - // Count shards by traversing - let mut cursor = reader.traverse_dual::().unwrap(); - let mut shard_count = 0; - let mut total_blocks = 0; - - // Find entries for our address - if let Some((k1, _, list)) = cursor.next_dual_above(&addr, &0u64).unwrap() - && k1 == addr - { - shard_count += 1; - total_blocks += list.iter().count(); - - // Continue reading for same address - while let Some((k1, _, list)) = cursor.read_next().unwrap() { - if k1 != addr { - break; - } - shard_count += 1; - total_blocks += list.iter().count(); - } - } - - assert!(shard_count >= 2, "Should have at least 2 shards, got {}", shard_count); - assert_eq!(total_blocks, total_entries, "Total blocks across shards should match"); - } -} - -// ============================================================================ -// HistoryRead Method Tests -// ============================================================================ - -/// Test get_headers_range retrieves headers in range. -pub fn test_get_headers_range(hot_kv: &T) { - // Write headers 500-509 - { - let writer = hot_kv.writer().unwrap(); - for i in 500u64..510 { - let header = Header { number: i, gas_limit: 1_000_000, ..Default::default() }; - writer.put_header_inconsistent(&header).unwrap(); - } - writer.commit().unwrap(); - } - - let reader = hot_kv.reader().unwrap(); - - // Get range 502-506 - let headers = reader.get_headers_range(502, 506).unwrap(); - assert_eq!(headers.len(), 5, "Should get 5 headers (502, 503, 504, 505, 506)"); - for (i, header) in headers.iter().enumerate() { - assert_eq!(header.number, 502 + i as u64); - } - - // Get range that starts before existing entries - let headers = reader.get_headers_range(498, 502).unwrap(); - // Should get 500, 501, 502 (498 and 499 don't exist) - assert_eq!(headers.len(), 3); - - // Get range with no entries - let headers = reader.get_headers_range(600, 610).unwrap(); - assert!(headers.is_empty(), "Should get empty vec for non-existent range"); -} - -/// Test first_header and last_header. -pub fn test_first_last_header(hot_kv: &T) { - // Write headers 1000, 1005, 1010 - { - let writer = hot_kv.writer().unwrap(); - for i in [1000u64, 1005, 1010] { - let header = Header { number: i, gas_limit: 1_000_000, ..Default::default() }; - writer.put_header_inconsistent(&header).unwrap(); - } - writer.commit().unwrap(); - } - - let reader = hot_kv.reader().unwrap(); - - let first = reader.first_header().unwrap(); - assert!(first.is_some()); - assert_eq!(first.unwrap().number, 1000); - - let last = reader.last_header().unwrap(); - assert!(last.is_some()); - assert_eq!(last.unwrap().number, 1010); -} - -/// Test has_block returns correct boolean. -pub fn test_has_block(hot_kv: &T) { - // Write header at block 2000 - { - let writer = hot_kv.writer().unwrap(); - let header = Header { number: 2000, gas_limit: 1_000_000, ..Default::default() }; - writer.put_header_inconsistent(&header).unwrap(); - writer.commit().unwrap(); - } - - let reader = hot_kv.reader().unwrap(); - - assert!(reader.has_block(2000).unwrap(), "Block 2000 should exist"); - assert!(!reader.has_block(2001).unwrap(), "Block 2001 should not exist"); - assert!(!reader.has_block(1999).unwrap(), "Block 1999 should not exist"); -} - -/// Test get_execution_range returns first and last block numbers. -pub fn test_get_execution_range(hot_kv: &T) { - // Write headers 3000, 3001, 3002 - { - let writer = hot_kv.writer().unwrap(); - for i in [3000u64, 3001, 3002] { - let header = Header { number: i, gas_limit: 1_000_000, ..Default::default() }; - writer.put_header_inconsistent(&header).unwrap(); - } - writer.commit().unwrap(); - } - - let reader = hot_kv.reader().unwrap(); - - let range = reader.get_execution_range().unwrap(); - assert!(range.is_some()); - let (first, last) = range.unwrap(); - assert_eq!(first, 3000); - assert_eq!(last, 3002); -} - -/// Test get_chain_tip returns highest block number and hash. -pub fn test_get_chain_tip(hot_kv: &T) { - let header = Header { number: 4000, gas_limit: 1_000_000, ..Default::default() }; - let expected_hash = header.hash_slow(); - - { - let writer = hot_kv.writer().unwrap(); - writer.put_header_inconsistent(&header).unwrap(); - writer.commit().unwrap(); - } - - let reader = hot_kv.reader().unwrap(); - - let tip = reader.get_chain_tip().unwrap(); - assert!(tip.is_some()); - let (num, hash) = tip.unwrap(); - assert_eq!(num, 4000); - assert_eq!(hash, expected_hash); -} diff --git a/crates/hot/src/conformance/cursor.rs b/crates/hot/src/conformance/cursor.rs new file mode 100644 index 0000000..f92a86d --- /dev/null +++ b/crates/hot/src/conformance/cursor.rs @@ -0,0 +1,218 @@ +//! Cursor operation tests for hot storage. + +use crate::{ + db::UnsafeDbWrite, + model::{DualTableTraverse, HotKv, HotKvRead}, + tables, +}; +use alloy::{ + consensus::Header, + primitives::{U256, address}, +}; +use signet_storage_types::Account; + +/// Test cursor operations on an empty table. +/// +/// Verifies that first(), last(), exact(), lower_bound() return None on empty tables. +pub fn test_cursor_empty_table(hot_kv: &T) { + // Use a table that we haven't written to in this test + // We'll use HeaderNumbers which should be empty if we haven't written headers with hashes + let reader = hot_kv.reader().unwrap(); + + // Create a fresh address that definitely doesn't exist + let missing_addr = address!("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb01"); + + // Test single-key cursor on PlainAccountState for a non-existent key + { + let mut cursor = reader.traverse::().unwrap(); + + // exact() for non-existent key should return None + let exact_result = cursor.exact(&missing_addr).unwrap(); + assert!(exact_result.is_none(), "exact() on non-existent key should return None"); + + // lower_bound for a key beyond all existing should return None + let lb_result = + cursor.lower_bound(&address!("0xffffffffffffffffffffffffffffffffffffff99")).unwrap(); + // This might return something if there are entries, but for a truly empty table it would be None + // We're mainly testing that it doesn't panic + let _ = lb_result; + } + + // Test dual-key cursor + { + let mut cursor = reader.traverse_dual::().unwrap(); + + // exact_dual for non-existent keys should return None + let exact_result = cursor.exact_dual(&missing_addr, &U256::from(999)).unwrap(); + assert!(exact_result.is_none(), "exact_dual() on non-existent key should return None"); + } +} + +/// Test cursor exact() match semantics. +/// +/// Verifies that exact() returns only exact matches, not lower_bound semantics. +pub fn test_cursor_exact_match(hot_kv: &T) { + // Write headers at block numbers 10, 20, 30 + { + let writer = hot_kv.writer().unwrap(); + for i in [10u64, 20, 30] { + let header = Header { number: i, gas_limit: 1_000_000, ..Default::default() }; + writer.put_header_inconsistent(&header).unwrap(); + } + writer.commit().unwrap(); + } + + let reader = hot_kv.reader().unwrap(); + let mut cursor = reader.traverse::().unwrap(); + + // exact() for existing key should return value + let exact_10 = cursor.exact(&10u64).unwrap(); + assert!(exact_10.is_some(), "exact(10) should find the header"); + assert_eq!(exact_10.unwrap().number, 10); + + // exact() for non-existing key should return None, not the next key + let exact_15 = cursor.exact(&15u64).unwrap(); + assert!(exact_15.is_none(), "exact(15) should return None, not header 20"); + + // Verify lower_bound would have found something at 15 + let lb_15 = cursor.lower_bound(&15u64).unwrap(); + assert!(lb_15.is_some(), "lower_bound(15) should find header 20"); + assert_eq!(lb_15.unwrap().0, 20); +} + +/// Test cursor backward iteration with read_prev(). +pub fn test_cursor_backward_iteration(hot_kv: &T) { + // Write headers at block numbers 100, 101, 102, 103, 104 + { + let writer = hot_kv.writer().unwrap(); + for i in 100u64..105 { + let header = Header { number: i, gas_limit: 1_000_000, ..Default::default() }; + writer.put_header_inconsistent(&header).unwrap(); + } + writer.commit().unwrap(); + } + + let reader = hot_kv.reader().unwrap(); + let mut cursor = reader.traverse::().unwrap(); + + // Position at last entry + let last = cursor.last().unwrap(); + assert!(last.is_some()); + let (num, _) = last.unwrap(); + assert_eq!(num, 104); + + // Iterate backward + let prev1 = cursor.read_prev().unwrap(); + assert!(prev1.is_some()); + assert_eq!(prev1.unwrap().0, 103); + + let prev2 = cursor.read_prev().unwrap(); + assert!(prev2.is_some()); + assert_eq!(prev2.unwrap().0, 102); + + let prev3 = cursor.read_prev().unwrap(); + assert!(prev3.is_some()); + assert_eq!(prev3.unwrap().0, 101); + + let prev4 = cursor.read_prev().unwrap(); + assert!(prev4.is_some()); + assert_eq!(prev4.unwrap().0, 100); + + // Should hit beginning + let prev5 = cursor.read_prev().unwrap(); + assert!(prev5.is_none(), "read_prev() past beginning should return None"); +} + +/// Test dual-key cursor navigation between k1 values. +pub fn test_cursor_dual_navigation(hot_kv: &T) { + let addr1 = address!("0xcccccccccccccccccccccccccccccccccccccc01"); + let addr2 = address!("0xcccccccccccccccccccccccccccccccccccccc02"); + let addr3 = address!("0xcccccccccccccccccccccccccccccccccccccc03"); + + // Write storage for multiple addresses with multiple slots + { + let writer = hot_kv.writer().unwrap(); + + // addr1: slots 1, 2, 3 + writer.put_storage(&addr1, &U256::from(1), &U256::from(10)).unwrap(); + writer.put_storage(&addr1, &U256::from(2), &U256::from(20)).unwrap(); + writer.put_storage(&addr1, &U256::from(3), &U256::from(30)).unwrap(); + + // addr2: slots 1, 2 + writer.put_storage(&addr2, &U256::from(1), &U256::from(100)).unwrap(); + writer.put_storage(&addr2, &U256::from(2), &U256::from(200)).unwrap(); + + // addr3: slot 1 + writer.put_storage(&addr3, &U256::from(1), &U256::from(1000)).unwrap(); + + writer.commit().unwrap(); + } + + let reader = hot_kv.reader().unwrap(); + let mut cursor = reader.traverse_dual::().unwrap(); + + // Position at first entry + let first = + DualTableTraverse::::first(&mut *cursor.inner_mut()).unwrap(); + assert!(first.is_some()); + let (k1, k2, _) = first.unwrap(); + assert_eq!(k1, addr1); + assert_eq!(k2, U256::from(1)); + + // next_k1() should jump to addr2 + let next_addr = cursor.next_k1().unwrap(); + assert!(next_addr.is_some()); + let (k1, k2, _) = next_addr.unwrap(); + assert_eq!(k1, addr2, "next_k1() should jump to addr2"); + assert_eq!(k2, U256::from(1), "Should be at first slot of addr2"); + + // next_k1() again should jump to addr3 + let next_addr = cursor.next_k1().unwrap(); + assert!(next_addr.is_some()); + let (k1, _, _) = next_addr.unwrap(); + assert_eq!(k1, addr3, "next_k1() should jump to addr3"); + + // next_k1() again should return None (no more k1 values) + let next_addr = cursor.next_k1().unwrap(); + assert!(next_addr.is_none(), "next_k1() at end should return None"); + + // Test previous_k1() + // First position at addr3 + cursor.last_of_k1(&addr3).unwrap(); + let prev_addr = cursor.previous_k1().unwrap(); + assert!(prev_addr.is_some()); + let (k1, _, _) = prev_addr.unwrap(); + assert_eq!(k1, addr2, "previous_k1() from addr3 should go to addr2"); +} + +/// Test cursor on table with single entry. +pub fn test_cursor_single_entry(hot_kv: &T) { + let addr = address!("0xdddddddddddddddddddddddddddddddddddddd01"); + let account = Account { nonce: 42, balance: U256::from(1000), bytecode_hash: None }; + + // Write single account + { + let writer = hot_kv.writer().unwrap(); + writer.put_account(&addr, &account).unwrap(); + writer.commit().unwrap(); + } + + let reader = hot_kv.reader().unwrap(); + let mut cursor = reader.traverse::().unwrap(); + + // first() and last() should return the same entry + let first = cursor.first().unwrap(); + assert!(first.is_some()); + let (first_addr, _) = first.unwrap(); + + let last = cursor.last().unwrap(); + assert!(last.is_some()); + let (last_addr, _) = last.unwrap(); + + assert_eq!(first_addr, last_addr, "first() and last() should be same for single entry"); + + // read_next() after first() should return None + cursor.first().unwrap(); + let next = cursor.read_next().unwrap(); + assert!(next.is_none(), "read_next() after first() on single entry should return None"); +} diff --git a/crates/hot/src/conformance/edge_cases.rs b/crates/hot/src/conformance/edge_cases.rs new file mode 100644 index 0000000..db30a39 --- /dev/null +++ b/crates/hot/src/conformance/edge_cases.rs @@ -0,0 +1,256 @@ +//! Value edge cases and batch operations tests. + +use crate::{ + db::{HotDbRead, UnsafeDbWrite}, + model::{HotKv, HotKvRead, HotKvWrite}, + tables, +}; +use alloy::{ + consensus::{Header, Sealable}, + primitives::{U256, address}, +}; +use signet_storage_types::Account; + +/// Test that zero storage values are correctly stored and retrieved. +/// +/// This verifies that U256::ZERO is not confused with "not set" or deleted. +pub fn test_zero_storage_value(hot_kv: &T) { + let addr = address!("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1"); + let slot = U256::from(1); + + // Write zero value + { + let writer = hot_kv.writer().unwrap(); + writer.put_storage(&addr, &slot, &U256::ZERO).unwrap(); + writer.commit().unwrap(); + } + + // Read zero value - should return Some(ZERO), not None + { + let reader = hot_kv.reader().unwrap(); + let value = reader.get_storage(&addr, &slot).unwrap(); + assert!(value.is_some(), "Zero storage value should be Some, not None"); + assert_eq!(value.unwrap(), U256::ZERO, "Zero storage value should be U256::ZERO"); + } + + // Verify via traversal that the entry exists + { + let reader = hot_kv.reader().unwrap(); + let mut cursor = reader.traverse_dual::().unwrap(); + let mut found = false; + while let Some((k1, k2, v)) = cursor.read_next().unwrap() { + if k1 == addr && k2 == slot { + found = true; + assert_eq!(v, U256::ZERO); + } + } + assert!(found, "Zero value entry should exist in table"); + } +} + +/// Test that empty accounts (all zero fields) are correctly stored and retrieved. +/// +/// This verifies that an account with nonce=0, balance=0, no code is not +/// confused with a non-existent account. +pub fn test_empty_account(hot_kv: &T) { + let addr = address!("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa2"); + let empty_account = Account { nonce: 0, balance: U256::ZERO, bytecode_hash: None }; + + // Write empty account + { + let writer = hot_kv.writer().unwrap(); + writer.put_account(&addr, &empty_account).unwrap(); + writer.commit().unwrap(); + } + + // Read empty account - should return Some, not None + { + let reader = hot_kv.reader().unwrap(); + let account = reader.get_account(&addr).unwrap(); + assert!(account.is_some(), "Empty account should be Some, not None"); + let account = account.unwrap(); + assert_eq!(account.nonce, 0); + assert_eq!(account.balance, U256::ZERO); + assert!(account.bytecode_hash.is_none()); + } +} + +/// Test that maximum storage values (U256::MAX) are correctly stored and retrieved. +pub fn test_max_storage_value(hot_kv: &T) { + let addr = address!("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa3"); + let slot = U256::from(1); + + // Write max value + { + let writer = hot_kv.writer().unwrap(); + writer.put_storage(&addr, &slot, &U256::MAX).unwrap(); + writer.commit().unwrap(); + } + + // Read max value + { + let reader = hot_kv.reader().unwrap(); + let value = reader.get_storage(&addr, &slot).unwrap(); + assert!(value.is_some()); + assert_eq!(value.unwrap(), U256::MAX, "Max storage value should be preserved"); + } +} + +/// Test that maximum block numbers (u64::MAX) work correctly in headers. +pub fn test_max_block_number(hot_kv: &T) { + let header = Header { number: u64::MAX, gas_limit: 1_000_000, ..Default::default() }; + let sealed = header.seal_slow(); + + // Write header at max block number + { + let writer = hot_kv.writer().unwrap(); + writer.put_header(&sealed).unwrap(); + writer.commit().unwrap(); + } + + // Read header + { + let reader = hot_kv.reader().unwrap(); + let read_header = reader.get_header(u64::MAX).unwrap(); + assert!(read_header.is_some()); + assert_eq!(read_header.unwrap().number, u64::MAX); + } +} + +/// Test get_many batch retrieval. +pub fn test_get_many(hot_kv: &T) { + let addr1 = address!("0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee01"); + let addr2 = address!("0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee02"); + let addr3 = address!("0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee03"); + + let acc1 = Account { nonce: 1, balance: U256::from(100), bytecode_hash: None }; + let acc2 = Account { nonce: 2, balance: U256::from(200), bytecode_hash: None }; + let acc3 = Account { nonce: 3, balance: U256::from(300), bytecode_hash: None }; + + // Write accounts + { + let writer = hot_kv.writer().unwrap(); + writer.put_account(&addr1, &acc1).unwrap(); + writer.put_account(&addr2, &acc2).unwrap(); + writer.put_account(&addr3, &acc3).unwrap(); + writer.commit().unwrap(); + } +} + +/// Test queue_put_many batch writes. +pub fn test_queue_put_many(hot_kv: &T) { + let entries: Vec<(u64, Header)> = (200u64..210) + .map(|i| (i, Header { number: i, gas_limit: 1_000_000, ..Default::default() })) + .collect(); + + // Batch write using queue_put_many + { + let writer = hot_kv.writer().unwrap(); + let refs: Vec<(&u64, &Header)> = entries.iter().map(|(k, v)| (k, v)).collect(); + writer.queue_put_many::(refs).unwrap(); + writer.commit().unwrap(); + } + + // Verify all entries exist + { + let reader = hot_kv.reader().unwrap(); + for i in 200u64..210 { + let header = reader.get_header(i).unwrap(); + assert!(header.is_some(), "Header {} should exist after batch write", i); + assert_eq!(header.unwrap().number, i); + } + } +} + +/// Test queue_clear clears all entries in a table. +pub fn test_queue_clear(hot_kv: &T) { + // Write some headers + { + let writer = hot_kv.writer().unwrap(); + for i in 300u64..310 { + let header = Header { number: i, gas_limit: 1_000_000, ..Default::default() }; + writer.put_header_inconsistent(&header).unwrap(); + } + writer.commit().unwrap(); + } + + // Verify entries exist + { + let reader = hot_kv.reader().unwrap(); + for i in 300u64..310 { + assert!(reader.get_header(i).unwrap().is_some()); + } + } + + // Clear the table + { + let writer = hot_kv.writer().unwrap(); + writer.queue_clear::().unwrap(); + writer.commit().unwrap(); + } + + // Verify all entries are gone + { + let reader = hot_kv.reader().unwrap(); + for i in 300u64..310 { + assert!( + reader.get_header(i).unwrap().is_none(), + "Header {} should be gone after clear", + i + ); + } + } +} + +/// Test that put-then-delete in the same transaction results in deletion. +pub fn test_put_then_delete_same_key(hot_kv: &T) { + let addr = address!("0xffffffffffffffffffffffffffffffffffff0001"); + let account = Account { nonce: 99, balance: U256::from(9999), bytecode_hash: None }; + + // In a single transaction: put then delete + { + let writer = hot_kv.writer().unwrap(); + writer.put_account(&addr, &account).unwrap(); + writer.queue_delete::(&addr).unwrap(); + writer.commit().unwrap(); + } + + // Account should not exist + { + let reader = hot_kv.reader().unwrap(); + let result = reader.get_account(&addr).unwrap(); + assert!(result.is_none(), "Put-then-delete should result in no entry"); + } +} + +/// Test that delete-then-put in the same transaction results in the put value. +pub fn test_delete_then_put_same_key(hot_kv: &T) { + let addr = address!("0xffffffffffffffffffffffffffffffffffff0002"); + let old_account = Account { nonce: 1, balance: U256::from(100), bytecode_hash: None }; + let new_account = Account { nonce: 2, balance: U256::from(200), bytecode_hash: None }; + + // First, write an account + { + let writer = hot_kv.writer().unwrap(); + writer.put_account(&addr, &old_account).unwrap(); + writer.commit().unwrap(); + } + + // In a single transaction: delete then put new value + { + let writer = hot_kv.writer().unwrap(); + writer.queue_delete::(&addr).unwrap(); + writer.put_account(&addr, &new_account).unwrap(); + writer.commit().unwrap(); + } + + // Should have the new value + { + let reader = hot_kv.reader().unwrap(); + let result = reader.get_account(&addr).unwrap(); + assert!(result.is_some(), "Delete-then-put should result in entry existing"); + let account = result.unwrap(); + assert_eq!(account.nonce, 2, "Should have the new nonce"); + assert_eq!(account.balance, U256::from(200), "Should have the new balance"); + } +} diff --git a/crates/hot/src/conformance/history.rs b/crates/hot/src/conformance/history.rs new file mode 100644 index 0000000..af0250c --- /dev/null +++ b/crates/hot/src/conformance/history.rs @@ -0,0 +1,456 @@ +//! History and change set tests for hot storage. + +use crate::{ + db::{HistoryRead, UnsafeDbWrite, UnsafeHistoryWrite}, + model::{HotKv, HotKvWrite}, + tables, +}; +use alloy::primitives::{U256, address}; +use signet_storage_types::{Account, BlockNumberList, ShardedKey}; + +/// Test update_history_indices_inconsistent for account history. +/// +/// This test verifies that: +/// 1. Account change sets are correctly indexed into account history +/// 2. Appending to existing history works correctly +/// 3. Old shards are deleted when appending +pub fn test_update_history_indices_account(hot_kv: &T) { + let addr1 = address!("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + let addr2 = address!("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + + // Phase 1: Write account change sets for blocks 1-3 + { + let writer = hot_kv.writer().unwrap(); + + // Block 1: addr1 changed + let pre_acc = Account::default(); + writer.write_account_prestate(1, addr1, &pre_acc).unwrap(); + + // Block 2: addr1 and addr2 changed + let acc1 = Account { nonce: 1, balance: U256::from(100), bytecode_hash: None }; + writer.write_account_prestate(2, addr1, &acc1).unwrap(); + writer.write_account_prestate(2, addr2, &pre_acc).unwrap(); + + // Block 3: addr2 changed + let acc2 = Account { nonce: 1, balance: U256::from(200), bytecode_hash: None }; + writer.write_account_prestate(3, addr2, &acc2).unwrap(); + + writer.commit().unwrap(); + } + + // Phase 2: Run update_history_indices_inconsistent for blocks 1-3 + { + let writer = hot_kv.writer().unwrap(); + writer.update_history_indices_inconsistent(1..=3).unwrap(); + writer.commit().unwrap(); + } + + // Phase 3: Verify account history was created correctly + { + let reader = hot_kv.reader().unwrap(); + + // addr1 should have history at blocks 1, 2 + let (_, history1) = + reader.last_account_history(addr1).unwrap().expect("addr1 should have history"); + let blocks1: Vec = history1.iter().collect(); + assert_eq!(blocks1, vec![1, 2], "addr1 history mismatch"); + + // addr2 should have history at blocks 2, 3 + let (_, history2) = + reader.last_account_history(addr2).unwrap().expect("addr2 should have history"); + let blocks2: Vec = history2.iter().collect(); + assert_eq!(blocks2, vec![2, 3], "addr2 history mismatch"); + } + + // Phase 4: Write more change sets for blocks 4-5 + { + let writer = hot_kv.writer().unwrap(); + + // Block 4: addr1 changed + let acc1 = Account { nonce: 2, balance: U256::from(300), bytecode_hash: None }; + writer.write_account_prestate(4, addr1, &acc1).unwrap(); + + // Block 5: addr1 changed again + let acc1_v2 = Account { nonce: 3, balance: U256::from(400), bytecode_hash: None }; + writer.write_account_prestate(5, addr1, &acc1_v2).unwrap(); + + writer.commit().unwrap(); + } + + // Phase 5: Run update_history_indices_inconsistent for blocks 4-5 + { + let writer = hot_kv.writer().unwrap(); + writer.update_history_indices_inconsistent(4..=5).unwrap(); + writer.commit().unwrap(); + } + + // Phase 6: Verify history was appended correctly + { + let reader = hot_kv.reader().unwrap(); + + // addr1 should now have history at blocks 1, 2, 4, 5 + let (_, history1) = + reader.last_account_history(addr1).unwrap().expect("addr1 should have history"); + let blocks1: Vec = history1.iter().collect(); + assert_eq!(blocks1, vec![1, 2, 4, 5], "addr1 history mismatch after append"); + + // addr2 should still have history at blocks 2, 3 (unchanged) + let (_, history2) = + reader.last_account_history(addr2).unwrap().expect("addr2 should have history"); + let blocks2: Vec = history2.iter().collect(); + assert_eq!(blocks2, vec![2, 3], "addr2 history should be unchanged"); + } +} + +/// Test update_history_indices_inconsistent for storage history. +/// +/// This test verifies that: +/// 1. Storage change sets are correctly indexed into storage history +/// 2. Appending to existing history works correctly +/// 3. Old shards are deleted when appending +/// 4. Different slots for the same address are tracked separately +pub fn test_update_history_indices_storage(hot_kv: &T) { + let addr1 = address!("0xcccccccccccccccccccccccccccccccccccccccc"); + let slot1 = U256::from(1); + let slot2 = U256::from(2); + + // Phase 1: Write storage change sets for blocks 1-3 + { + let writer = hot_kv.writer().unwrap(); + + // Block 1: addr1.slot1 changed + writer.write_storage_prestate(1, addr1, &slot1, &U256::ZERO).unwrap(); + + // Block 2: addr1.slot1 and addr1.slot2 changed + writer.write_storage_prestate(2, addr1, &slot1, &U256::from(100)).unwrap(); + writer.write_storage_prestate(2, addr1, &slot2, &U256::ZERO).unwrap(); + + // Block 3: addr1.slot2 changed + writer.write_storage_prestate(3, addr1, &slot2, &U256::from(200)).unwrap(); + + writer.commit().unwrap(); + } + + // Phase 2: Run update_history_indices_inconsistent for blocks 1-3 + { + let writer = hot_kv.writer().unwrap(); + writer.update_history_indices_inconsistent(1..=3).unwrap(); + writer.commit().unwrap(); + } + + // Phase 3: Verify storage history was created correctly + { + let reader = hot_kv.reader().unwrap(); + + // addr1.slot1 should have history at blocks 1, 2 + let (_, history1) = reader + .last_storage_history(&addr1, &slot1) + .unwrap() + .expect("addr1.slot1 should have history"); + let blocks1: Vec = history1.iter().collect(); + assert_eq!(blocks1, vec![1, 2], "addr1.slot1 history mismatch"); + + // addr1.slot2 should have history at blocks 2, 3 + let (_, history2) = reader + .last_storage_history(&addr1, &slot2) + .unwrap() + .expect("addr1.slot2 should have history"); + let blocks2: Vec = history2.iter().collect(); + assert_eq!(blocks2, vec![2, 3], "addr1.slot2 history mismatch"); + } + + // Phase 4: Write more change sets for blocks 4-5 + { + let writer = hot_kv.writer().unwrap(); + + // Block 4: addr1.slot1 changed + writer.write_storage_prestate(4, addr1, &slot1, &U256::from(300)).unwrap(); + + // Block 5: addr1.slot1 changed again + writer.write_storage_prestate(5, addr1, &slot1, &U256::from(400)).unwrap(); + + writer.commit().unwrap(); + } + + // Phase 5: Run update_history_indices_inconsistent for blocks 4-5 + { + let writer = hot_kv.writer().unwrap(); + writer.update_history_indices_inconsistent(4..=5).unwrap(); + writer.commit().unwrap(); + } + + // Phase 6: Verify history was appended correctly + { + let reader = hot_kv.reader().unwrap(); + + // addr1.slot1 should now have history at blocks 1, 2, 4, 5 + let (_, history1) = reader + .last_storage_history(&addr1, &slot1) + .unwrap() + .expect("addr1.slot1 should have history"); + let blocks1: Vec = history1.iter().collect(); + assert_eq!(blocks1, vec![1, 2, 4, 5], "addr1.slot1 history mismatch after append"); + + // addr1.slot2 should still have history at blocks 2, 3 (unchanged) + let (_, history2) = reader + .last_storage_history(&addr1, &slot2) + .unwrap() + .expect("addr1.slot2 should have history"); + let blocks2: Vec = history2.iter().collect(); + assert_eq!(blocks2, vec![2, 3], "addr1.slot2 history should be unchanged"); + } +} + +/// Test that appending to history correctly removes old entries at same k1,k2. +/// +/// This test specifically verifies that when we append new indices to an existing +/// shard, the old shard is properly deleted so we don't end up with duplicate data. +pub fn test_history_append_removes_old_entries(hot_kv: &T) { + let addr = address!("0xdddddddddddddddddddddddddddddddddddddddd"); + + // Phase 1: Manually write account history + { + let writer = hot_kv.writer().unwrap(); + let initial_history = BlockNumberList::new([10, 20, 30]).unwrap(); + writer.write_account_history(&addr, u64::MAX, &initial_history).unwrap(); + writer.commit().unwrap(); + } + + // Verify initial state + { + let reader = hot_kv.reader().unwrap(); + let (key, history) = + reader.last_account_history(addr).unwrap().expect("should have history"); + assert_eq!(key, u64::MAX); + let blocks: Vec = history.iter().collect(); + assert_eq!(blocks, vec![10, 20, 30]); + } + + // Phase 2: Write account change set for block 40 + { + let writer = hot_kv.writer().unwrap(); + let acc = Account { nonce: 1, balance: U256::from(100), bytecode_hash: None }; + writer.write_account_prestate(40, addr, &acc).unwrap(); + writer.commit().unwrap(); + } + + // Phase 3: Run update_history_indices_inconsistent + { + let writer = hot_kv.writer().unwrap(); + writer.update_history_indices_inconsistent(40..=40).unwrap(); + writer.commit().unwrap(); + } + + // Phase 4: Verify history was correctly appended + { + let reader = hot_kv.reader().unwrap(); + let (key, history) = + reader.last_account_history(addr).unwrap().expect("should have history"); + assert_eq!(key, u64::MAX, "key should still be u64::MAX"); + let blocks: Vec = history.iter().collect(); + assert_eq!(blocks, vec![10, 20, 30, 40], "history should include appended block"); + } +} + +/// Test deleting dual-keyed account history entries. +/// +/// This test verifies that: +/// 1. Writing dual-keyed entries works correctly +/// 2. Deleting specific dual-keyed entries removes only that entry +/// 3. Other entries for the same k1 remain intact +/// 4. Traversal after deletion shows the entry is gone +pub fn test_delete_dual_account_history(hot_kv: &T) { + let addr1 = address!("0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"); + let addr2 = address!("0xffffffffffffffffffffffffffffffffffffffff"); + + // Phase 1: Write account history entries for multiple addresses + { + let writer = hot_kv.writer().unwrap(); + + // Write history for addr1 at two different shard keys + let history1_a = BlockNumberList::new([1, 2, 3]).unwrap(); + let history1_b = BlockNumberList::new([4, 5, 6]).unwrap(); + writer.write_account_history(&addr1, 100, &history1_a).unwrap(); + writer.write_account_history(&addr1, u64::MAX, &history1_b).unwrap(); + + // Write history for addr2 + let history2 = BlockNumberList::new([10, 20, 30]).unwrap(); + writer.write_account_history(&addr2, u64::MAX, &history2).unwrap(); + + writer.commit().unwrap(); + } + + // Phase 2: Verify all entries exist + { + let reader = hot_kv.reader().unwrap(); + + // Check addr1 entries + let hist1_a = reader.get_account_history(&addr1, 100).unwrap(); + assert!(hist1_a.is_some(), "addr1 shard 100 should exist"); + assert_eq!(hist1_a.unwrap().iter().collect::>(), vec![1, 2, 3]); + + let hist1_b = reader.get_account_history(&addr1, u64::MAX).unwrap(); + assert!(hist1_b.is_some(), "addr1 shard u64::MAX should exist"); + assert_eq!(hist1_b.unwrap().iter().collect::>(), vec![4, 5, 6]); + + // Check addr2 entry + let hist2 = reader.get_account_history(&addr2, u64::MAX).unwrap(); + assert!(hist2.is_some(), "addr2 should exist"); + assert_eq!(hist2.unwrap().iter().collect::>(), vec![10, 20, 30]); + } + + // Phase 3: Delete addr1's u64::MAX entry + { + let writer = hot_kv.writer().unwrap(); + writer.queue_delete_dual::(&addr1, &u64::MAX).unwrap(); + writer.commit().unwrap(); + } + + // Phase 4: Verify only the deleted entry is gone + { + let reader = hot_kv.reader().unwrap(); + + // addr1 shard 100 should still exist + let hist1_a = reader.get_account_history(&addr1, 100).unwrap(); + assert!(hist1_a.is_some(), "addr1 shard 100 should still exist after delete"); + assert_eq!(hist1_a.unwrap().iter().collect::>(), vec![1, 2, 3]); + + // addr1 shard u64::MAX should be gone + let hist1_b = reader.get_account_history(&addr1, u64::MAX).unwrap(); + assert!(hist1_b.is_none(), "addr1 shard u64::MAX should be deleted"); + + // addr2 should be unaffected + let hist2 = reader.get_account_history(&addr2, u64::MAX).unwrap(); + assert!(hist2.is_some(), "addr2 should be unaffected by delete"); + assert_eq!(hist2.unwrap().iter().collect::>(), vec![10, 20, 30]); + + // Verify last_account_history now returns shard 100 for addr1 + let (key, _) = + reader.last_account_history(addr1).unwrap().expect("addr1 should still have history"); + assert_eq!(key, 100, "last shard for addr1 should now be 100"); + } +} + +/// Test deleting dual-keyed storage history entries. +/// +/// This test verifies that: +/// 1. Writing storage history entries works correctly +/// 2. Deleting specific (address, slot, shard) entries removes only that entry +/// 3. Other slots for the same address remain intact +/// 4. Traversal after deletion shows the entry is gone +pub fn test_delete_dual_storage_history(hot_kv: &T) { + let addr = address!("0x1111111111111111111111111111111111111111"); + let slot1 = U256::from(100); + let slot2 = U256::from(200); + + // Phase 1: Write storage history entries for multiple slots + { + let writer = hot_kv.writer().unwrap(); + + // Write history for slot1 + let history1 = BlockNumberList::new([1, 2, 3]).unwrap(); + writer.write_storage_history(&addr, slot1, u64::MAX, &history1).unwrap(); + + // Write history for slot2 + let history2 = BlockNumberList::new([10, 20, 30]).unwrap(); + writer.write_storage_history(&addr, slot2, u64::MAX, &history2).unwrap(); + + writer.commit().unwrap(); + } + + // Phase 2: Verify both entries exist + { + let reader = hot_kv.reader().unwrap(); + + let hist1 = reader.get_storage_history(&addr, slot1, u64::MAX).unwrap(); + assert!(hist1.is_some(), "slot1 should exist"); + assert_eq!(hist1.unwrap().iter().collect::>(), vec![1, 2, 3]); + + let hist2 = reader.get_storage_history(&addr, slot2, u64::MAX).unwrap(); + assert!(hist2.is_some(), "slot2 should exist"); + assert_eq!(hist2.unwrap().iter().collect::>(), vec![10, 20, 30]); + } + + // Phase 3: Delete slot1's entry + { + let writer = hot_kv.writer().unwrap(); + let key_to_delete = ShardedKey::new(slot1, u64::MAX); + writer.queue_delete_dual::(&addr, &key_to_delete).unwrap(); + writer.commit().unwrap(); + } + + // Phase 4: Verify only slot1 is gone + { + let reader = hot_kv.reader().unwrap(); + + // slot1 should be gone + let hist1 = reader.get_storage_history(&addr, slot1, u64::MAX).unwrap(); + assert!(hist1.is_none(), "slot1 should be deleted"); + + // slot2 should be unaffected + let hist2 = reader.get_storage_history(&addr, slot2, u64::MAX).unwrap(); + assert!(hist2.is_some(), "slot2 should be unaffected"); + assert_eq!(hist2.unwrap().iter().collect::>(), vec![10, 20, 30]); + + // last_storage_history for slot1 should return None + let last1 = reader.last_storage_history(&addr, &slot1).unwrap(); + assert!(last1.is_none(), "last_storage_history for slot1 should return None"); + + // last_storage_history for slot2 should still work + let last2 = reader.last_storage_history(&addr, &slot2).unwrap(); + assert!(last2.is_some(), "last_storage_history for slot2 should still work"); + } +} + +/// Test deleting and re-adding dual-keyed entries. +/// +/// This test verifies that after deleting an entry, we can write a new entry +/// with the same key and it works correctly. +pub fn test_delete_and_rewrite_dual(hot_kv: &T) { + let addr = address!("0x2222222222222222222222222222222222222222"); + + // Phase 1: Write initial entry + { + let writer = hot_kv.writer().unwrap(); + let history = BlockNumberList::new([1, 2, 3]).unwrap(); + writer.write_account_history(&addr, u64::MAX, &history).unwrap(); + writer.commit().unwrap(); + } + + // Verify initial state + { + let reader = hot_kv.reader().unwrap(); + let hist = reader.get_account_history(&addr, u64::MAX).unwrap(); + assert_eq!(hist.unwrap().iter().collect::>(), vec![1, 2, 3]); + } + + // Phase 2: Delete the entry + { + let writer = hot_kv.writer().unwrap(); + writer.queue_delete_dual::(&addr, &u64::MAX).unwrap(); + writer.commit().unwrap(); + } + + // Verify deleted + { + let reader = hot_kv.reader().unwrap(); + let hist = reader.get_account_history(&addr, u64::MAX).unwrap(); + assert!(hist.is_none(), "entry should be deleted"); + } + + // Phase 3: Write new entry with same key but different value + { + let writer = hot_kv.writer().unwrap(); + let new_history = BlockNumberList::new([100, 200, 300]).unwrap(); + writer.write_account_history(&addr, u64::MAX, &new_history).unwrap(); + writer.commit().unwrap(); + } + + // Verify new value + { + let reader = hot_kv.reader().unwrap(); + let hist = reader.get_account_history(&addr, u64::MAX).unwrap(); + assert!(hist.is_some(), "new entry should exist"); + assert_eq!(hist.unwrap().iter().collect::>(), vec![100, 200, 300]); + } +} diff --git a/crates/hot/src/conformance/mod.rs b/crates/hot/src/conformance/mod.rs new file mode 100644 index 0000000..7f22728 --- /dev/null +++ b/crates/hot/src/conformance/mod.rs @@ -0,0 +1,42 @@ +#![allow(dead_code)] + +mod cursor; +mod edge_cases; +mod history; +mod range; +mod roundtrip; +mod unwind; + +pub use cursor::*; +pub use edge_cases::*; +pub use history::*; +pub use range::*; +pub use roundtrip::*; +pub use unwind::*; + +use crate::model::HotKv; +use alloy::{ + consensus::{Header, Sealable}, + primitives::B256, +}; +use signet_storage_types::SealedHeader; + +/// Run all conformance tests against a [`HotKv`] implementation. +pub fn conformance(hot_kv: &T) { + test_header_roundtrip(hot_kv); + test_account_roundtrip(hot_kv); + test_storage_roundtrip(hot_kv); + test_storage_update_replaces(hot_kv); + test_bytecode_roundtrip(hot_kv); + test_account_history(hot_kv); + test_storage_history(hot_kv); + test_account_changes(hot_kv); + test_storage_changes(hot_kv); + test_missing_reads(hot_kv); +} + +/// Helper to create a sealed header at a given height with specific parent +pub(crate) fn make_header(number: u64, parent_hash: B256) -> SealedHeader { + let header = Header { number, parent_hash, gas_limit: 1_000_000, ..Default::default() }; + header.seal_slow() +} diff --git a/crates/hot/src/conformance/range.rs b/crates/hot/src/conformance/range.rs new file mode 100644 index 0000000..5f6f81d --- /dev/null +++ b/crates/hot/src/conformance/range.rs @@ -0,0 +1,492 @@ +//! Clear/take range operations for single and dual-keyed tables. + +use crate::{ + db::{HistoryRead, HotDbRead, UnsafeDbWrite, UnsafeHistoryWrite}, + model::{HotKv, HotKvWrite}, + tables, +}; +use alloy::{ + consensus::Header, + primitives::{U256, address}, +}; +use signet_storage_types::BlockNumberList; +use trevm::revm::database::states::PlainStorageChangeset; + +/// Test clear_range on a single-keyed table. +/// +/// This test verifies that: +/// 1. Keys within the range are deleted +/// 2. Keys outside the range remain intact +/// 3. Edge cases like adjacent keys and boundary conditions work correctly +pub fn test_clear_range(hot_kv: &T) { + // Phase 1: Write 15 headers with block numbers 0-14 + { + let writer = hot_kv.writer().unwrap(); + for i in 0u64..15 { + let header = Header { number: i, gas_limit: 1_000_000, ..Default::default() }; + writer.put_header_inconsistent(&header).unwrap(); + } + writer.commit().unwrap(); + } + + // Verify all headers exist + { + let reader = hot_kv.reader().unwrap(); + for i in 0u64..15 { + assert!(reader.get_header(i).unwrap().is_some(), "header {} should exist", i); + } + } + + // Phase 2: Clear range 5..=9 (middle range) + { + let writer = hot_kv.writer().unwrap(); + writer.traverse_mut::().unwrap().delete_range_inclusive(5..=9).unwrap(); + writer.commit().unwrap(); + } + + // Verify: 0-4 and 10-14 should exist, 5-9 should be gone + { + let reader = hot_kv.reader().unwrap(); + + // Keys before range should exist + for i in 0u64..5 { + assert!(reader.get_header(i).unwrap().is_some(), "header {} should still exist", i); + } + + // Keys in range should be deleted + for i in 5u64..10 { + assert!(reader.get_header(i).unwrap().is_none(), "header {} should be deleted", i); + } + + // Keys after range should exist + for i in 10u64..15 { + assert!(reader.get_header(i).unwrap().is_some(), "header {} should still exist", i); + } + } + + // Phase 3: Test corner case - clear adjacent keys at the boundary + { + let writer = hot_kv.writer().unwrap(); + // Clear keys 3 and 4 (adjacent to the already cleared range) + writer.traverse_mut::().unwrap().delete_range_inclusive(3..=4).unwrap(); + writer.commit().unwrap(); + } + + // Verify: 0-2 and 10-14 should exist, 3-9 should be gone + { + let reader = hot_kv.reader().unwrap(); + + // Keys 0-2 should exist + for i in 0u64..3 { + assert!(reader.get_header(i).unwrap().is_some(), "header {} should still exist", i); + } + + // Keys 3-9 should all be deleted now + for i in 3u64..10 { + assert!(reader.get_header(i).unwrap().is_none(), "header {} should be deleted", i); + } + + // Keys 10-14 should exist + for i in 10u64..15 { + assert!(reader.get_header(i).unwrap().is_some(), "header {} should still exist", i); + } + } + + // Phase 4: Test clearing a range that includes the first key + { + let writer = hot_kv.writer().unwrap(); + writer.traverse_mut::().unwrap().delete_range_inclusive(0..=1).unwrap(); + writer.commit().unwrap(); + } + + { + let reader = hot_kv.reader().unwrap(); + assert!(reader.get_header(0).unwrap().is_none(), "header 0 should be deleted"); + assert!(reader.get_header(1).unwrap().is_none(), "header 1 should be deleted"); + assert!(reader.get_header(2).unwrap().is_some(), "header 2 should still exist"); + } + + // Phase 5: Test clearing a range that includes the last key + { + let writer = hot_kv.writer().unwrap(); + writer.traverse_mut::().unwrap().delete_range_inclusive(13..=14).unwrap(); + writer.commit().unwrap(); + } + + { + let reader = hot_kv.reader().unwrap(); + assert!(reader.get_header(12).unwrap().is_some(), "header 12 should still exist"); + assert!(reader.get_header(13).unwrap().is_none(), "header 13 should be deleted"); + assert!(reader.get_header(14).unwrap().is_none(), "header 14 should be deleted"); + } + + // Phase 6: Test clearing a single key + { + let writer = hot_kv.writer().unwrap(); + writer.traverse_mut::().unwrap().delete_range_inclusive(11..=11).unwrap(); + writer.commit().unwrap(); + } + + { + let reader = hot_kv.reader().unwrap(); + assert!(reader.get_header(10).unwrap().is_some(), "header 10 should still exist"); + assert!(reader.get_header(11).unwrap().is_none(), "header 11 should be deleted"); + assert!(reader.get_header(12).unwrap().is_some(), "header 12 should still exist"); + } + + // Phase 7: Test clearing a range where nothing exists (should be no-op) + { + let writer = hot_kv.writer().unwrap(); + writer + .traverse_mut::() + .unwrap() + .delete_range_inclusive(100..=200) + .unwrap(); + writer.commit().unwrap(); + } + + // Verify remaining keys are still intact + { + let reader = hot_kv.reader().unwrap(); + assert!(reader.get_header(2).unwrap().is_some(), "header 2 should still exist"); + assert!(reader.get_header(10).unwrap().is_some(), "header 10 should still exist"); + assert!(reader.get_header(12).unwrap().is_some(), "header 12 should still exist"); + } +} + +/// Test take_range on a single-keyed table. +/// +/// Similar to clear_range but also returns the removed keys. +pub fn test_take_range(hot_kv: &T) { + let headers = (0..10u64) + .map(|i| Header { number: i, gas_limit: 1_000_000, ..Default::default() }) + .collect::>(); + + // Phase 1: Write 10 headers with block numbers 0-9 + { + let writer = hot_kv.writer().unwrap(); + for header in headers.iter() { + writer.put_header_inconsistent(header).unwrap(); + } + writer.commit().unwrap(); + } + + // Phase 2: Take range 3..=6 and verify returned keys + { + let writer = hot_kv.writer().unwrap(); + let removed = writer.traverse_mut::().unwrap().take_range(3..=6).unwrap(); + writer.commit().unwrap(); + + // Should return keys 3, 4, 5, 6 in order + assert_eq!(removed.len(), 4); + + for i in 0..4 { + assert_eq!(removed[i].0, (i as u64) + 3); + assert_eq!(&removed[i].1, &headers[i + 3]); + } + } + + // Verify the keys are actually removed + { + let reader = hot_kv.reader().unwrap(); + for i in 0u64..3 { + assert!(reader.get_header(i).unwrap().is_some(), "header {} should exist", i); + } + for i in 3u64..7 { + assert!(reader.get_header(i).unwrap().is_none(), "header {} should be gone", i); + } + for i in 7u64..10 { + assert!(reader.get_header(i).unwrap().is_some(), "header {} should exist", i); + } + } + + // Phase 3: Take empty range (nothing to remove) + { + let writer = hot_kv.writer().unwrap(); + let removed = + writer.traverse_mut::().unwrap().take_range(100..=200).unwrap(); + writer.commit().unwrap(); + + assert!(removed.is_empty(), "should return empty vec for non-existent range"); + } + + // Phase 4: Take single key + { + let writer = hot_kv.writer().unwrap(); + let removed = writer.traverse_mut::().unwrap().take_range(8..=8).unwrap(); + writer.commit().unwrap(); + + assert_eq!(removed.len(), 1); + assert_eq!(removed[0].0, 8); + assert_eq!(&removed[0].1, &headers[8]); + } + + { + let reader = hot_kv.reader().unwrap(); + assert!(reader.get_header(7).unwrap().is_some()); + assert!(reader.get_header(8).unwrap().is_none()); + assert!(reader.get_header(9).unwrap().is_some()); + } +} + +/// Test clear_range_dual on a dual-keyed table. +/// +/// This test verifies that: +/// 1. All k2 entries for k1 values within the range are deleted +/// 2. k1 values outside the range remain intact +/// 3. Edge cases work correctly +pub fn test_clear_range_dual(hot_kv: &T) { + let addr1 = address!("0x1000000000000000000000000000000000000001"); + let addr2 = address!("0x2000000000000000000000000000000000000002"); + let addr3 = address!("0x3000000000000000000000000000000000000003"); + let addr4 = address!("0x4000000000000000000000000000000000000004"); + let addr5 = address!("0x5000000000000000000000000000000000000005"); + + // Phase 1: Write account history entries for multiple addresses with multiple shards + { + let writer = hot_kv.writer().unwrap(); + + // addr1: two shards + let history1_a = BlockNumberList::new([1, 2, 3]).unwrap(); + let history1_b = BlockNumberList::new([4, 5, 6]).unwrap(); + writer.write_account_history(&addr1, 100, &history1_a).unwrap(); + writer.write_account_history(&addr1, u64::MAX, &history1_b).unwrap(); + + // addr2: one shard + let history2 = BlockNumberList::new([10, 20]).unwrap(); + writer.write_account_history(&addr2, u64::MAX, &history2).unwrap(); + + // addr3: one shard + let history3 = BlockNumberList::new([30, 40]).unwrap(); + writer.write_account_history(&addr3, u64::MAX, &history3).unwrap(); + + // addr4: two shards + let history4_a = BlockNumberList::new([50, 60]).unwrap(); + let history4_b = BlockNumberList::new([70, 80]).unwrap(); + writer.write_account_history(&addr4, 200, &history4_a).unwrap(); + writer.write_account_history(&addr4, u64::MAX, &history4_b).unwrap(); + + // addr5: one shard + let history5 = BlockNumberList::new([90, 100]).unwrap(); + writer.write_account_history(&addr5, u64::MAX, &history5).unwrap(); + + writer.commit().unwrap(); + } + + // Verify all entries exist + { + let reader = hot_kv.reader().unwrap(); + assert!(reader.get_account_history(&addr1, 100).unwrap().is_some()); + assert!(reader.get_account_history(&addr1, u64::MAX).unwrap().is_some()); + assert!(reader.get_account_history(&addr2, u64::MAX).unwrap().is_some()); + assert!(reader.get_account_history(&addr3, u64::MAX).unwrap().is_some()); + assert!(reader.get_account_history(&addr4, 200).unwrap().is_some()); + assert!(reader.get_account_history(&addr4, u64::MAX).unwrap().is_some()); + assert!(reader.get_account_history(&addr5, u64::MAX).unwrap().is_some()); + } + + // Phase 2: Clear range addr2..=addr3 (middle range) + { + let writer = hot_kv.writer().unwrap(); + writer + .traverse_dual_mut::() + .unwrap() + .delete_range((addr2, 0)..=(addr3, u64::MAX)) + .unwrap(); + writer.commit().unwrap(); + } + + // Verify: addr1 and addr4, addr5 should exist, addr2 and addr3 should be gone + { + let reader = hot_kv.reader().unwrap(); + + // addr1 entries should still exist + assert!( + reader.get_account_history(&addr1, 100).unwrap().is_some(), + "addr1 shard 100 should exist" + ); + assert!( + reader.get_account_history(&addr1, u64::MAX).unwrap().is_some(), + "addr1 shard max should exist" + ); + + // addr2 and addr3 should be deleted + assert!( + reader.get_account_history(&addr2, u64::MAX).unwrap().is_none(), + "addr2 should be deleted" + ); + assert!( + reader.get_account_history(&addr3, u64::MAX).unwrap().is_none(), + "addr3 should be deleted" + ); + + // addr4 and addr5 entries should still exist + assert!( + reader.get_account_history(&addr4, 200).unwrap().is_some(), + "addr4 shard 200 should exist" + ); + assert!( + reader.get_account_history(&addr4, u64::MAX).unwrap().is_some(), + "addr4 shard max should exist" + ); + assert!( + reader.get_account_history(&addr5, u64::MAX).unwrap().is_some(), + "addr5 should exist" + ); + } +} + +/// Test take_range_dual on a dual-keyed table. +/// +/// Similar to clear_range_dual but also returns the removed (k1, k2) pairs. +pub fn test_take_range_dual(hot_kv: &T) { + let addr1 = address!("0xa000000000000000000000000000000000000001"); + let addr2 = address!("0xb000000000000000000000000000000000000002"); + let addr3 = address!("0xc000000000000000000000000000000000000003"); + + // Phase 1: Write account history entries + { + let writer = hot_kv.writer().unwrap(); + + // addr1: two shards + let history1_a = BlockNumberList::new([1, 2]).unwrap(); + let history1_b = BlockNumberList::new([3, 4]).unwrap(); + writer.write_account_history(&addr1, 50, &history1_a).unwrap(); + writer.write_account_history(&addr1, u64::MAX, &history1_b).unwrap(); + + // addr2: one shard + let history2 = BlockNumberList::new([10, 20]).unwrap(); + writer.write_account_history(&addr2, u64::MAX, &history2).unwrap(); + + // addr3: one shard + let history3 = BlockNumberList::new([30, 40]).unwrap(); + writer.write_account_history(&addr3, u64::MAX, &history3).unwrap(); + + writer.commit().unwrap(); + } + + // Phase 2: Take range addr1..=addr2 and verify returned pairs + { + let writer = hot_kv.writer().unwrap(); + let removed = writer + .traverse_dual_mut::() + .unwrap() + .take_range((addr1, 0)..=(addr2, u64::MAX)) + .unwrap(); + writer.commit().unwrap(); + + // Should return (addr1, 50), (addr1, max), (addr2, max) + assert_eq!(removed.len(), 3, "should have removed 3 entries"); + assert_eq!(removed[0].0, addr1); + assert_eq!(removed[0].1, 50); + assert_eq!(removed[1].0, addr1); + assert_eq!(removed[1].1, u64::MAX); + assert_eq!(removed[2].0, addr2); + assert_eq!(removed[2].1, u64::MAX); + } + + // Verify only addr3 remains + { + let reader = hot_kv.reader().unwrap(); + assert!(reader.get_account_history(&addr1, 50).unwrap().is_none()); + assert!(reader.get_account_history(&addr1, u64::MAX).unwrap().is_none()); + assert!(reader.get_account_history(&addr2, u64::MAX).unwrap().is_none()); + assert!(reader.get_account_history(&addr3, u64::MAX).unwrap().is_some()); + } + + // Phase 3: Take empty range + { + let writer = hot_kv.writer().unwrap(); + let removed = writer + .traverse_dual_mut::() + .unwrap() + .take_range( + (address!("0xf000000000000000000000000000000000000000"), 0) + ..=(address!("0xff00000000000000000000000000000000000000"), u64::MAX), + ) + .unwrap(); + writer.commit().unwrap(); + + assert!(removed.is_empty(), "should return empty vec for non-existent range"); + } +} + +/// Test that storage wipe via changeset uses `clear_k1`. +/// +/// This test verifies that: +/// 1. Multiple storage slots can be written for an address +/// 2. `write_changed_storage` with `wipe_storage: true` clears all slots +/// 3. After wipe, all slots return None +pub fn test_write_changed_storage_wipe(hot_kv: &T) { + let addr = address!("0x1111111111111111111111111111111111111111"); + + // Setup: write multiple storage slots for an address + { + let writer = hot_kv.writer().unwrap(); + for i in 0..10u64 { + writer.put_storage(&addr, &U256::from(i), &U256::from(i * 100)).unwrap(); + } + writer.commit().unwrap(); + } + + // Verify storage exists + { + let reader = hot_kv.reader().unwrap(); + for i in 0..10u64 { + let val = reader.get_storage(&addr, &U256::from(i)).unwrap(); + assert_eq!(val, Some(U256::from(i * 100))); + } + } + + // Apply wipe via write_changed_storage + { + let writer = hot_kv.writer().unwrap(); + let changeset = + PlainStorageChangeset { address: addr, wipe_storage: true, storage: vec![] }; + writer.write_changed_storage(&changeset).unwrap(); + writer.commit().unwrap(); + } + + // Verify all storage is cleared + { + let reader = hot_kv.reader().unwrap(); + for i in 0..10u64 { + let val = reader.get_storage(&addr, &U256::from(i)).unwrap(); + assert!(val.is_none(), "slot {i} should be cleared"); + } + } +} + +/// Test the `clear_k1_for` trait method. +/// +/// This test verifies that: +/// 1. Storage slots can be written for an address +/// 2. `clear_k1_for` removes all k2 entries for the given k1 +/// 3. After clearing, all slots return None +pub fn test_clear_k1_for(hot_kv: &T) { + let addr = address!("0x2222222222222222222222222222222222222222"); + + // Setup: write storage slots + { + let writer = hot_kv.writer().unwrap(); + for i in 0..5u64 { + writer.put_storage(&addr, &U256::from(i), &U256::from(i + 1)).unwrap(); + } + writer.commit().unwrap(); + } + + // Clear using clear_k1_for + { + let writer = hot_kv.writer().unwrap(); + writer.clear_k1_for::(&addr).unwrap(); + writer.commit().unwrap(); + } + + // Verify cleared + { + let reader = hot_kv.reader().unwrap(); + for i in 0..5u64 { + assert!(reader.get_storage(&addr, &U256::from(i)).unwrap().is_none()); + } + } +} diff --git a/crates/hot/src/conformance/roundtrip.rs b/crates/hot/src/conformance/roundtrip.rs new file mode 100644 index 0000000..cba2c76 --- /dev/null +++ b/crates/hot/src/conformance/roundtrip.rs @@ -0,0 +1,291 @@ +//! Basic CRUD roundtrip tests for hot storage. + +use crate::{ + db::{HistoryRead, HotDbRead, UnsafeDbWrite, UnsafeHistoryWrite}, + model::{HotKv, HotKvRead}, + tables, +}; +use alloy::{ + consensus::Header, + primitives::{B256, Bytes, U256, address, b256}, +}; +use signet_storage_types::{Account, BlockNumberList, SealedHeader}; +use trevm::revm::bytecode::Bytecode; + +/// Test writing and reading headers via HotDbWrite/HotDbRead +pub fn test_header_roundtrip(hot_kv: &T) { + let header = Header { number: 42, gas_limit: 1_000_000, ..Default::default() }; + let sealed = SealedHeader::new(header.clone()); + let hash = sealed.hash(); + + // Write header + { + let writer = hot_kv.writer().unwrap(); + writer.put_header(&sealed).unwrap(); + writer.commit().unwrap(); + } + + // Read header by number + { + let reader = hot_kv.reader().unwrap(); + let read_header = reader.get_header(42).unwrap(); + assert!(read_header.is_some()); + assert_eq!(read_header.unwrap().number, 42); + } + + // Read header number by hash + { + let reader = hot_kv.reader().unwrap(); + let read_number = reader.get_header_number(&hash).unwrap(); + assert!(read_number.is_some()); + assert_eq!(read_number.unwrap(), 42); + } + + // Read header by hash + { + let reader = hot_kv.reader().unwrap(); + let read_header = reader.header_by_hash(&hash).unwrap(); + assert!(read_header.is_some()); + assert_eq!(read_header.unwrap().number, 42); + } +} + +/// Test writing and reading accounts via HotDbWrite/HotDbRead +pub fn test_account_roundtrip(hot_kv: &T) { + let addr = address!("0x1234567890123456789012345678901234567890"); + let account = Account { nonce: 5, balance: U256::from(1000), bytecode_hash: Some(B256::ZERO) }; + + // Write account + { + let writer = hot_kv.writer().unwrap(); + writer.put_account(&addr, &account).unwrap(); + writer.commit().unwrap(); + } + + // Read account + { + let reader = hot_kv.reader().unwrap(); + let read_account = reader.get_account(&addr).unwrap(); + assert!(read_account.is_some()); + let read_account = read_account.unwrap(); + assert_eq!(read_account.nonce, 5); + assert_eq!(read_account.balance, U256::from(1000)); + } +} + +/// Test writing and reading storage via HotDbWrite/HotDbRead +pub fn test_storage_roundtrip(hot_kv: &T) { + let addr = address!("0xabcdef0123456789abcdef0123456789abcdef01"); + let slot = U256::from(42); + let value = U256::from(999); + + // Write storage + { + let writer = hot_kv.writer().unwrap(); + writer.put_storage(&addr, &slot, &value).unwrap(); + writer.commit().unwrap(); + } + + // Read storage + { + let reader = hot_kv.reader().unwrap(); + let read_value = reader.get_storage(&addr, &slot).unwrap(); + assert!(read_value.is_some()); + assert_eq!(read_value.unwrap(), U256::from(999)); + } +} + +/// Test that updating a storage slot replaces the value (no duplicates). +/// +/// This test verifies that DUPSORT tables properly handle updates by deleting +/// existing entries before inserting new ones. +pub fn test_storage_update_replaces(hot_kv: &T) { + let addr = address!("0x2222222222222222222222222222222222222222"); + let slot = U256::from(1); + + // Write initial value + { + let writer = hot_kv.writer().unwrap(); + writer.put_storage(&addr, &slot, &U256::from(10)).unwrap(); + writer.commit().unwrap(); + } + + // Update to new value + { + let writer = hot_kv.writer().unwrap(); + writer.put_storage(&addr, &slot, &U256::from(20)).unwrap(); + writer.commit().unwrap(); + } + + // Verify: only ONE entry exists with the NEW value + let reader = hot_kv.reader().unwrap(); + let mut cursor = reader.traverse_dual::().unwrap(); + + let mut count = 0; + let mut found_value = None; + while let Some((k, k2, v)) = cursor.read_next().unwrap() { + if k == addr && k2 == slot { + count += 1; + found_value = Some(v); + } + } + + assert_eq!(count, 1, "Should have exactly one entry, not duplicates"); + assert_eq!(found_value, Some(U256::from(20)), "Value should be 20"); +} + +/// Test writing and reading bytecode via HotDbWrite/HotDbRead +pub fn test_bytecode_roundtrip(hot_kv: &T) { + let code = Bytes::from_static(&[0x60, 0x00, 0x60, 0x00, 0xf3]); // Simple EVM bytecode + let bytecode = Bytecode::new_raw(code); + let code_hash = bytecode.hash_slow(); + + // Write bytecode + { + let writer = hot_kv.writer().unwrap(); + writer.put_bytecode(&code_hash, &bytecode).unwrap(); + writer.commit().unwrap(); + } + + // Read bytecode + { + let reader = hot_kv.reader().unwrap(); + let read_bytecode = reader.get_bytecode(&code_hash).unwrap(); + assert!(read_bytecode.is_some()); + } +} + +/// Test account history via HotHistoryWrite/HotHistoryRead +pub fn test_account_history(hot_kv: &T) { + let addr = address!("0x1111111111111111111111111111111111111111"); + let touched_blocks = BlockNumberList::new([10, 20, 30]).unwrap(); + let latest_height = 100u64; + + // Write account history + { + let writer = hot_kv.writer().unwrap(); + writer.write_account_history(&addr, latest_height, &touched_blocks).unwrap(); + writer.commit().unwrap(); + } + + // Read account history + { + let reader = hot_kv.reader().unwrap(); + let read_history = reader.get_account_history(&addr, latest_height).unwrap(); + assert!(read_history.is_some()); + let history = read_history.unwrap(); + assert_eq!(history.iter().collect::>(), vec![10, 20, 30]); + } +} + +/// Test storage history via HotHistoryWrite/HotHistoryRead +pub fn test_storage_history(hot_kv: &T) { + let addr = address!("0x2222222222222222222222222222222222222222"); + let slot = U256::from(42); + let touched_blocks = BlockNumberList::new([5, 15, 25]).unwrap(); + let highest_block = 50u64; + + // Write storage history + { + let writer = hot_kv.writer().unwrap(); + writer.write_storage_history(&addr, slot, highest_block, &touched_blocks).unwrap(); + writer.commit().unwrap(); + } + + // Read storage history + { + let reader = hot_kv.reader().unwrap(); + let read_history = reader.get_storage_history(&addr, slot, highest_block).unwrap(); + assert!(read_history.is_some()); + let history = read_history.unwrap(); + assert_eq!(history.iter().collect::>(), vec![5, 15, 25]); + } +} + +/// Test account change sets via HotHistoryWrite/HotHistoryRead +pub fn test_account_changes(hot_kv: &T) { + let addr = address!("0x3333333333333333333333333333333333333333"); + let pre_state = Account { nonce: 10, balance: U256::from(5000), bytecode_hash: None }; + let block_number = 100u64; + + // Write account change + { + let writer = hot_kv.writer().unwrap(); + writer.write_account_prestate(block_number, addr, &pre_state).unwrap(); + writer.commit().unwrap(); + } + + // Read account change + { + let reader = hot_kv.reader().unwrap(); + + let read_change = reader.get_account_change(block_number, &addr).unwrap(); + + assert!(read_change.is_some()); + let change = read_change.unwrap(); + assert_eq!(change.nonce, 10); + assert_eq!(change.balance, U256::from(5000)); + } +} + +/// Test storage change sets via HotHistoryWrite/HotHistoryRead +pub fn test_storage_changes(hot_kv: &T) { + let addr = address!("0x4444444444444444444444444444444444444444"); + let slot = U256::from(153); + let pre_value = U256::from(12345); + let block_number = 200u64; + + // Write storage change + { + let writer = hot_kv.writer().unwrap(); + writer.write_storage_prestate(block_number, addr, &slot, &pre_value).unwrap(); + writer.commit().unwrap(); + } + + // Read storage change + { + let reader = hot_kv.reader().unwrap(); + let read_change = reader.get_storage_change(block_number, &addr, &slot).unwrap(); + assert!(read_change.is_some()); + assert_eq!(read_change.unwrap(), U256::from(12345)); + } +} + +/// Test that missing reads return None +pub fn test_missing_reads(hot_kv: &T) { + let missing_addr = address!("0x9999999999999999999999999999999999999999"); + let missing_hash = b256!("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + let missing_slot = U256::from(99999); + + let reader = hot_kv.reader().unwrap(); + + // Missing header + assert!(reader.get_header(999999).unwrap().is_none()); + + // Missing header number + assert!(reader.get_header_number(&missing_hash).unwrap().is_none()); + + // Missing account + assert!(reader.get_account(&missing_addr).unwrap().is_none()); + + // Missing storage + assert!(reader.get_storage(&missing_addr, &missing_slot).unwrap().is_none()); + + // Missing bytecode + assert!(reader.get_bytecode(&missing_hash).unwrap().is_none()); + + // Missing header by hash + assert!(reader.header_by_hash(&missing_hash).unwrap().is_none()); + + // Missing account history + assert!(reader.get_account_history(&missing_addr, 1000).unwrap().is_none()); + + // Missing storage history + assert!(reader.get_storage_history(&missing_addr, missing_slot, 1000).unwrap().is_none()); + + // Missing account change + assert!(reader.get_account_change(999999, &missing_addr).unwrap().is_none()); + + // Missing storage change + assert!(reader.get_storage_change(999999, &missing_addr, &missing_slot).unwrap().is_none()); +} diff --git a/crates/hot/src/conformance/unwind.rs b/crates/hot/src/conformance/unwind.rs new file mode 100644 index 0000000..0271804 --- /dev/null +++ b/crates/hot/src/conformance/unwind.rs @@ -0,0 +1,485 @@ +//! Unwind conformance test and helpers. + +use crate::{ + db::{HistoryWrite, UnsafeDbWrite}, + model::{DualKeyValue, DualTableTraverse, HotKv, HotKvRead, KeyValue, TableTraverse}, + tables::{self, DualKey, SingleKey}, +}; +use alloy::{ + consensus::{Header, Sealable}, + primitives::{Address, B256, Bytes, U256, address}, +}; +use std::{collections::HashMap, fmt::Debug}; +use trevm::revm::{ + bytecode::Bytecode, + database::{ + AccountStatus, BundleAccount, BundleState, + states::{ + StorageSlot, + reverts::{AccountInfoRevert, AccountRevert, RevertToSlot, Reverts}, + }, + }, + primitives::map::DefaultHashBuilder, + state::AccountInfo, +}; + +/// Collect all entries from a single-keyed table. +pub fn collect_single_table(reader: &R) -> Vec> +where + T: SingleKey, + T::Key: Ord, + R: HotKvRead, +{ + let mut cursor = reader.traverse::().unwrap(); + let mut entries = Vec::new(); + if let Some(first) = TableTraverse::::first(&mut *cursor.inner_mut()).unwrap() { + entries.push(first); + while let Some(next) = TableTraverse::::read_next(&mut *cursor.inner_mut()).unwrap() { + entries.push(next); + } + } + entries.sort_by(|a, b| a.0.cmp(&b.0)); + entries +} + +/// Collect all entries from a dual-keyed table. +pub fn collect_dual_table(reader: &R) -> Vec> +where + T: DualKey, + T::Key: Ord, + T::Key2: Ord, + R: HotKvRead, +{ + let mut cursor = reader.traverse_dual::().unwrap(); + let mut entries = Vec::new(); + if let Some(first) = DualTableTraverse::::first(&mut *cursor.inner_mut()).unwrap() { + entries.push(first); + while let Some(next) = + DualTableTraverse::::read_next(&mut *cursor.inner_mut()).unwrap() + { + entries.push(next); + } + } + entries.sort_by(|a, b| (&a.0, &a.1).cmp(&(&b.0, &b.1))); + entries +} + +/// Assert two single-keyed table contents are equal. +pub fn assert_single_tables_equal(table_name: &str, a: Vec>, b: Vec>) +where + T: SingleKey, + T::Key: Debug + PartialEq, + T::Value: Debug + PartialEq, +{ + assert_eq!( + a.len(), + b.len(), + "{} table entry count mismatch: {} vs {}", + table_name, + a.len(), + b.len() + ); + for (i, (entry_a, entry_b)) in a.iter().zip(b.iter()).enumerate() { + assert_eq!( + entry_a, entry_b, + "{} table entry {} mismatch:\n A: {:?}\n B: {:?}", + table_name, i, entry_a, entry_b + ); + } +} + +/// Assert two dual-keyed table contents are equal. +pub fn assert_dual_tables_equal( + table_name: &str, + a: Vec>, + b: Vec>, +) where + T: DualKey, + T::Key: Debug + PartialEq, + T::Key2: Debug + PartialEq, + T::Value: Debug + PartialEq, +{ + assert_eq!( + a.len(), + b.len(), + "{} table entry count mismatch: {} vs {}", + table_name, + a.len(), + b.len() + ); + for (i, (entry_a, entry_b)) in a.iter().zip(b.iter()).enumerate() { + assert_eq!( + entry_a, entry_b, + "{} table entry {} mismatch:\n A: {:?}\n B: {:?}", + table_name, i, entry_a, entry_b + ); + } +} + +/// Create a BundleState with account and storage changes. +/// +/// This function creates a proper BundleState with reverts populated so that +/// `to_plain_state_and_reverts` will produce the expected output. +#[allow(clippy::type_complexity)] +pub fn make_bundle_state( + accounts: Vec<(Address, Option, Option)>, + storage: Vec<(Address, Vec<(U256, U256, U256)>)>, // (addr, [(slot, old, new)]) + _contracts: Vec<(B256, Bytecode)>, +) -> BundleState { + let mut state: HashMap = Default::default(); + + // Build account reverts for this block + let mut block_reverts: Vec<(Address, AccountRevert)> = Vec::new(); + + for (addr, original, info) in &accounts { + let account_storage: HashMap = Default::default(); + state.insert( + *addr, + BundleAccount { + info: info.clone(), + original_info: original.clone(), + storage: account_storage, + status: AccountStatus::Changed, + }, + ); + + // Create account revert - this stores what to restore to when unwinding + let account_info_revert = match original { + Some(orig) => AccountInfoRevert::RevertTo(orig.clone()), + None => AccountInfoRevert::DeleteIt, + }; + + block_reverts.push(( + *addr, + AccountRevert { + account: account_info_revert, + storage: Default::default(), // Storage reverts added below + previous_status: AccountStatus::Changed, + wipe_storage: false, + }, + )); + } + + // Process storage changes + for (addr, slots) in &storage { + let account = state.entry(*addr).or_insert_with(|| BundleAccount { + info: None, + original_info: None, + storage: Default::default(), + status: AccountStatus::Changed, + }); + + // Find or create the account revert entry + let revert_entry = block_reverts.iter_mut().find(|(a, _)| a == addr); + let account_revert = if let Some((_, revert)) = revert_entry { + revert + } else { + block_reverts.push(( + *addr, + AccountRevert { + account: AccountInfoRevert::DoNothing, + storage: Default::default(), + previous_status: AccountStatus::Changed, + wipe_storage: false, + }, + )); + &mut block_reverts.last_mut().unwrap().1 + }; + + for (slot, old_value, new_value) in slots { + account.storage.insert( + *slot, + StorageSlot { previous_or_original_value: *old_value, present_value: *new_value }, + ); + + // Add storage revert entry + account_revert.storage.insert(*slot, RevertToSlot::Some(*old_value)); + } + } + + // Create Reverts with one block's worth of reverts + let reverts = Reverts::new(vec![block_reverts]); + + BundleState { state, contracts: Default::default(), reverts, state_size: 0, reverts_size: 0 } +} + +/// Create a simple AccountInfo for testing. +pub fn make_account_info(nonce: u64, balance: U256, code_hash: Option) -> AccountInfo { + AccountInfo { nonce, balance, code_hash: code_hash.unwrap_or(B256::ZERO), code: None } +} + +/// Test that unwinding produces the exact same state as never having appended. +/// +/// This test: +/// 1. Creates 5 blocks with complex state changes +/// 2. Appends all 5 blocks to store_a, then unwinds to block 1 (keeping blocks 0, 1) +/// 3. Appends only blocks 0, 1 to store_b +/// 4. Compares ALL tables between the two stores - they must be exactly equal +/// +/// This proves that `unwind_above` correctly reverses all state changes including: +/// - Plain account state +/// - Plain storage state +/// - Headers and header number mappings +/// - Account and storage change sets +/// - Account and storage history indices +pub fn test_unwind_conformance(store_a: &Kv, store_b: &Kv) { + // Test addresses + let addr1 = address!("0x1111111111111111111111111111111111111111"); + let addr2 = address!("0x2222222222222222222222222222222222222222"); + let addr3 = address!("0x3333333333333333333333333333333333333333"); + let addr4 = address!("0x4444444444444444444444444444444444444444"); + + // Storage slots + let slot1 = U256::from(1); + let slot2 = U256::from(2); + let slot3 = U256::from(3); + + // Create bytecode + let code = Bytes::from_static(&[0x60, 0x00, 0x60, 0x00, 0xf3]); + let bytecode = Bytecode::new_raw(code); + let code_hash = bytecode.hash_slow(); + + // Create 5 blocks with complex state + let mut blocks: Vec<(signet_storage_types::SealedHeader, BundleState)> = Vec::new(); + let mut prev_hash = B256::ZERO; + + // Block 0: Create addr1, addr2, addr3 with different states + { + let header = Header { + number: 0, + parent_hash: prev_hash, + gas_limit: 1_000_000, + ..Default::default() + }; + let sealed = header.seal_slow(); + prev_hash = sealed.hash(); + + let bundle = make_bundle_state( + vec![ + (addr1, None, Some(make_account_info(1, U256::from(100), None))), + (addr2, None, Some(make_account_info(1, U256::from(200), None))), + (addr3, None, Some(make_account_info(1, U256::from(300), None))), + ], + vec![(addr1, vec![(slot1, U256::ZERO, U256::from(10))])], + vec![], + ); + blocks.push((sealed, bundle)); + } + + // Block 1: Update addr1, addr2; add storage to addr2 + { + let header = Header { + number: 1, + parent_hash: prev_hash, + gas_limit: 1_000_000, + ..Default::default() + }; + let sealed = header.seal_slow(); + prev_hash = sealed.hash(); + + let bundle = make_bundle_state( + vec![ + ( + addr1, + Some(make_account_info(1, U256::from(100), None)), + Some(make_account_info(2, U256::from(150), None)), + ), + ( + addr2, + Some(make_account_info(1, U256::from(200), None)), + Some(make_account_info(2, U256::from(250), None)), + ), + ], + vec![ + (addr1, vec![(slot1, U256::from(10), U256::from(20))]), + (addr2, vec![(slot1, U256::ZERO, U256::from(100))]), + ], + vec![], + ); + blocks.push((sealed, bundle)); + } + + // Block 2: Update addr3, add bytecode (this is the boundary - will be unwound) + { + let header = Header { + number: 2, + parent_hash: prev_hash, + gas_limit: 1_000_000, + ..Default::default() + }; + let sealed = header.seal_slow(); + prev_hash = sealed.hash(); + + let bundle = make_bundle_state( + vec![( + addr3, + Some(make_account_info(1, U256::from(300), None)), + Some(make_account_info(2, U256::from(350), Some(code_hash))), + )], + vec![(addr3, vec![(slot1, U256::ZERO, U256::from(1000))])], + vec![(code_hash, bytecode.clone())], + ); + blocks.push((sealed, bundle)); + } + + // Block 3: Create addr4, update existing storage + { + let header = Header { + number: 3, + parent_hash: prev_hash, + gas_limit: 1_000_000, + ..Default::default() + }; + let sealed = header.seal_slow(); + prev_hash = sealed.hash(); + + let bundle = make_bundle_state( + vec![ + (addr4, None, Some(make_account_info(1, U256::from(400), None))), + ( + addr1, + Some(make_account_info(2, U256::from(150), None)), + Some(make_account_info(3, U256::from(175), None)), + ), + ], + vec![ + ( + addr1, + vec![ + (slot1, U256::from(20), U256::from(30)), + (slot2, U256::ZERO, U256::from(50)), + ], + ), + (addr4, vec![(slot1, U256::ZERO, U256::from(500))]), + ], + vec![], + ); + blocks.push((sealed, bundle)); + } + + // Block 4: Update multiple addresses and storage + { + let header = Header { + number: 4, + parent_hash: prev_hash, + gas_limit: 1_000_000, + ..Default::default() + }; + let sealed = header.seal_slow(); + + let bundle = make_bundle_state( + vec![ + ( + addr1, + Some(make_account_info(3, U256::from(175), None)), + Some(make_account_info(4, U256::from(200), None)), + ), + ( + addr2, + Some(make_account_info(2, U256::from(250), None)), + Some(make_account_info(3, U256::from(275), None)), + ), + ( + addr4, + Some(make_account_info(1, U256::from(400), None)), + Some(make_account_info(2, U256::from(450), None)), + ), + ], + vec![ + ( + addr1, + vec![ + (slot1, U256::from(30), U256::from(40)), + (slot3, U256::ZERO, U256::from(60)), + ], + ), + ( + addr2, + vec![ + (slot1, U256::from(100), U256::from(150)), + (slot2, U256::ZERO, U256::from(200)), + ], + ), + ], + vec![], + ); + blocks.push((sealed, bundle)); + } + + // Store A: Append all 5 blocks, then unwind to block 1 + { + let writer = store_a.writer().unwrap(); + writer.append_blocks(&blocks).unwrap(); + writer.commit().unwrap(); + } + { + let writer = store_a.writer().unwrap(); + writer.unwind_above(1).unwrap(); + writer.commit().unwrap(); + } + + // Store B: Append only blocks 0, 1 + { + let writer = store_b.writer().unwrap(); + writer.append_blocks(&blocks[0..2]).unwrap(); + writer.commit().unwrap(); + } + + // Compare all tables + let reader_a = store_a.reader().unwrap(); + let reader_b = store_b.reader().unwrap(); + + // Single-keyed tables + assert_single_tables_equal::( + "Headers", + collect_single_table::(&reader_a), + collect_single_table::(&reader_b), + ); + + assert_single_tables_equal::( + "HeaderNumbers", + collect_single_table::(&reader_a), + collect_single_table::(&reader_b), + ); + + assert_single_tables_equal::( + "PlainAccountState", + collect_single_table::(&reader_a), + collect_single_table::(&reader_b), + ); + + // Note: Bytecodes are not removed on unwind (they're content-addressed), + // so store_a may have more bytecodes than store_b. We skip this comparison. + // assert_single_tables_equal::(...) + + // Dual-keyed tables + assert_dual_tables_equal::( + "PlainStorageState", + collect_dual_table::(&reader_a), + collect_dual_table::(&reader_b), + ); + + assert_dual_tables_equal::( + "AccountChangeSets", + collect_dual_table::(&reader_a), + collect_dual_table::(&reader_b), + ); + + assert_dual_tables_equal::( + "StorageChangeSets", + collect_dual_table::(&reader_a), + collect_dual_table::(&reader_b), + ); + + assert_dual_tables_equal::( + "AccountsHistory", + collect_dual_table::(&reader_a), + collect_dual_table::(&reader_b), + ); + + assert_dual_tables_equal::( + "StorageHistory", + collect_dual_table::(&reader_a), + collect_dual_table::(&reader_b), + ); +} diff --git a/crates/hot/src/db/consistent.rs b/crates/hot/src/db/consistent.rs index bd4f1a2..730ecc0 100644 --- a/crates/hot/src/db/consistent.rs +++ b/crates/hot/src/db/consistent.rs @@ -93,128 +93,134 @@ pub trait HistoryWrite: UnsafeDbWrite + UnsafeHistoryWrite { /// Unwind all data above the given block number. /// - /// This completely reverts the database state to what it was at block `block`, - /// including: + /// This completely reverts the database state to what it was at block + /// `block`, including: /// - Plain account state /// - Plain storage state /// - Headers and header number mappings /// - Account and storage change sets /// - Account and storage history indices fn unwind_above(&self, block: BlockNumber) -> Result<(), HistoryError> { - let first_block_number = block + 1; - let Some(last_block_number) = self.last_block_number()? else { + let first_block = block + 1; + let Some(last_block) = self.last_block_number()? else { return Ok(()); }; - if first_block_number > last_block_number { + if first_block > last_block { return Ok(()); } - let storage_range_start = ((first_block_number, Address::ZERO), U256::ZERO); - let storage_range_end = ((last_block_number, ADDRESS_MAX), U256::MAX); - let storage_range = storage_range_start..=storage_range_end; + // ═══════════════════════════════════════════════════════════════════ + // 1. STREAM AccountChangeSets → restore + filter history in one pass + // ═══════════════════════════════════════════════════════════════════ + let mut seen_accounts: HashSet
= HashSet::new(); + let mut account_cursor = self.traverse_dual::()?; - let acct_range_start = (first_block_number, Address::ZERO); - let acct_range_end = (last_block_number, ADDRESS_MAX); - let acct_range = acct_range_start..=acct_range_end; + // Position at first entry + let mut current = account_cursor.next_dual_above(&first_block, &Address::ZERO)?; - // 1. Take and process changesets (reverts plain state) - let storage_changeset = self.take_range_dual::(storage_range)?; - let account_changeset = self.take_range_dual::(acct_range)?; + while let Some((block_num, address, old_account)) = current { + if block_num > last_block { + break; + } - // Collect affected addresses and slots for history cleanup - let mut affected_addresses: HashSet
= HashSet::new(); - let mut affected_storage: HashSet<(Address, U256)> = HashSet::new(); + // First occurrence = process both plain state and history + if seen_accounts.insert(address) { + // Restore plain state + if old_account.is_empty() { + self.queue_delete::(&address)?; + } else { + self.put_account(&address, &old_account)?; + } - for (_, address, _) in &account_changeset { - affected_addresses.insert(*address); - } - for (block_addr, slot, _) in &storage_changeset { - affected_storage.insert((block_addr.1, *slot)); + // Filter history index + if let Some((shard_key, list)) = self.last_account_history(address)? { + self.queue_delete_dual::(&address, &shard_key)?; + let filtered: Vec = list.iter().filter(|&bn| bn <= block).collect(); + if !filtered.is_empty() { + self.write_account_history( + &address, + u64::MAX, + &BlockNumberList::new_pre_sorted(filtered), + )?; + } + } + } + + current = account_cursor.read_next()?; } - // Revert plain state using existing logic - let mut plain_accounts_cursor = self.traverse_mut::()?; - let mut plain_storage_cursor = self.traverse_dual_mut::()?; + // ═══════════════════════════════════════════════════════════════════ + // 2. STREAM StorageChangeSets → restore + filter history in one pass + // ═══════════════════════════════════════════════════════════════════ + let mut seen_storage: HashSet<(Address, U256)> = HashSet::new(); + let mut storage_cursor = self.traverse_dual::()?; - let state = self.populate_bundle_state( - account_changeset, - storage_changeset, - &mut plain_accounts_cursor, - &mut plain_storage_cursor, - )?; + // Position at first entry + let mut current_storage = + storage_cursor.next_dual_above(&(first_block, Address::ZERO), &U256::ZERO)?; - for (address, (old_account, new_account, storage)) in &state { - if old_account != new_account { - let existing_entry = plain_accounts_cursor.lower_bound(address)?; - if let Some(account) = old_account { - // Check if the old account is effectively empty (account didn't exist before) - // An empty account has nonce=0, balance=0, no bytecode - let is_empty = account.nonce == 0 - && account.balance.is_zero() - && account.bytecode_hash.is_none(); - - if is_empty { - // Account was created - delete it - if existing_entry.is_some_and(|(k, _)| k == *address) { - plain_accounts_cursor.delete_current()?; - } - } else { - // Account existed before - restore it - self.put_account(address, account)?; - } - } else if existing_entry.is_some_and(|(k, _)| k == *address) { - plain_accounts_cursor.delete_current()?; - } + while let Some(((block_num, address), slot, old_value)) = current_storage { + if block_num > last_block { + break; } - for (storage_key_b256, (old_storage_value, _)) in storage { - let storage_key = U256::from_be_bytes(storage_key_b256.0); - - if plain_storage_cursor - .next_dual_above(address, &storage_key)? - .is_some_and(|(k, k2, _)| k == *address && k2 == storage_key) - { - plain_storage_cursor.delete_current()?; + if seen_storage.insert((address, slot)) { + // Restore plain state + if old_value.is_zero() { + self.queue_delete_dual::(&address, &slot)?; + } else { + self.put_storage(&address, &slot, &old_value)?; } - if !old_storage_value.is_zero() { - self.put_storage(address, &storage_key, old_storage_value)?; + // Filter history index + if let Some((shard_key, list)) = self.last_storage_history(&address, &slot)? { + self.queue_delete_dual::(&address, &shard_key)?; + let filtered: Vec = list.iter().filter(|&bn| bn <= block).collect(); + if !filtered.is_empty() { + self.write_storage_history( + &address, + slot, + u64::MAX, + &BlockNumberList::new_pre_sorted(filtered), + )?; + } } } - } - // 2. Remove headers and header number mappings - let removed_headers = - self.take_range::(first_block_number..=last_block_number)?; - for (_, header) in removed_headers { - let hash = header.hash_slow(); - self.delete_header_number(&hash)?; + current_storage = storage_cursor.read_next()?; } - // 3. Clean up account history indices - for address in affected_addresses { - if let Some((shard_key, list)) = self.last_account_history(address)? { - let filtered: Vec = list.iter().filter(|&bn| bn <= block).collect(); - self.queue_delete_dual::(&address, &shard_key)?; - if !filtered.is_empty() { - let new_list = BlockNumberList::new_pre_sorted(filtered); - self.write_account_history(&address, u64::MAX, &new_list)?; - } - } - } + // ═══════════════════════════════════════════════════════════════════ + // 3. DELETE changeset ranges + // ═══════════════════════════════════════════════════════════════════ + self.traverse_dual_mut::()? + .delete_range((first_block, Address::ZERO)..=(last_block, ADDRESS_MAX))?; + self.traverse_dual_mut::()?.delete_range( + ((first_block, Address::ZERO), U256::ZERO)..=((last_block, ADDRESS_MAX), U256::MAX), + )?; - // 4. Clean up storage history indices - for (address, slot) in affected_storage { - if let Some((shard_key, list)) = self.last_storage_history(&address, &slot)? { - let filtered: Vec = list.iter().filter(|&bn| bn <= block).collect(); - self.queue_delete_dual::(&address, &shard_key)?; - if !filtered.is_empty() { - let new_list = BlockNumberList::new_pre_sorted(filtered); - self.write_storage_history(&address, slot, u64::MAX, &new_list)?; + // ═══════════════════════════════════════════════════════════════════ + // 4. STREAM Headers → delete HeaderNumbers, then clear Headers + // ═══════════════════════════════════════════════════════════════════ + let mut header_cursor = self.traverse::()?; + + // Position at first entry and process it + let first_entry = header_cursor.lower_bound(&first_block)?; + if let Some((block_num, header)) = first_entry + && block_num <= last_block + { + self.delete_header_number(&header.hash_slow())?; + + // Continue with remaining entries + while let Some((block_num, header)) = header_cursor.read_next()? { + if block_num > last_block { + break; } + self.delete_header_number(&header.hash_slow())?; } } + self.traverse_mut::()?.delete_range_inclusive(first_block..=last_block)?; Ok(()) } diff --git a/crates/hot/src/db/inconsistent.rs b/crates/hot/src/db/inconsistent.rs index 730f6e3..0691db4 100644 --- a/crates/hot/src/db/inconsistent.rs +++ b/crates/hot/src/db/inconsistent.rs @@ -1,17 +1,15 @@ use crate::{ db::{HistoryError, HistoryRead}, - model::{DualKeyTraverse, DualTableCursor, HotKvWrite, KvTraverse, TableCursor}, + model::HotKvWrite, tables, }; use alloy::{ consensus::Header, primitives::{Address, B256, BlockNumber, U256}, }; +use itertools::Itertools; use signet_storage_types::{Account, BlockNumberList, SealedHeader, ShardedKey}; -use std::{ - collections::{BTreeMap, HashMap, hash_map}, - ops::RangeInclusive, -}; +use std::{collections::HashMap, ops::RangeInclusive}; use trevm::revm::{ bytecode::Bytecode, database::{ @@ -150,9 +148,11 @@ pub trait UnsafeHistoryWrite: UnsafeDbWrite + HistoryRead { fn write_wipe(&self, block_number: u64, address: &Address) -> Result<(), Self::Error> { let mut cursor = self.traverse_dual::()?; - cursor.for_each_k2(address, &U256::ZERO, |_addr, slot, value| { - self.write_storage_prestate(block_number, *address, &slot, &value) - }) + for entry in cursor.iter_k2(address, &U256::ZERO)? { + let (_addr, slot, value) = entry?; + self.write_storage_prestate(block_number, *address, &slot, &value)?; + } + Ok(()) } /// Write a block's plain state revert information. @@ -175,7 +175,8 @@ pub trait UnsafeHistoryWrite: UnsafeDbWrite + HistoryRead { for entry in storage { if entry.wiped { - return self.write_wipe(block_number, &entry.address); + self.write_wipe(block_number, &entry.address)?; + continue; } for (key, old_value) in entry.storage_revert.iter() { self.write_storage_prestate( @@ -226,16 +227,7 @@ pub trait UnsafeHistoryWrite: UnsafeDbWrite + HistoryRead { PlainStorageChangeset { address, wipe_storage, storage }: &PlainStorageChangeset, ) -> Result<(), Self::Error> { if *wipe_storage { - let mut cursor = self.traverse_dual_mut::()?; - - while let Some((key, _, _)) = cursor.next_k2()? { - if key != *address { - break; - } - cursor.delete_current()?; - } - - return Ok(()); + return self.clear_k1_for::(address); } storage.iter().try_for_each(|(key, value)| self.put_storage(address, key, value)) @@ -270,27 +262,19 @@ pub trait UnsafeHistoryWrite: UnsafeDbWrite + HistoryRead { /// Get all changed accounts with the list of block numbers in the given /// range. /// - /// Note: This iterates using `next_k2()` which stays within the same k1 - /// (block number). It effectively only collects changes from the first - /// block number in the range. + /// Iterates over entries starting from the first block in the range, + /// collecting changes while the block number remains in range. fn changed_accounts_with_range( &self, range: RangeInclusive, - ) -> Result>, Self::Error> { - let mut changeset_cursor = self.traverse_dual::()?; - let mut result: BTreeMap> = BTreeMap::new(); - - changeset_cursor.for_each_while_k2( - range.start(), - &Address::ZERO, - |num, _, _| range.contains(num), - |num, addr, _| { - result.entry(addr).or_default().push(num); - Ok(()) - }, - )?; - - Ok(result) + ) -> Result>, Self::Error> { + self.traverse_dual::()? + .iter_from(range.start(), &Address::ZERO)? + .process_results(|iter| { + iter.take_while(|(num, _, _)| range.contains(num)) + .map(|(num, addr, _)| (addr, num)) + .into_group_map() + }) } /// Append account history indices for multiple accounts. @@ -299,46 +283,13 @@ pub trait UnsafeHistoryWrite: UnsafeDbWrite + HistoryRead { index_updates: impl IntoIterator)>, ) -> Result<(), HistoryError> { for (acct, indices) in index_updates { - // Get the existing last shard (if any) and remember its key so we can - // delete it before writing new shards let existing = self.last_account_history(acct)?; - // Save the old key before taking ownership of the list - let old_key = existing.as_ref().map(|(key, _)| *key); - // Take ownership instead of cloning - let mut last_shard = existing.map(|(_, list)| list).unwrap_or_default(); - - last_shard.append(indices).map_err(HistoryError::IntList)?; - - // Delete the existing shard before writing new ones to avoid duplicates - if let Some(old_key) = old_key { - self.queue_delete_dual::(&acct, &old_key)?; - } - - // fast path: all indices fit in one shard - if last_shard.len() <= ShardedKey::SHARD_COUNT as u64 { - self.write_account_history(&acct, u64::MAX, &last_shard)?; - continue; - } - - // slow path: rechunk into multiple shards - // Reuse a single buffer to avoid allocating a new Vec per chunk - let mut chunk_buf = Vec::with_capacity(ShardedKey::SHARD_COUNT); - let mut iter = last_shard.iter().peekable(); - - while iter.peek().is_some() { - chunk_buf.clear(); - chunk_buf.extend(iter.by_ref().take(ShardedKey::SHARD_COUNT)); - - let highest_block_number = if iter.peek().is_some() { - *chunk_buf.last().expect("chunk_buf is non-empty") - } else { - // Insert last list with `u64::MAX`. - u64::MAX - }; - - let shard = BlockNumberList::new_pre_sorted(chunk_buf.iter().copied()); - self.write_account_history(&acct, highest_block_number, &shard)?; - } + append_to_sharded_history( + existing, + indices, + |key| self.queue_delete_dual::(&acct, &key), + |height, list| self.write_account_history(&acct, height, list), + )?; } Ok(()) } @@ -346,28 +297,20 @@ pub trait UnsafeHistoryWrite: UnsafeDbWrite + HistoryRead { /// Get all changed storages with the list of block numbers in the given /// range. /// - /// Note: This iterates using `next_k2()` which stays within the same k1 - /// (block number + address). It effectively only collects changes from - /// the first key1 value in the range. + /// Iterates over entries starting from the first block in the range, + /// collecting changes while the block number remains in range. #[allow(clippy::type_complexity)] fn changed_storages_with_range( &self, range: RangeInclusive, - ) -> Result>, Self::Error> { - let mut changeset_cursor = self.traverse_dual::()?; - let mut result: BTreeMap<(Address, U256), Vec> = BTreeMap::new(); - - changeset_cursor.for_each_while_k2( - &(*range.start(), Address::ZERO), - &U256::ZERO, - |num_addr, _, _| range.contains(&num_addr.0), - |num_addr, slot, _| { - result.entry((num_addr.1, slot)).or_default().push(num_addr.0); - Ok(()) - }, - )?; - - Ok(result) + ) -> Result>, Self::Error> { + self.traverse_dual::()? + .iter_from(&(*range.start(), Address::ZERO), &U256::ZERO)? + .process_results(|iter| { + iter.take_while(|(num_addr, _, _)| range.contains(&num_addr.0)) + .map(|(num_addr, slot, _)| ((num_addr.1, slot), num_addr.0)) + .into_group_map() + }) } /// Append storage history indices for multiple (address, slot) pairs. @@ -376,46 +319,13 @@ pub trait UnsafeHistoryWrite: UnsafeDbWrite + HistoryRead { index_updates: impl IntoIterator)>, ) -> Result<(), HistoryError> { for ((addr, slot), indices) in index_updates { - // Get the existing last shard (if any) and remember its key so we can - // delete it before writing new shards let existing = self.last_storage_history(&addr, &slot)?; - // Save the old key before taking ownership of the list (clone is cheap for ShardedKey) - let old_key = existing.as_ref().map(|(key, _)| key.clone()); - // Take ownership instead of cloning the BlockNumberList - let mut last_shard = existing.map(|(_, list)| list).unwrap_or_default(); - - last_shard.append(indices).map_err(HistoryError::IntList)?; - - // Delete the existing shard before writing new ones to avoid duplicates - if let Some(old_key) = old_key { - self.queue_delete_dual::(&addr, &old_key)?; - } - - // fast path: all indices fit in one shard - if last_shard.len() <= ShardedKey::SHARD_COUNT as u64 { - self.write_storage_history(&addr, slot, u64::MAX, &last_shard)?; - continue; - } - - // slow path: rechunk into multiple shards - // Reuse a single buffer to avoid allocating a new Vec per chunk - let mut chunk_buf = Vec::with_capacity(ShardedKey::SHARD_COUNT); - let mut iter = last_shard.iter().peekable(); - - while iter.peek().is_some() { - chunk_buf.clear(); - chunk_buf.extend(iter.by_ref().take(ShardedKey::SHARD_COUNT)); - - let highest_block_number = if iter.peek().is_some() { - *chunk_buf.last().expect("chunk_buf is non-empty") - } else { - // Insert last list with `u64::MAX`. - u64::MAX - }; - - let shard = BlockNumberList::new_pre_sorted(chunk_buf.iter().copied()); - self.write_storage_history(&addr, slot, highest_block_number, &shard)?; - } + append_to_sharded_history( + existing, + indices, + |key| self.queue_delete_dual::(&addr, &key), + |height, list| self.write_storage_history(&addr, slot, height, list), + )?; } Ok(()) } @@ -480,75 +390,67 @@ pub trait UnsafeHistoryWrite: UnsafeDbWrite + HistoryRead { ) -> Result<(), Self::Error> { blocks.iter().try_for_each(|(header, state)| self.append_block_inconsistent(header, state)) } +} - /// Populate a [`BundleInit`] using cursors over the - /// [`tables::PlainAccountState`] and [`tables::PlainStorageState`] tables, - /// based on the given storage and account changesets. - /// - /// Returns a map of address -> (old_account, new_account, storage_changes) - /// where storage_changes maps slot -> (old_value, new_value). - fn populate_bundle_state( - &self, - account_changeset: Vec<(u64, Address, Account)>, - storage_changeset: Vec<((u64, Address), U256, U256)>, - plain_accounts_cursor: &mut TableCursor, - plain_storage_cursor: &mut DualTableCursor, - ) -> Result - where - C: KvTraverse, - D: DualKeyTraverse, - { - // iterate previous value and get plain state value to create changeset - // Double option around Account represent if Account state is known (first option) and - // account is removed (second option) - let mut state: BundleInit = Default::default(); - - // add account changeset changes in reverse order - for (_block_number, address, old_account) in account_changeset.into_iter().rev() { - match state.entry(address) { - hash_map::Entry::Vacant(entry) => { - let new_account = plain_accounts_cursor.exact(&address)?; - entry.insert((Some(old_account), new_account, HashMap::default())); - } - hash_map::Entry::Occupied(mut entry) => { - // overwrite old account state. - entry.get_mut().0 = Some(old_account); - } - } - } +impl UnsafeHistoryWrite for T where T: UnsafeDbWrite + HotKvWrite {} - // add storage changeset changes - for ((_block, address), storage_key, old_value) in storage_changeset.into_iter().rev() { - // get account state or insert from plain state. - let account_state = match state.entry(address) { - hash_map::Entry::Vacant(entry) => { - let present_account = plain_accounts_cursor.exact(&address)?; - entry.insert((present_account, present_account, HashMap::default())) - } - hash_map::Entry::Occupied(entry) => entry.into_mut(), - }; - - // Convert U256 storage key to B256 for the BundleInit map - let storage_key_b256 = B256::from(storage_key); - - // match storage. - match account_state.2.entry(storage_key_b256) { - hash_map::Entry::Vacant(entry) => { - let new_value = plain_storage_cursor - .next_dual_above(&address, &storage_key)? - .filter(|(k, k2, _)| *k == address && *k2 == storage_key) - .map(|(_, _, v)| v) - .unwrap_or_default(); - entry.insert((old_value, new_value)); - } - hash_map::Entry::Occupied(mut entry) => { - entry.get_mut().0 = old_value; - } - }; - } +/// Append indices to a sharded history entry, handling shard splitting. +/// +/// This helper handles the common pattern of: +/// 1. Appending new block numbers to an existing shard +/// 2. Deleting the old shard if it exists +/// 3. Splitting into multiple shards if the result exceeds the shard size +/// +/// # Arguments +/// - `existing`: The current last shard (key, list) if any +/// - `indices`: New block numbers to append +/// - `delete_old`: Called to delete the old shard key before writing new ones +/// - `write_shard`: Called for each resulting shard (highest_block, list) +fn append_to_sharded_history( + existing: Option<(K, BlockNumberList)>, + indices: impl IntoIterator, + mut delete_old: D, + mut write_shard: W, +) -> Result<(), HistoryError> +where + E: std::error::Error, + D: FnMut(K) -> Result<(), E>, + W: FnMut(u64, &BlockNumberList) -> Result<(), E>, +{ + let (old_key, last_shard) = + existing.map_or_else(|| (None, BlockNumberList::default()), |(k, list)| (Some(k), list)); + let mut last_shard = last_shard; + + last_shard.append(indices).map_err(HistoryError::IntList)?; + + // Delete the existing shard before writing new ones to avoid duplicates + if let Some(key) = old_key { + delete_old(key).map_err(HistoryError::Db)?; + } - Ok(state) + // Fast path: all indices fit in one shard + if last_shard.len() <= ShardedKey::SHARD_COUNT as u64 { + return write_shard(u64::MAX, &last_shard).map_err(HistoryError::Db); } -} -impl UnsafeHistoryWrite for T where T: UnsafeDbWrite + HotKvWrite {} + // Slow path: rechunk into multiple shards + // Reuse a single buffer to avoid allocating a new Vec per chunk + let mut chunk_buf = Vec::with_capacity(ShardedKey::SHARD_COUNT); + let mut iter = last_shard.iter().peekable(); + + while iter.peek().is_some() { + chunk_buf.clear(); + chunk_buf.extend(iter.by_ref().take(ShardedKey::SHARD_COUNT)); + + let highest = if iter.peek().is_some() { + *chunk_buf.last().expect("chunk_buf is non-empty") + } else { + // Insert last list with `u64::MAX` + u64::MAX + }; + + let shard = BlockNumberList::new_pre_sorted(chunk_buf.iter().copied()); + write_shard(highest, &shard).map_err(HistoryError::Db)?; + } + Ok(()) +} diff --git a/crates/hot/src/mem.rs b/crates/hot/src/mem.rs index 4969a24..41fbafc 100644 --- a/crates/hot/src/mem.rs +++ b/crates/hot/src/mem.rs @@ -5,8 +5,8 @@ use crate::{ model::{ - DualKeyTraverse, HotKv, HotKvError, HotKvRead, HotKvReadError, HotKvWrite, KvTraverse, - KvTraverseMut, RawDualKeyValue, RawKeyValue, RawValue, + DualKeyTraverse, DualKeyTraverseMut, HotKv, HotKvError, HotKvRead, HotKvReadError, + HotKvWrite, KvTraverse, KvTraverseMut, RawDualKeyValue, RawKeyValue, RawValue, }, ser::{DeserError, MAX_KEY_SIZE}, }; @@ -1039,6 +1039,40 @@ impl<'a> DualKeyTraverse for MemKvCursorMut<'a> { } } +impl DualKeyTraverseMut for MemKvCursorMut<'_> { + fn delete_current(&mut self) -> Result<(), MemKvError> { + // Delegate to KvTraverseMut since the cursor position is shared + KvTraverseMut::delete_current(self) + } + + fn clear_k1(&mut self, key1: &[u8]) -> Result<(), MemKvError> { + // Build prefix for k1: [k1 padded to MAX_KEY_SIZE][zeros for k2] + let prefix_start = MemKv::dual_key(key1, &[0u8; MAX_KEY_SIZE]); + let prefix_end = MemKv::dual_key(key1, &[0xffu8; MAX_KEY_SIZE]); + + // Collect all keys to delete from both committed storage and queued ops + let mut keys_to_delete: Vec<_> = if !self.is_cleared { + self.table.range(prefix_start..=prefix_end).map(|(k, _)| *k).collect() + } else { + Vec::new() + }; + + // Also collect any keys in queued ops within this range + { + let queued_ops = self.queued_ops.lock().unwrap(); + keys_to_delete.extend(queued_ops.range(prefix_start..=prefix_end).map(|(k, _)| *k)); + } + + // Queue deletion for all keys + let mut queued_ops = self.queued_ops.lock().unwrap(); + for key in keys_to_delete { + queued_ops.insert(key, QueuedKvOp::Delete); + } + + Ok(()) + } +} + impl HotKv for MemKv { type RoTx = MemKvRoTx; type RwTx = MemKvRwTx; @@ -2353,4 +2387,74 @@ mod tests { let result = DualTableTraverse::::previous_k1(&mut cursor).unwrap(); assert!(result.is_none()); } + + #[test] + fn test_clear_k1() { + use crate::model::DualTableTraverseMut; + + let store = MemKv::new(); + + // Setup test data: + // k1=1: k2=[10, 20, 30] + // k1=2: k2=[100, 200] + // k1=3: k2=[1000] + let dual_data = vec![ + (1u64, 10u32, Bytes::from_static(b"v1_10")), + (1u64, 20u32, Bytes::from_static(b"v1_20")), + (1u64, 30u32, Bytes::from_static(b"v1_30")), + (2u64, 100u32, Bytes::from_static(b"v2_100")), + (2u64, 200u32, Bytes::from_static(b"v2_200")), + (3u64, 1000u32, Bytes::from_static(b"v3_1000")), + ]; + + // Insert data + { + let writer = store.writer().unwrap(); + for (key1, key2, value) in &dual_data { + writer.queue_put_dual::(key1, key2, value).unwrap(); + } + writer.raw_commit().unwrap(); + } + + // Clear all K2 entries for K1=2 + { + let writer = store.writer().unwrap(); + { + let mut cursor = writer.raw_traverse_mut(DualTestTable::NAME).unwrap(); + DualTableTraverseMut::::clear_k1(&mut cursor, &2u64).unwrap(); + } + writer.raw_commit().unwrap(); + } + + // Verify K1=2 entries are deleted, others remain + { + let reader = store.reader().unwrap(); + // K1=1 should exist + assert!(reader.get_dual::(&1u64, &10u32).unwrap().is_some()); + assert!(reader.get_dual::(&1u64, &20u32).unwrap().is_some()); + assert!(reader.get_dual::(&1u64, &30u32).unwrap().is_some()); + // K1=2 should be deleted + assert!(reader.get_dual::(&2u64, &100u32).unwrap().is_none()); + assert!(reader.get_dual::(&2u64, &200u32).unwrap().is_none()); + // K1=3 should exist + assert!(reader.get_dual::(&3u64, &1000u32).unwrap().is_some()); + } + + // Test idempotent - clearing already deleted K1 should be no-op + { + let writer = store.writer().unwrap(); + { + let mut cursor = writer.raw_traverse_mut(DualTestTable::NAME).unwrap(); + DualTableTraverseMut::::clear_k1(&mut cursor, &2u64).unwrap(); + } + writer.raw_commit().unwrap(); + } + + // Verify state is unchanged + { + let reader = store.reader().unwrap(); + assert!(reader.get_dual::(&1u64, &10u32).unwrap().is_some()); + assert!(reader.get_dual::(&3u64, &1000u32).unwrap().is_some()); + } + } } diff --git a/crates/hot/src/model/mod.rs b/crates/hot/src/model/mod.rs index 9f2ab66..7f67903 100644 --- a/crates/hot/src/model/mod.rs +++ b/crates/hot/src/model/mod.rs @@ -45,8 +45,8 @@ pub use traits::{HotKv, HotKvRead, HotKvWrite}; mod traverse; pub use traverse::{ - DualKeyTraverse, DualTableCursor, DualTableTraverse, KvTraverse, KvTraverseMut, TableCursor, - TableTraverse, TableTraverseMut, + DualKeyTraverse, DualKeyTraverseMut, DualTableCursor, DualTableTraverse, DualTableTraverseMut, + KvTraverse, KvTraverseMut, TableCursor, TableTraverse, TableTraverseMut, }; use crate::tables::{DualKey, Table}; diff --git a/crates/hot/src/model/traits.rs b/crates/hot/src/model/traits.rs index e228c09..6802db2 100644 --- a/crates/hot/src/model/traits.rs +++ b/crates/hot/src/model/traits.rs @@ -1,13 +1,13 @@ use crate::{ model::{ - DualKeyTraverse, DualKeyValue, DualTableCursor, HotKvError, HotKvReadError, KeyValue, + DualKeyTraverse, DualKeyTraverseMut, DualTableCursor, HotKvError, HotKvReadError, KvTraverse, KvTraverseMut, TableCursor, revm::{RevmRead, RevmWrite}, }, ser::{KeySer, MAX_KEY_SIZE, ValSer}, tables::{DualKey, SingleKey, Table}, }; -use std::{borrow::Cow, ops::RangeInclusive}; +use std::borrow::Cow; /// Trait for hot storage. This is a KV store with read/write transactions. /// @@ -182,7 +182,7 @@ pub trait HotKvRead { /// This extends the [`HotKvRead`] trait with write capabilities. pub trait HotKvWrite: HotKvRead { /// The mutable cursor type for traversing key-value pairs. - type TraverseMut<'a>: KvTraverseMut + DualKeyTraverse + type TraverseMut<'a>: KvTraverseMut + DualKeyTraverseMut where Self: 'a; @@ -387,127 +387,10 @@ pub trait HotKvWrite: HotKvRead { self.queue_raw_clear(T::NAME) } - /// Remove all data in the given range and return the removed keys. - fn clear_with_op( - &self, - range: RangeInclusive, - mut op: impl FnMut(T::Key, T::Value), - ) -> Result<(), Self::Error> { - let mut cursor = self.traverse_mut::()?; - - // Position cursor at first entry at or above range start - let Some((key, value)) = cursor.lower_bound(range.start())? else { - // No entries at or above range start - return Ok(()); - }; - - if !range.contains(&key) { - // First entry is outside range - return Ok(()); - } - - op(key, value); - cursor.delete_current()?; - - // Iterate through remaining entries - while let Some((key, value)) = cursor.read_next()? { - if !range.contains(&key) { - break; - } - op(key, value); - cursor.delete_current()?; - } - - Ok(()) - } - - /// Remove all data in the given range from the database. - fn clear_range(&self, range: RangeInclusive) -> Result<(), Self::Error> { - self.clear_with_op::(range, |_, _| {}) - } - - /// Remove all data in the given range and return the removed key-value - /// pairs. - fn take_range( - &self, - range: RangeInclusive, - ) -> Result>, Self::Error> { - let mut vec = Vec::new(); - self.clear_with_op::(range, |key, value| vec.push((key, value)))?; - Ok(vec) - } - - /// Remove all dual-keyed data in the given range from the database. - fn clear_range_dual_with_op( - &self, - range: RangeInclusive<(T::Key, T::Key2)>, - mut op: impl FnMut(T::Key, T::Key2, T::Value), - ) -> Result<(), Self::Error> { + /// Clear all K2 entries for a specific K1 in a dual-keyed table. + fn clear_k1_for(&self, key1: &T::Key) -> Result<(), Self::Error> { let mut cursor = self.traverse_dual_mut::()?; - - let (start_k1, start_k2) = range.start(); - - // Position at first entry at or above (range.start(), minimal_k2) - let Some((k1, k2, value)) = cursor.next_dual_above(start_k1, start_k2)? else { - // No entries at or above range start - return Ok(()); - }; - - // inline range contains to avoid moving k1,k2 - let (range_1, range_2) = range.start(); - if range_1 > &k1 || (range_1 == &k1 && range_2 > &k2) { - // First entry is outside range - return Ok(()); - } - let (range_1, range_2) = range.end(); - if range_1 < &k1 || (range_1 == &k1 && range_2 < &k2) { - // First entry is outside range - return Ok(()); - } - // end of inline range contains - - op(k1, k2, value); - cursor.delete_current()?; - - // Iterate through all entries (both k1 and k2 changes) - // Use read_next() instead of next_k2() to navigate across different k1 values - while let Some((k1, k2, value)) = cursor.read_next()? { - // inline range contains to avoid moving k1,k2 - let (range_1, range_2) = range.start(); - if range_1 > &k1 || (range_1 == &k1 && range_2 > &k2) { - break; - } - let (range_1, range_2) = range.end(); - if range_1 < &k1 || (range_1 == &k1 && range_2 < &k2) { - break; - } - // end of inline range contains - op(k1, k2, value); - cursor.delete_current()?; - } - - Ok(()) - } - - /// Remove all dual-keyed data in the given k1,k2 range from the database. - fn clear_range_dual( - &self, - range: RangeInclusive<(T::Key, T::Key2)>, - ) -> Result<(), Self::Error> { - self.clear_range_dual_with_op::(range, |_, _, _| {}) - } - - /// Remove all dual-keyed data in the given k1,k2 range and return the - /// removed key-key-value tuples. - fn take_range_dual( - &self, - range: RangeInclusive<(T::Key, T::Key2)>, - ) -> Result>, Self::Error> { - let mut vec = Vec::new(); - self.clear_range_dual_with_op::(range, |k1, k2, value| { - vec.push((k1, k2, value)); - })?; - Ok(vec) + cursor.clear_k1(key1) } /// Commit the queued operations. diff --git a/crates/hot/src/model/traverse.rs b/crates/hot/src/model/traverse.rs index fc303d1..613acd8 100644 --- a/crates/hot/src/model/traverse.rs +++ b/crates/hot/src/model/traverse.rs @@ -6,7 +6,7 @@ use crate::{ tables::{DualKey, SingleKey}, }; use core::marker::PhantomData; -use std::ops::Range; +use std::ops::{Range, RangeInclusive}; /// Trait for traversing key-value pairs in the database. pub trait KvTraverse { @@ -37,6 +37,36 @@ pub trait KvTraverse { /// Returning `Ok(None)` indicates the cursor is before the start of the /// database. fn read_prev<'a>(&'a mut self) -> Result>, E>; + + /// Position at first entry and return iterator over all entries. + /// + /// The default implementation uses `first()` followed by repeated + /// `read_next()` calls. Implementations may override this to use + /// more efficient native iterators. + fn iter(&mut self) -> Result, E>> + '_, E> + where + Self: Sized, + { + self.first()?; + Ok(RawKvIter { cursor: self, done: false, _marker: PhantomData }) + } + + /// Position at `key` and return iterator over subsequent entries. + /// + /// The iterator starts at the first entry with key >= `key`. + /// The default implementation uses `lower_bound()` followed by repeated + /// `read_next()` calls. Implementations may override this to use + /// more efficient native iterators. + fn iter_from<'a>( + &'a mut self, + key: &[u8], + ) -> Result, E>> + 'a, E> + where + Self: Sized, + { + self.lower_bound(key)?; + Ok(RawKvIter { cursor: self, done: false, _marker: PhantomData }) + } } /// Trait for traversing key-value pairs in the database with mutation @@ -45,7 +75,7 @@ pub trait KvTraverseMut: KvTraverse { /// Delete the current key-value pair in the database. fn delete_current(&mut self) -> Result<(), E>; - /// Delete a range of key-value pairs in the database, from `start_key` + /// Delete a range of key-value pairs in the database (exclusive end). fn delete_range(&mut self, range: Range<&[u8]>) -> Result<(), E> { let Some((key, _)) = self.lower_bound(range.start)? else { return Ok(()); @@ -63,6 +93,25 @@ pub trait KvTraverseMut: KvTraverse { } Ok(()) } + + /// Delete a range of key-value pairs in the database (inclusive end). + fn delete_range_inclusive(&mut self, start: &[u8], end: &[u8]) -> Result<(), E> { + let Some((key, _)) = self.lower_bound(start)? else { + return Ok(()); + }; + if key.as_ref() > end { + return Ok(()); + } + self.delete_current()?; + + while let Some((key, _)) = self.read_next()? { + if key.as_ref() > end { + break; + } + self.delete_current()?; + } + Ok(()) + } } /// Trait for traversing dual-keyed key-value pairs in the database. @@ -133,6 +182,108 @@ pub trait DualKeyTraverse { /// duplicate values. /// Returning `Ok(None)` indicates there is no previous key2 for this key1. fn previous_k2<'a>(&'a mut self) -> Result>, E>; + + /// Position at first entry and return iterator over all entries. + /// + /// The default implementation uses `first()` followed by repeated + /// `read_next()` calls. Implementations may override this to use + /// more efficient native iterators. + fn iter(&mut self) -> Result, E>> + '_, E> + where + Self: Sized, + { + self.first()?; + Ok(RawDualKeyIter { cursor: self, done: false, _marker: PhantomData }) + } + + /// Position at (k1, k2) and return iterator over subsequent entries. + /// + /// The iterator starts at the first entry with (k1, k2) >= the specified + /// keys and continues through all subsequent entries, crossing k1 boundaries. + fn iter_from<'a>( + &'a mut self, + k1: &[u8], + k2: &[u8], + ) -> Result, E>> + 'a, E> + where + Self: Sized, + { + self.next_dual_above(k1, k2)?; + Ok(RawDualKeyIter { cursor: self, done: false, _marker: PhantomData }) + } + + /// Iterate k2 entries within a single k1. + /// + /// The iterator starts at the first entry with (k1, k2) >= the specified + /// keys and stops when k1 changes or the table is exhausted. + fn iter_k2<'a>( + &'a mut self, + k1: &[u8], + start_k2: &[u8], + ) -> Result, E>> + 'a, E> + where + Self: Sized, + { + let entry = self.next_dual_above(k1, start_k2)?; + let Some((found_k1, _, _)) = entry else { + return Ok(RawDualKeyK2Iter { + cursor: self, + k1: Vec::new(), + done: true, + _marker: PhantomData, + }); + }; + // If the found k1 doesn't match, we're done + let done = found_k1.as_ref() != k1; + Ok(RawDualKeyK2Iter { cursor: self, k1: k1.to_vec(), done, _marker: PhantomData }) + } +} + +/// Trait for traversing dual-keyed key-value pairs with mutation capabilities. +pub trait DualKeyTraverseMut: DualKeyTraverse { + /// Delete all K2 entries for the specified K1. + /// + /// This positions the cursor at the given K1 and removes all associated + /// K2 entries in a single operation. + /// + /// Returns `Ok(())` if the K1 was cleared or didn't exist. + fn clear_k1(&mut self, key1: &[u8]) -> Result<(), E>; + + /// Delete the current dual-keyed entry. + fn delete_current(&mut self) -> Result<(), E>; + + /// Delete a range of dual-keyed entries (inclusive). + /// + /// Deletes all entries where `(k1, k2) >= (start_k1, start_k2)` and + /// `(k1, k2) <= (end_k1, end_k2)`. + fn delete_range( + &mut self, + start_k1: &[u8], + start_k2: &[u8], + end_k1: &[u8], + end_k2: &[u8], + ) -> Result<(), E> { + let Some((k1, k2, _)) = self.next_dual_above(start_k1, start_k2)? else { + return Ok(()); + }; + + // Check if first entry is past end of range + if k1.as_ref() > end_k1 || (k1.as_ref() == end_k1 && k2.as_ref() > end_k2) { + return Ok(()); + } + + self.delete_current()?; + + while let Some((k1, k2, _)) = self.read_next()? { + // Check if we're past end of range + if k1.as_ref() > end_k1 || (k1.as_ref() == end_k1 && k2.as_ref() > end_k2) { + break; + } + self.delete_current()?; + } + + Ok(()) + } } // ============================================================================ @@ -183,51 +334,30 @@ pub trait TableTraverse: KvTraverse { KvTraverse::read_prev(self)?.map(T::decode_kv_tuple).transpose().map_err(Into::into) } - /// Iterate entries starting from a key while a predicate holds. - /// - /// Positions the cursor at `start_key` and calls `f` for each entry - /// while `predicate` returns true. + /// Position at `key` and return iterator over subsequent entries. /// - /// Returns `Ok(())` on successful completion, or the first error encountered. - fn for_each_while(&mut self, start_key: &T::Key, predicate: P, mut f: F) -> Result<(), E> + /// The iterator starts at the first entry with key >= `key`. + fn iter_from( + &mut self, + key: &T::Key, + ) -> Result, E>> + '_, E> where - P: Fn(&T::Key, &T::Value) -> bool, - F: FnMut(T::Key, T::Value) -> Result<(), E>, + Self: Sized, { - let Some((k, v)) = TableTraverse::lower_bound(self, start_key)? else { - return Ok(()); - }; - - if !predicate(&k, &v) { - return Ok(()); - } - - f(k, v)?; - - while let Some((k, v)) = TableTraverse::read_next(self)? { - if !predicate(&k, &v) { - break; - } - f(k, v)?; - } - - Ok(()) + // Position the cursor at the key + TableTraverse::::lower_bound(self, key)?; + // Return iterator that decodes from raw + Ok(KvTraverse::iter(self)? + .map(|r| r.and_then(|kv| T::decode_kv_tuple(kv).map_err(Into::into)))) } - /// Collect entries from start_key while predicate holds. - /// - /// This is useful when you need to process entries after iteration completes - /// or when the closure would need to borrow mutably from multiple sources. - fn collect_while

(&mut self, start_key: &T::Key, predicate: P) -> Result>, E> + /// Position at first entry and return iterator over all entries. + fn iter(&mut self) -> Result, E>> + '_, E> where - P: Fn(&T::Key, &T::Value) -> bool, + Self: Sized, { - let mut result = Vec::new(); - self.for_each_while(start_key, predicate, |k, v| { - result.push((k, v)); - Ok(()) - })?; - Ok(result) + Ok(KvTraverse::iter(self)? + .map(|r| r.and_then(|kv| T::decode_kv_tuple(kv).map_err(Into::into)))) } } @@ -247,7 +377,7 @@ pub trait TableTraverseMut: KvTraverseMut { KvTraverseMut::delete_current(self) } - /// Delete a range of key-value pairs. + /// Delete a range of key-value pairs (exclusive end). fn delete_range(&mut self, range: Range) -> Result<(), E> { let mut start_key_buf = [0u8; MAX_KEY_SIZE]; let mut end_key_buf = [0u8; MAX_KEY_SIZE]; @@ -256,6 +386,55 @@ pub trait TableTraverseMut: KvTraverseMut { KvTraverseMut::delete_range(self, start_key_bytes..end_key_bytes) } + + /// Delete a range of key-value pairs (inclusive end). + fn delete_range_inclusive(&mut self, range: RangeInclusive) -> Result<(), E> + where + Self: Sized, + { + let Some((key, _)) = TableTraverse::::lower_bound(self, range.start())? else { + return Ok(()); + }; + if !range.contains(&key) { + return Ok(()); + } + KvTraverseMut::delete_current(self)?; + + while let Some((key, _)) = TableTraverse::::read_next(self)? { + if !range.contains(&key) { + break; + } + KvTraverseMut::delete_current(self)?; + } + Ok(()) + } + + /// Delete a range of key-value pairs and return the removed entries. + fn take_range(&mut self, range: RangeInclusive) -> Result>, E> + where + Self: Sized, + { + let mut result = Vec::new(); + + let Some((key, value)) = TableTraverse::::lower_bound(self, range.start())? else { + return Ok(result); + }; + if !range.contains(&key) { + return Ok(result); + } + result.push((key, value)); + KvTraverseMut::delete_current(self)?; + + while let Some((key, value)) = TableTraverse::::read_next(self)? { + if !range.contains(&key) { + break; + } + result.push((key, value)); + KvTraverseMut::delete_current(self)?; + } + + Ok(result) + } } /// Blanket implementation of [`TableTraverseMut`] for any cursor that implements [`KvTraverseMut`]. @@ -324,77 +503,34 @@ pub trait DualTableTraverse: DualKeyTraverse { /// Move to the PREVIOUS key2 entry for the CURRENT key1. fn previous_k2(&mut self) -> Result>, E>; - /// Iterate entries (crossing k1 boundaries) while a predicate holds. - /// - /// Positions the cursor at `(key1, start_k2)` and calls `f` for each entry - /// while `predicate` returns true. Uses `read_next()` to cross k1 boundaries. + /// Position at (k1, k2) and iterate forward, crossing k1 boundaries. /// - /// Returns `Ok(())` on successful completion, or the first error encountered. - fn for_each_while( + /// The iterator starts at the first entry with (k1, k2) >= the specified + /// keys and continues through all subsequent entries. + fn iter_from( &mut self, - key1: &T::Key, - start_k2: &T::Key2, - predicate: P, - mut f: F, - ) -> Result<(), E> + k1: &T::Key, + k2: &T::Key2, + ) -> Result, E>> + '_, E> where - P: Fn(&T::Key, &T::Key2, &T::Value) -> bool, - F: FnMut(T::Key, T::Key2, T::Value) -> Result<(), E>, - { - let Some((k1, k2, v)) = DualTableTraverse::next_dual_above(self, key1, start_k2)? else { - return Ok(()); - }; - - if !predicate(&k1, &k2, &v) { - return Ok(()); - } - - f(k1, k2, v)?; - - while let Some((k1, k2, v)) = DualTableTraverse::read_next(self)? { - if !predicate(&k1, &k2, &v) { - break; - } - f(k1, k2, v)?; - } + T::Key: PartialEq; - Ok(()) - } + /// Position at first entry and return iterator over all entries. + fn iter(&mut self) -> Result, E>> + '_, E> + where + T::Key: PartialEq; - /// Iterate entries within the same k1 while a predicate holds. + /// Iterate k2 entries within a single k1. /// - /// Positions the cursor at `(key1, start_k2)` and calls `f` for each entry - /// while `predicate` returns true. Uses `next_k2()` which stays within - /// the same k1 value. - /// - /// Returns `Ok(())` on successful completion, or the first error encountered. - fn for_each_while_k2( + /// The iterator starts at the first entry with (k1, k2) >= the specified + /// keys and stops when k1 changes or the table is exhausted. + fn iter_k2( &mut self, - key1: &T::Key, + k1: &T::Key, start_k2: &T::Key2, - predicate: P, - f: F, - ) -> Result<(), E> - where - P: Fn(&T::Key, &T::Key2, &T::Value) -> bool, - F: FnMut(T::Key, T::Key2, T::Value) -> Result<(), E>, - { - self.for_each_while(key1, start_k2, |k, k2, v| key1 == k && predicate(k, k2, v), f) - } - - /// Iterate all k2 entries for a given k1, starting from `start_k2`. - /// - /// Calls `f` for each (k1, k2, v) tuple where k1 matches the provided key1 - /// and k2 >= start_k2. Stops when k1 changes or the table is exhausted. - /// - /// Returns `Ok(())` on successful completion, or the first error encountered. - fn for_each_k2(&mut self, key1: &T::Key, start_k2: &T::Key2, f: F) -> Result<(), E> + ) -> Result, E>> + '_, E> where - T::Key: PartialEq, - F: FnMut(T::Key, T::Key2, T::Value) -> Result<(), E>, - { - self.for_each_while_k2(key1, start_k2, |_, _, _| true, f) - } + T::Key: PartialEq; } impl DualTableTraverse for C @@ -444,6 +580,337 @@ where fn previous_k2(&mut self) -> Result>, E> { DualKeyTraverse::previous_k2(self)?.map(T::decode_kkv_tuple).transpose().map_err(Into::into) } + + fn iter_from( + &mut self, + k1: &T::Key, + k2: &T::Key2, + ) -> Result, E>> + '_, E> + where + T::Key: PartialEq, + { + // Position cursor at (k1, k2) + DualTableTraverse::::next_dual_above(self, k1, k2)?; + // Return iterator that decodes from raw + Ok(DualKeyTraverse::iter(self)? + .map(|r| r.and_then(|kkv| T::decode_kkv_tuple(kkv).map_err(Into::into)))) + } + + fn iter(&mut self) -> Result, E>> + '_, E> + where + T::Key: PartialEq, + { + Ok(DualKeyTraverse::iter(self)? + .map(|r| r.and_then(|kkv| T::decode_kkv_tuple(kkv).map_err(Into::into)))) + } + + fn iter_k2( + &mut self, + k1: &T::Key, + start_k2: &T::Key2, + ) -> Result, E>> + '_, E> + where + T::Key: PartialEq, + { + // Position cursor and get the target k1 for comparison + let entry = DualTableTraverse::::next_dual_above(self, k1, start_k2)?; + let Some((found_k1, _, _)) = entry else { + return Ok(DualTableK2Iter::<'_, C, T, E> { + cursor: self, + k1: k1.clone(), + done: true, + _marker: PhantomData, + }); + }; + // If the found k1 doesn't match, we're done + let done = found_k1 != *k1; + Ok(DualTableK2Iter::<'_, C, T, E> { + cursor: self, + k1: found_k1, + done, + _marker: PhantomData, + }) + } +} + +/// Extension trait for typed dual table traversal with mutation capabilities. +pub trait DualTableTraverseMut: DualKeyTraverseMut { + /// Delete all K2 entries for the specified K1. + fn clear_k1(&mut self, key1: &T::Key) -> Result<(), E> { + let mut key1_buf = [0u8; MAX_KEY_SIZE]; + let key1_bytes = key1.encode_key(&mut key1_buf); + DualKeyTraverseMut::clear_k1(self, key1_bytes) + } + + /// Delete the current dual-keyed entry. + fn delete_current(&mut self) -> Result<(), E> { + DualKeyTraverseMut::delete_current(self) + } + + /// Delete a range of dual-keyed entries (inclusive). + fn delete_range(&mut self, range: RangeInclusive<(T::Key, T::Key2)>) -> Result<(), E> + where + Self: Sized, + { + let (start_k1, start_k2) = range.start(); + let (end_k1, end_k2) = range.end(); + + // Loop using next_dual_above to handle backends where delete auto-advances + // the cursor (e.g., MDBX) vs those that don't (e.g., in-memory). + loop { + let Some((k1, k2, _)) = + DualTableTraverse::::next_dual_above(self, start_k1, start_k2)? + else { + break; + }; + + // Check if entry is past end of range (typed comparison) + if &k1 > end_k1 || (&k1 == end_k1 && &k2 > end_k2) { + break; + } + + DualKeyTraverseMut::delete_current(self)?; + } + + Ok(()) + } + + /// Delete a range of dual-keyed entries and return the removed entries. + fn take_range( + &mut self, + range: RangeInclusive<(T::Key, T::Key2)>, + ) -> Result>, E> + where + Self: Sized, + { + let (start_k1, start_k2) = range.start(); + let (end_k1, end_k2) = range.end(); + + let mut result = Vec::new(); + + // Loop using next_dual_above to handle backends where delete auto-advances + // the cursor (e.g., MDBX) vs those that don't (e.g., in-memory). + loop { + let Some((k1, k2, value)) = + DualTableTraverse::::next_dual_above(self, start_k1, start_k2)? + else { + break; + }; + + // Check if entry is past end of range (typed comparison) + if &k1 > end_k1 || (&k1 == end_k1 && &k2 > end_k2) { + break; + } + + result.push((k1, k2, value)); + DualKeyTraverseMut::delete_current(self)?; + } + + Ok(result) + } +} + +/// Blanket implementation of `DualTableTraverseMut`. +impl DualTableTraverseMut for C +where + C: DualKeyTraverseMut, + T: DualKey, + E: HotKvReadError, +{ +} + +// ============================================================================ +// Raw Iterator Structs (for base traits) +// ============================================================================ + +/// Default forward iterator over raw key-value entries. +/// +/// This iterator wraps a cursor implementing `KvTraverse` and yields entries +/// by calling `read_next` on each iteration. +pub struct RawKvIter<'a, C, E> { + cursor: &'a mut C, + done: bool, + _marker: PhantomData E>, +} + +impl<'a, C, E> Iterator for RawKvIter<'a, C, E> +where + C: KvTraverse, + E: HotKvReadError, +{ + type Item = Result, E>; + + fn next(&mut self) -> Option { + if self.done { + return None; + } + // SAFETY: We're using a mutable borrow of the cursor, so the lifetime + // of the returned data is correctly tied to this iterator. + // We transmute the lifetime because the borrow checker can't see + // that the cursor's internal state keeps the data alive. + let result = unsafe { + std::mem::transmute::< + Result>, E>, + Result>, E>, + >(self.cursor.read_next()) + }; + match result { + Ok(Some(kv)) => Some(Ok(kv)), + Ok(None) => { + self.done = true; + None + } + Err(e) => { + self.done = true; + Some(Err(e)) + } + } + } +} + +/// Default forward iterator over raw dual-keyed entries. +/// +/// This iterator wraps a cursor implementing `DualKeyTraverse` and yields +/// entries by calling `read_next` on each iteration. +pub struct RawDualKeyIter<'a, C, E> { + cursor: &'a mut C, + done: bool, + _marker: PhantomData E>, +} + +impl<'a, C, E> Iterator for RawDualKeyIter<'a, C, E> +where + C: DualKeyTraverse, + E: HotKvReadError, +{ + type Item = Result, E>; + + fn next(&mut self) -> Option { + if self.done { + return None; + } + // SAFETY: Same rationale as RawKvIter - the cursor's internal state + // keeps the data alive for the duration of iteration. + let result = unsafe { + std::mem::transmute::< + Result>, E>, + Result>, E>, + >(self.cursor.read_next()) + }; + match result { + Ok(Some(kv)) => Some(Ok(kv)), + Ok(None) => { + self.done = true; + None + } + Err(e) => { + self.done = true; + Some(Err(e)) + } + } + } +} + +/// Default forward iterator over raw dual-keyed entries within a single k1. +/// +/// This iterator wraps a cursor implementing `DualKeyTraverse` and yields +/// entries while k1 remains unchanged. +pub struct RawDualKeyK2Iter<'a, C, E> { + cursor: &'a mut C, + k1: Vec, + done: bool, + _marker: PhantomData E>, +} + +impl<'a, C, E> Iterator for RawDualKeyK2Iter<'a, C, E> +where + C: DualKeyTraverse, + E: HotKvReadError, +{ + type Item = Result, E>; + + fn next(&mut self) -> Option { + if self.done { + return None; + } + // SAFETY: Same rationale as RawKvIter + let result = unsafe { + std::mem::transmute::< + Result>, E>, + Result>, E>, + >(self.cursor.read_next()) + }; + match result { + Ok(Some((k1, k2, v))) => { + if k1.as_ref() != self.k1.as_slice() { + self.done = true; + None + } else { + Some(Ok((k1, k2, v))) + } + } + Ok(None) => { + self.done = true; + None + } + Err(e) => { + self.done = true; + Some(Err(e)) + } + } + } +} + +// ============================================================================ +// Typed Iterator Structs (for extension traits) +// ============================================================================ + +/// Forward iterator over k2 entries within a single k1. +/// +/// This iterator wraps a cursor and yields entries while k1 remains equal +/// to the initial k1 value. +pub struct DualTableK2Iter<'a, C, T, E> +where + T: DualKey, +{ + cursor: &'a mut C, + k1: T::Key, + done: bool, + _marker: PhantomData E>, +} + +impl<'a, C, T, E> Iterator for DualTableK2Iter<'a, C, T, E> +where + C: DualKeyTraverse, + T: DualKey, + T::Key: PartialEq, + E: HotKvReadError, +{ + type Item = Result, E>; + + fn next(&mut self) -> Option { + if self.done { + return None; + } + match DualTableTraverse::::read_next(self.cursor) { + Ok(Some((k1, k2, v))) => { + if k1 != self.k1 { + self.done = true; + None + } else { + Some(Ok((k1, k2, v))) + } + } + Ok(None) => { + self.done = true; + None + } + Err(e) => { + self.done = true; + Some(Err(e)) + } + } + } } // ============================================================================ @@ -519,28 +986,19 @@ where TableTraverse::::read_prev(&mut self.inner) } - /// Iterate entries starting from a key while a predicate holds. + /// Position at `key` and return iterator over subsequent entries. /// - /// Positions the cursor at `start_key` and calls `f` for each entry - /// while `predicate` returns true. - pub fn for_each_while(&mut self, start_key: &T::Key, predicate: P, f: F) -> Result<(), E> - where - P: Fn(&T::Key, &T::Value) -> bool, - F: FnMut(T::Key, T::Value) -> Result<(), E>, - { - TableTraverse::::for_each_while(&mut self.inner, start_key, predicate, f) + /// The iterator starts at the first entry with key >= `key`. + pub fn iter_from( + &mut self, + key: &T::Key, + ) -> Result, E>> + '_, E> { + TableTraverse::::iter_from(&mut self.inner, key) } - /// Collect entries from start_key while predicate holds. - pub fn collect_while

( - &mut self, - start_key: &T::Key, - predicate: P, - ) -> Result>, E> - where - P: Fn(&T::Key, &T::Value) -> bool, - { - TableTraverse::::collect_while(&mut self.inner, start_key, predicate) + /// Position at first entry and return iterator over all entries. + pub fn iter(&mut self) -> Result, E>> + '_, E> { + TableTraverse::::iter(&mut self.inner) } } @@ -555,10 +1013,20 @@ where TableTraverseMut::::delete_current(&mut self.inner) } - /// Delete a range of key-value pairs. + /// Delete a range of key-value pairs (exclusive end). pub fn delete_range(&mut self, range: Range) -> Result<(), E> { TableTraverseMut::::delete_range(&mut self.inner, range) } + + /// Delete a range of key-value pairs (inclusive end). + pub fn delete_range_inclusive(&mut self, range: RangeInclusive) -> Result<(), E> { + TableTraverseMut::::delete_range_inclusive(&mut self.inner, range) + } + + /// Delete a range of key-value pairs and return the removed entries. + pub fn take_range(&mut self, range: RangeInclusive) -> Result>, E> { + TableTraverseMut::::take_range(&mut self.inner, range) + } } /// A wrapper struct for typed dual-keyed table traversal. @@ -666,65 +1134,72 @@ where pub fn read_prev(&mut self) -> Result>, E> { DualTableTraverse::::read_prev(&mut self.inner) } +} - /// Iterate all k2 entries for a given k1, starting from `start_k2`. +// Iterator methods for DualTableCursor - require T::Key: PartialEq for k2 iteration +impl DualTableCursor +where + C: DualTableTraverse, + T: DualKey, + T::Key: PartialEq, + E: HotKvReadError, +{ + /// Position at (k1, k2) and iterate forward, crossing k1 boundaries. /// - /// Calls `f` for each (k1, k2, v) tuple where k1 matches the provided key1 - /// and k2 >= start_k2. Stops when k1 changes or the table is exhausted. - pub fn for_each_k2(&mut self, key1: &T::Key, start_k2: &T::Key2, f: F) -> Result<(), E> - where - T::Key: PartialEq, - F: FnMut(T::Key, T::Key2, T::Value) -> Result<(), E>, - { - DualTableTraverse::::for_each_k2(&mut self.inner, key1, start_k2, f) + /// The iterator starts at the first entry with (k1, k2) >= the specified + /// keys and continues through all subsequent entries. + pub fn iter_from( + &mut self, + k1: &T::Key, + k2: &T::Key2, + ) -> Result, E>> + '_, E> { + DualTableTraverse::::iter_from(&mut self.inner, k1, k2) } - /// Iterate entries within the same k1 while a predicate holds. - /// - /// Positions the cursor at `(key1, start_k2)` and calls `f` for each entry - /// while `predicate` returns true. Uses `next_k2()` which stays within - /// the same k1 value. - pub fn for_each_while_k2( - &mut self, - key1: &T::Key, - start_k2: &T::Key2, - predicate: P, - f: F, - ) -> Result<(), E> - where - P: Fn(&T::Key, &T::Key2, &T::Value) -> bool, - F: FnMut(T::Key, T::Key2, T::Value) -> Result<(), E>, - { - DualTableTraverse::::for_each_while_k2(&mut self.inner, key1, start_k2, predicate, f) + /// Position at first entry and return iterator over all entries. + pub fn iter(&mut self) -> Result, E>> + '_, E> { + DualTableTraverse::::iter(&mut self.inner) } - /// Iterate entries (crossing k1 boundaries) while a predicate holds. + /// Iterate k2 entries within a single k1. /// - /// Positions the cursor at `(key1, start_k2)` and calls `f` for each entry - /// while `predicate` returns true. Uses `read_next()` to cross k1 boundaries. - pub fn for_each_while( + /// The iterator starts at the first entry with (k1, k2) >= the specified + /// keys and stops when k1 changes or the table is exhausted. + pub fn iter_k2( &mut self, - key1: &T::Key, + k1: &T::Key, start_k2: &T::Key2, - predicate: P, - f: F, - ) -> Result<(), E> - where - P: Fn(&T::Key, &T::Key2, &T::Value) -> bool, - F: FnMut(T::Key, T::Key2, T::Value) -> Result<(), E>, - { - DualTableTraverse::::for_each_while(&mut self.inner, key1, start_k2, predicate, f) + ) -> Result, E>> + '_, E> { + DualTableTraverse::::iter_k2(&mut self.inner, k1, start_k2) } } impl DualTableCursor where - C: KvTraverseMut, + C: DualTableTraverseMut, T: DualKey, E: HotKvReadError, { /// Delete the current key-value pair. pub fn delete_current(&mut self) -> Result<(), E> { - KvTraverseMut::delete_current(&mut self.inner) + DualTableTraverseMut::::delete_current(&mut self.inner) + } + + /// Delete all K2 entries for the specified K1. + pub fn clear_k1(&mut self, key1: &T::Key) -> Result<(), E> { + DualTableTraverseMut::::clear_k1(&mut self.inner, key1) + } + + /// Delete a range of dual-keyed entries (inclusive). + pub fn delete_range(&mut self, range: RangeInclusive<(T::Key, T::Key2)>) -> Result<(), E> { + DualTableTraverseMut::::delete_range(&mut self.inner, range) + } + + /// Delete a range of dual-keyed entries and return the removed entries. + pub fn take_range( + &mut self, + range: RangeInclusive<(T::Key, T::Key2)>, + ) -> Result>, E> { + DualTableTraverseMut::::take_range(&mut self.inner, range) } } From f0e78c47183fa531c72f65a6f7cb05262cf8faba Mon Sep 17 00:00:00 2001 From: James Date: Thu, 29 Jan 2026 18:08:46 -0500 Subject: [PATCH 3/6] refactor: make iter_k2 more sensible --- TODOS | 1 + crates/hot/src/db/inconsistent.rs | 4 +- crates/hot/src/model/mod.rs | 2 +- crates/hot/src/model/traverse.rs | 127 +++++++++++++++--------------- 4 files changed, 66 insertions(+), 68 deletions(-) create mode 100644 TODOS diff --git a/TODOS b/TODOS new file mode 100644 index 0000000..c2230c0 --- /dev/null +++ b/TODOS @@ -0,0 +1 @@ +- Append optimization for tables keyed by blocknumbers \ No newline at end of file diff --git a/crates/hot/src/db/inconsistent.rs b/crates/hot/src/db/inconsistent.rs index 0691db4..e4af776 100644 --- a/crates/hot/src/db/inconsistent.rs +++ b/crates/hot/src/db/inconsistent.rs @@ -148,8 +148,8 @@ pub trait UnsafeHistoryWrite: UnsafeDbWrite + HistoryRead { fn write_wipe(&self, block_number: u64, address: &Address) -> Result<(), Self::Error> { let mut cursor = self.traverse_dual::()?; - for entry in cursor.iter_k2(address, &U256::ZERO)? { - let (_addr, slot, value) = entry?; + for entry in cursor.iter_k2(address)? { + let (slot, value) = entry?; self.write_storage_prestate(block_number, *address, &slot, &value)?; } Ok(()) diff --git a/crates/hot/src/model/mod.rs b/crates/hot/src/model/mod.rs index 7f67903..f484972 100644 --- a/crates/hot/src/model/mod.rs +++ b/crates/hot/src/model/mod.rs @@ -46,7 +46,7 @@ pub use traits::{HotKv, HotKvRead, HotKvWrite}; mod traverse; pub use traverse::{ DualKeyTraverse, DualKeyTraverseMut, DualTableCursor, DualTableTraverse, DualTableTraverseMut, - KvTraverse, KvTraverseMut, TableCursor, TableTraverse, TableTraverseMut, + K2Value, KvTraverse, KvTraverseMut, RawK2Value, TableCursor, TableTraverse, TableTraverseMut, }; use crate::tables::{DualKey, Table}; diff --git a/crates/hot/src/model/traverse.rs b/crates/hot/src/model/traverse.rs index 613acd8..2388dbb 100644 --- a/crates/hot/src/model/traverse.rs +++ b/crates/hot/src/model/traverse.rs @@ -6,8 +6,18 @@ use crate::{ tables::{DualKey, SingleKey}, }; use core::marker::PhantomData; +use std::borrow::Cow; use std::ops::{Range, RangeInclusive}; +/// Raw k2-value pair: (key2, value). +/// +/// This is returned by `iter_k2()` on dual-keyed tables. The caller already +/// knows k1 (they passed it in), so we don't return it redundantly. +pub type RawK2Value<'a> = (Cow<'a, [u8]>, RawValue<'a>); + +/// Typed k2-value pair for a dual-keyed table. +pub type K2Value = (::Key2, ::Value); + /// Trait for traversing key-value pairs in the database. pub trait KvTraverse { /// Set position to the first key-value pair in the database, and return @@ -212,30 +222,29 @@ pub trait DualKeyTraverse { Ok(RawDualKeyIter { cursor: self, done: false, _marker: PhantomData }) } - /// Iterate k2 entries within a single k1. + /// Iterate all k2 entries within a single k1. /// - /// The iterator starts at the first entry with (k1, k2) >= the specified - /// keys and stops when k1 changes or the table is exhausted. + /// The iterator yields `(k2, value)` pairs for the specified k1, starting + /// from the first k2 value, and stops when k1 changes or the table is + /// exhausted. + /// + /// Note: k1 is not included in the output since the caller already knows + /// it (they passed it in). This avoids redundant allocations. fn iter_k2<'a>( &'a mut self, k1: &[u8], - start_k2: &[u8], - ) -> Result, E>> + 'a, E> + ) -> Result, E>> + 'a, E> where Self: Sized, { - let entry = self.next_dual_above(k1, start_k2)?; + // Position at first entry for this k1 (using empty slice as minimum k2) + let entry = self.next_dual_above(k1, &[])?; let Some((found_k1, _, _)) = entry else { - return Ok(RawDualKeyK2Iter { - cursor: self, - k1: Vec::new(), - done: true, - _marker: PhantomData, - }); + return Ok(RawDualKeyK2Iter { cursor: self, done: true, _marker: PhantomData }); }; // If the found k1 doesn't match, we're done let done = found_k1.as_ref() != k1; - Ok(RawDualKeyK2Iter { cursor: self, k1: k1.to_vec(), done, _marker: PhantomData }) + Ok(RawDualKeyK2Iter { cursor: self, done, _marker: PhantomData }) } } @@ -520,15 +529,18 @@ pub trait DualTableTraverse: DualKeyTraverse { where T::Key: PartialEq; - /// Iterate k2 entries within a single k1. + /// Iterate all k2 entries within a single k1. /// - /// The iterator starts at the first entry with (k1, k2) >= the specified - /// keys and stops when k1 changes or the table is exhausted. + /// The iterator yields `(k2, value)` pairs for the specified k1, starting + /// from the first k2 value, and stops when k1 changes or the table is + /// exhausted. + /// + /// Note: k1 is not included in the output since the caller already knows + /// it (they passed it in). This avoids redundant allocations. fn iter_k2( &mut self, k1: &T::Key, - start_k2: &T::Key2, - ) -> Result, E>> + '_, E> + ) -> Result, E>> + '_, E> where T::Key: PartialEq; } @@ -607,29 +619,26 @@ where fn iter_k2( &mut self, k1: &T::Key, - start_k2: &T::Key2, - ) -> Result, E>> + '_, E> + ) -> Result, E>> + '_, E> where T::Key: PartialEq, { - // Position cursor and get the target k1 for comparison - let entry = DualTableTraverse::::next_dual_above(self, k1, start_k2)?; + // Position cursor at first entry for this k1 using raw interface with empty k2 + let mut key1_buf = [0u8; MAX_KEY_SIZE]; + let key1_bytes = k1.encode_key(&mut key1_buf); + let entry = DualKeyTraverse::next_dual_above(self, key1_bytes, &[])?; let Some((found_k1, _, _)) = entry else { return Ok(DualTableK2Iter::<'_, C, T, E> { cursor: self, - k1: k1.clone(), done: true, _marker: PhantomData, }); }; + // Decode the found k1 to check if it matches + let decoded_k1 = T::decode_key(found_k1)?; // If the found k1 doesn't match, we're done - let done = found_k1 != *k1; - Ok(DualTableK2Iter::<'_, C, T, E> { - cursor: self, - k1: found_k1, - done, - _marker: PhantomData, - }) + let done = decoded_k1 != *k1; + Ok(DualTableK2Iter::<'_, C, T, E> { cursor: self, done, _marker: PhantomData }) } } @@ -811,13 +820,13 @@ where } } -/// Default forward iterator over raw dual-keyed entries within a single k1. +/// Default forward iterator over raw k2-value entries within a single k1. /// /// This iterator wraps a cursor implementing `DualKeyTraverse` and yields -/// entries while k1 remains unchanged. +/// `(k2, value)` pairs while k1 remains unchanged. The iterator stops when k1 +/// changes or the table is exhausted. pub struct RawDualKeyK2Iter<'a, C, E> { cursor: &'a mut C, - k1: Vec, done: bool, _marker: PhantomData E>, } @@ -827,7 +836,7 @@ where C: DualKeyTraverse, E: HotKvReadError, { - type Item = Result, E>; + type Item = Result, E>; fn next(&mut self) -> Option { if self.done { @@ -838,17 +847,10 @@ where std::mem::transmute::< Result>, E>, Result>, E>, - >(self.cursor.read_next()) + >(self.cursor.next_k2()) }; match result { - Ok(Some((k1, k2, v))) => { - if k1.as_ref() != self.k1.as_slice() { - self.done = true; - None - } else { - Some(Ok((k1, k2, v))) - } - } + Ok(Some((_k1, k2, v))) => Some(Ok((k2, v))), Ok(None) => { self.done = true; None @@ -865,42 +867,34 @@ where // Typed Iterator Structs (for extension traits) // ============================================================================ -/// Forward iterator over k2 entries within a single k1. +/// Forward iterator over k2-value pairs within a single k1. /// -/// This iterator wraps a cursor and yields entries while k1 remains equal -/// to the initial k1 value. +/// This iterator wraps a cursor and yields `(k2, value)` pairs by calling +/// `next_k2()` on each iteration. The iterator stops when there are no more +/// k2 entries for the current k1 or when an error occurs. pub struct DualTableK2Iter<'a, C, T, E> where T: DualKey, { cursor: &'a mut C, - k1: T::Key, done: bool, - _marker: PhantomData E>, + _marker: PhantomData (T, E)>, } impl<'a, C, T, E> Iterator for DualTableK2Iter<'a, C, T, E> where C: DualKeyTraverse, T: DualKey, - T::Key: PartialEq, E: HotKvReadError, { - type Item = Result, E>; + type Item = Result, E>; fn next(&mut self) -> Option { if self.done { return None; } - match DualTableTraverse::::read_next(self.cursor) { - Ok(Some((k1, k2, v))) => { - if k1 != self.k1 { - self.done = true; - None - } else { - Some(Ok((k1, k2, v))) - } - } + match DualTableTraverse::::next_k2(self.cursor) { + Ok(Some((_k1, k2, v))) => Some(Ok((k2, v))), Ok(None) => { self.done = true; None @@ -1161,16 +1155,19 @@ where DualTableTraverse::::iter(&mut self.inner) } - /// Iterate k2 entries within a single k1. + /// Iterate all k2 entries within a single k1. /// - /// The iterator starts at the first entry with (k1, k2) >= the specified - /// keys and stops when k1 changes or the table is exhausted. + /// The iterator yields `(k2, value)` pairs for the specified k1, starting + /// from the first k2 value, and stops when k1 changes or the table is + /// exhausted. + /// + /// Note: k1 is not included in the output since the caller already knows + /// it (they passed it in). This avoids redundant allocations. pub fn iter_k2( &mut self, k1: &T::Key, - start_k2: &T::Key2, - ) -> Result, E>> + '_, E> { - DualTableTraverse::::iter_k2(&mut self.inner, k1, start_k2) + ) -> Result, E>> + '_, E> { + DualTableTraverse::::iter_k2(&mut self.inner, k1) } } From 40849ef11f67a99fc1dc0ff9ebdc7a2c86d2c18c Mon Sep 17 00:00:00 2001 From: James Date: Thu, 29 Jan 2026 22:30:42 -0500 Subject: [PATCH 4/6] refactor: propagate iter changes from libmdbx --- Cargo.toml | 2 +- crates/hot-mdbx/src/cursor.rs | 61 ++++++++++++++++++++-- crates/hot/src/mem.rs | 87 +++++++++++++++++++++++++++++++- crates/hot/src/model/mod.rs | 5 +- crates/hot/src/model/traverse.rs | 79 +++++++++++++++++++++++++++++ crates/hot/src/ser/traits.rs | 2 +- crates/types/src/sharded.rs | 2 +- 7 files changed, 228 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0144a41..630c17a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,7 @@ signet-storage = { version = "0.0.1", path = "./crates/storage" } signet-storage-types = { version = "0.0.1", path = "./crates/types" } # External, in-house -signet-libmdbx = { version = "0.7.0" } +signet-libmdbx = { version = "0.8.0" } signet-zenith = "0.16.0-rc.5" diff --git a/crates/hot-mdbx/src/cursor.rs b/crates/hot-mdbx/src/cursor.rs index e126962..4a48a4a 100644 --- a/crates/hot-mdbx/src/cursor.rs +++ b/crates/hot-mdbx/src/cursor.rs @@ -4,11 +4,11 @@ use crate::{FixedSizeInfo, MdbxError}; use signet_hot::{ MAX_FIXED_VAL_SIZE, MAX_KEY_SIZE, model::{ - DualKeyTraverse, DualKeyTraverseMut, KvTraverse, KvTraverseMut, RawDualKeyValue, - RawKeyValue, RawValue, + DualKeyItem, DualKeyTraverse, DualKeyTraverseMut, KvTraverse, KvTraverseMut, + RawDualKeyItem, RawDualKeyValue, RawKeyValue, RawValue, }, }; -use signet_libmdbx::{Ro, Rw, RwSync, TransactionKind, tx::WriteMarker}; +use signet_libmdbx::{DupItem, Ro, Rw, RwSync, TransactionKind, tx::WriteMarker}; use std::{ borrow::Cow, ops::{Deref, DerefMut}, @@ -504,6 +504,61 @@ where None => Ok(None), } } + + fn iter_items( + &mut self, + ) -> Result, MdbxError>> + '_, MdbxError> { + if !self.fsi.is_dupsort() { + return Err(MdbxError::NotDupSort); + } + + let key2_size = self.fsi.key2_size().ok_or(MdbxError::UnknownFixedSize)?; + + // Use iter_dup_start which yields DupItem::NewKey/SameKey + let iter_dup = self.inner.iter_dup_start::, Cow<'_, [u8]>>()?; + Ok(MdbxDualKeyItemIter { iter_dup, key2_size }) + } +} + +/// Iterator adapter that converts mdbx's [`DupItem`] to [`DualKeyItem`]. +/// +/// This iterator returns owned data to avoid lifetime complexities between +/// the transaction lifetime and the iterator borrow lifetime. +struct MdbxDualKeyItemIter<'tx, 'cur, K: TransactionKind> { + iter_dup: signet_libmdbx::tx::iter::IterDup<'tx, 'cur, K, Cow<'tx, [u8]>, Cow<'tx, [u8]>>, + key2_size: usize, +} + +impl<'a, K: TransactionKind> Iterator for MdbxDualKeyItemIter<'_, 'a, K> { + type Item = Result, MdbxError>; + + fn next(&mut self) -> Option { + match self.iter_dup.borrow_next() { + Ok(Some(item)) => { + let result = match item { + DupItem::NewKey(k1, v) => { + let (k2, val) = split_cow_at_owned(v, self.key2_size); + DualKeyItem::NewK1(Cow::Owned(k1.into_owned()), k2, val) + } + DupItem::SameKey(v) => { + let (k2, val) = split_cow_at_owned(v, self.key2_size); + DualKeyItem::SameK1(k2, val) + } + }; + Some(Ok(result)) + } + Ok(None) => None, + Err(e) => Some(Err(MdbxError::from(e))), + } + } +} + +/// Splits a [`Cow`] at the given index and returns owned [`Cow`]s. +#[inline] +fn split_cow_at_owned(cow: Cow<'_, [u8]>, at: usize) -> (Cow<'static, [u8]>, Cow<'static, [u8]>) { + let vec = cow.into_owned(); + let (left, right) = vec.split_at(at); + (Cow::Owned(left.to_vec()), Cow::Owned(right.to_vec())) } impl DualKeyTraverseMut for Cursor<'_, K> { diff --git a/crates/hot/src/mem.rs b/crates/hot/src/mem.rs index 41fbafc..6a6d59c 100644 --- a/crates/hot/src/mem.rs +++ b/crates/hot/src/mem.rs @@ -5,8 +5,9 @@ use crate::{ model::{ - DualKeyTraverse, DualKeyTraverseMut, HotKv, HotKvError, HotKvRead, HotKvReadError, - HotKvWrite, KvTraverse, KvTraverseMut, RawDualKeyValue, RawKeyValue, RawValue, + DualKeyItem, DualKeyTraverse, DualKeyTraverseMut, HotKv, HotKvError, HotKvRead, + HotKvReadError, HotKvWrite, KvTraverse, KvTraverseMut, RawDualKeyItem, RawDualKeyValue, + RawKeyValue, RawValue, }, ser::{DeserError, MAX_KEY_SIZE}, }; @@ -514,6 +515,47 @@ impl<'a> DualKeyTraverse for MemKvCursor<'a> { self.set_current_key(*found_key); Ok(Some((found_k1, found_k2, Cow::Borrowed(value.as_ref())))) } + + fn iter_items( + &mut self, + ) -> Result, MemKvError>> + '_, MemKvError> { + DualKeyTraverse::first(self)?; + Ok(MemDualKeyItemIter { cursor: self, prev_k1: None }) + } +} + +/// Iterator that yields [`DualKeyItem`] for read-only memory cursors. +struct MemDualKeyItemIter<'a, 'b> { + cursor: &'a mut MemKvCursor<'b>, + prev_k1: Option>, +} + +impl<'a> Iterator for MemDualKeyItemIter<'a, '_> { + type Item = Result, MemKvError>; + + fn next(&mut self) -> Option { + let result = DualKeyTraverse::read_next(self.cursor); + match result { + Ok(Some((k1, k2, v))) => { + let k1_vec = k1.as_ref().to_vec(); + let is_new_k1 = self.prev_k1.as_ref() != Some(&k1_vec); + self.prev_k1 = Some(k1_vec); + + let item = if is_new_k1 { + DualKeyItem::NewK1( + Cow::Owned(k1.into_owned()), + Cow::Owned(k2.into_owned()), + Cow::Owned(v.into_owned()), + ) + } else { + DualKeyItem::SameK1(Cow::Owned(k2.into_owned()), Cow::Owned(v.into_owned())) + }; + Some(Ok(item)) + } + Ok(None) => None, + Err(e) => Some(Err(e)), + } + } } /// Memory cursor for read-write operations @@ -1037,6 +1079,47 @@ impl<'a> DualKeyTraverse for MemKvCursorMut<'a> { Cow::Owned(value.to_vec()), ))) } + + fn iter_items( + &mut self, + ) -> Result, MemKvError>> + '_, MemKvError> { + DualKeyTraverse::first(self)?; + Ok(MemDualKeyItemIterMut { cursor: self, prev_k1: None }) + } +} + +/// Iterator that yields [`DualKeyItem`] for read-write memory cursors. +struct MemDualKeyItemIterMut<'a, 'b> { + cursor: &'a mut MemKvCursorMut<'b>, + prev_k1: Option>, +} + +impl<'a> Iterator for MemDualKeyItemIterMut<'a, '_> { + type Item = Result, MemKvError>; + + fn next(&mut self) -> Option { + let result = DualKeyTraverse::read_next(self.cursor); + match result { + Ok(Some((k1, k2, v))) => { + let k1_vec = k1.as_ref().to_vec(); + let is_new_k1 = self.prev_k1.as_ref() != Some(&k1_vec); + self.prev_k1 = Some(k1_vec); + + let item = if is_new_k1 { + DualKeyItem::NewK1( + Cow::Owned(k1.into_owned()), + Cow::Owned(k2.into_owned()), + Cow::Owned(v.into_owned()), + ) + } else { + DualKeyItem::SameK1(Cow::Owned(k2.into_owned()), Cow::Owned(v.into_owned())) + }; + Some(Ok(item)) + } + Ok(None) => None, + Err(e) => Some(Err(e)), + } + } } impl DualKeyTraverseMut for MemKvCursorMut<'_> { diff --git a/crates/hot/src/model/mod.rs b/crates/hot/src/model/mod.rs index f484972..d58c774 100644 --- a/crates/hot/src/model/mod.rs +++ b/crates/hot/src/model/mod.rs @@ -45,8 +45,9 @@ pub use traits::{HotKv, HotKvRead, HotKvWrite}; mod traverse; pub use traverse::{ - DualKeyTraverse, DualKeyTraverseMut, DualTableCursor, DualTableTraverse, DualTableTraverseMut, - K2Value, KvTraverse, KvTraverseMut, RawK2Value, TableCursor, TableTraverse, TableTraverseMut, + DualKeyItem, DualKeyTraverse, DualKeyTraverseMut, DualTableCursor, DualTableTraverse, + DualTableTraverseMut, K2Value, KvTraverse, KvTraverseMut, RawDualKeyItem, RawK2Value, + TableCursor, TableTraverse, TableTraverseMut, }; use crate::tables::{DualKey, Table}; diff --git a/crates/hot/src/model/traverse.rs b/crates/hot/src/model/traverse.rs index 2388dbb..5febb69 100644 --- a/crates/hot/src/model/traverse.rs +++ b/crates/hot/src/model/traverse.rs @@ -18,6 +18,72 @@ pub type RawK2Value<'a> = (Cow<'a, [u8]>, RawValue<'a>); /// Typed k2-value pair for a dual-keyed table. pub type K2Value = (::Key2, ::Value); +/// An item from a dual-key iterator. +/// +/// This enum avoids cloning k1 for every value when iterating +/// over dual-keyed tables. K1 is only provided when it changes. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DualKeyItem { + /// First entry for a new k1. + NewK1(K1, K2, V), + /// Additional k2/value for the current k1. + SameK1(K2, V), +} + +impl DualKeyItem { + /// Returns the value, consuming self. + pub fn into_value(self) -> V { + match self { + Self::NewK1(_, _, v) | Self::SameK1(_, v) => v, + } + } + + /// Returns a reference to the value. + pub const fn value(&self) -> &V { + match self { + Self::NewK1(_, _, v) | Self::SameK1(_, v) => v, + } + } + + /// Returns the k2, consuming self. + pub fn into_k2(self) -> K2 { + match self { + Self::NewK1(_, k2, _) | Self::SameK1(k2, _) => k2, + } + } + + /// Returns a reference to k2. + pub const fn k2(&self) -> &K2 { + match self { + Self::NewK1(_, k2, _) | Self::SameK1(k2, _) => k2, + } + } + + /// Returns k1 if this is a NewK1 entry. + pub const fn k1(&self) -> Option<&K1> { + match self { + Self::NewK1(k1, _, _) => Some(k1), + Self::SameK1(_, _) => None, + } + } + + /// Returns true if this item represents a new k1. + pub const fn is_new_k1(&self) -> bool { + matches!(self, Self::NewK1(..)) + } + + /// Convert to a tuple, requiring k1 to be provided for SameK1 variants. + pub fn into_tuple(self, current_k1: K1) -> (K1, K2, V) { + match self { + Self::NewK1(k1, k2, v) => (k1, k2, v), + Self::SameK1(k2, v) => (current_k1, k2, v), + } + } +} + +/// Raw dual-key item: (k1?, k2, value) where k1 is only present on NewK1. +pub type RawDualKeyItem<'a> = DualKeyItem, Cow<'a, [u8]>, Cow<'a, [u8]>>; + /// Trait for traversing key-value pairs in the database. pub trait KvTraverse { /// Set position to the first key-value pair in the database, and return @@ -246,6 +312,19 @@ pub trait DualKeyTraverse { let done = found_k1.as_ref() != k1; Ok(RawDualKeyK2Iter { cursor: self, done, _marker: PhantomData }) } + + /// Position at first entry and return iterator yielding [`DualKeyItem`]. + /// + /// This is more efficient than [`Self::iter()`] as it avoids cloning k1 for + /// every entry within the same k1 group. The iterator yields + /// [`DualKeyItem::NewK1`] when k1 changes and [`DualKeyItem::SameK1`] for + /// subsequent entries with the same k1. + /// + /// Backends implement this natively to leverage efficient "new key" + /// notifications from the underlying database. + fn iter_items(&mut self) -> Result, E>> + '_, E> + where + Self: Sized; } /// Trait for traversing dual-keyed key-value pairs with mutation capabilities. diff --git a/crates/hot/src/ser/traits.rs b/crates/hot/src/ser/traits.rs index b4d0974..25d6cfb 100644 --- a/crates/hot/src/ser/traits.rs +++ b/crates/hot/src/ser/traits.rs @@ -17,7 +17,7 @@ pub const MAX_FIXED_VAL_SIZE: usize = 64; /// /// In practice, keys are often hashes, addresses, numbers, or composites /// of these. -pub trait KeySer: PartialOrd + Ord + Sized + Clone + core::fmt::Debug { +pub trait KeySer: PartialOrd + Ord + Sized + Clone + Copy + core::fmt::Debug { /// The fixed size of the serialized key in bytes. /// Must satisfy `SIZE <= MAX_KEY_SIZE`. const SIZE: usize; diff --git a/crates/types/src/sharded.rs b/crates/types/src/sharded.rs index e5814cd..d4f4dab 100644 --- a/crates/types/src/sharded.rs +++ b/crates/types/src/sharded.rs @@ -4,7 +4,7 @@ /// `Address | 200` -> data is from block 0 to 200. /// /// `Address | 300` -> data is from block 201 to 300. -#[derive(Debug, Default, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct ShardedKey { /// The key for this type. pub key: T, From 0305ea14f7ec95146735bf5f7935dd455f3724c6 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 29 Jan 2026 23:13:54 -0500 Subject: [PATCH 5/6] refactor: traverse module --- Cargo.toml | 1 + crates/hot-mdbx/src/tx.rs | 5 +- crates/hot/Cargo.toml | 1 + crates/hot/src/db/consistent.rs | 34 +- crates/hot/src/db/inconsistent.rs | 21 +- crates/hot/src/db/read.rs | 22 +- crates/hot/src/model/traverse.rs | 1281 ------------------- crates/hot/src/model/traverse/cursor.rs | 310 +++++ crates/hot/src/model/traverse/dual_key.rs | 194 +++ crates/hot/src/model/traverse/dual_table.rs | 284 ++++ crates/hot/src/model/traverse/iter.rs | 190 +++ crates/hot/src/model/traverse/kv.rs | 111 ++ crates/hot/src/model/traverse/mod.rs | 101 ++ crates/hot/src/model/traverse/table.rs | 161 +++ crates/hot/src/model/traverse/types.rs | 79 ++ 15 files changed, 1470 insertions(+), 1325 deletions(-) delete mode 100644 crates/hot/src/model/traverse.rs create mode 100644 crates/hot/src/model/traverse/cursor.rs create mode 100644 crates/hot/src/model/traverse/dual_key.rs create mode 100644 crates/hot/src/model/traverse/dual_table.rs create mode 100644 crates/hot/src/model/traverse/iter.rs create mode 100644 crates/hot/src/model/traverse/kv.rs create mode 100644 crates/hot/src/model/traverse/mod.rs create mode 100644 crates/hot/src/model/traverse/table.rs create mode 100644 crates/hot/src/model/traverse/types.rs diff --git a/Cargo.toml b/Cargo.toml index 630c17a..2f87094 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,7 @@ trevm = { version = "0.33.0", features = ["full_env_cfg"] } # alloy alloy = { version = "1.0.9", default-features = false, features = ["consensus", "rlp"] } +ahash = "0.8" auto_impl = "1.3.0" bytes = "1.11.0" byteorder = "1.5.0" diff --git a/crates/hot-mdbx/src/tx.rs b/crates/hot-mdbx/src/tx.rs index a248089..67c0e55 100644 --- a/crates/hot-mdbx/src/tx.rs +++ b/crates/hot-mdbx/src/tx.rs @@ -44,9 +44,8 @@ impl Tx { key[..to_copy].copy_from_slice(&name.as_bytes()[..to_copy]); let db = self.inner.open_db(None)?; - // Note: We request Vec (owned) since we only need it briefly for decoding - // and don't need zero-copy for this small metadata read. - let data: Vec = self + + let data: [u8; 8] = self .inner .get(db.dbi(), key.as_slice()) .map_err(MdbxError::from)? diff --git a/crates/hot/Cargo.toml b/crates/hot/Cargo.toml index c29b8e6..309230c 100644 --- a/crates/hot/Cargo.toml +++ b/crates/hot/Cargo.toml @@ -15,6 +15,7 @@ trevm.workspace = true alloy.workspace = true +ahash.workspace = true auto_impl.workspace = true bytes.workspace = true itertools.workspace = true diff --git a/crates/hot/src/db/consistent.rs b/crates/hot/src/db/consistent.rs index 730ecc0..061f6e0 100644 --- a/crates/hot/src/db/consistent.rs +++ b/crates/hot/src/db/consistent.rs @@ -2,9 +2,9 @@ use crate::{ db::{HistoryError, UnsafeDbWrite, UnsafeHistoryWrite}, tables, }; +use ahash::AHashSet; use alloy::primitives::{Address, BlockNumber, U256, address}; use signet_storage_types::{BlockNumberList, SealedHeader}; -use std::collections::HashSet; use trevm::revm::database::BundleState; /// Maximum address value (all bits set to 1). @@ -24,13 +24,10 @@ pub trait HistoryWrite: UnsafeDbWrite + UnsafeHistoryWrite { where I: IntoIterator, { - let headers: Vec<_> = headers.into_iter().collect(); - if headers.is_empty() { - return Err(HistoryError::EmptyRange); - } + let mut iter = headers.into_iter(); + let first = iter.next().ok_or(HistoryError::EmptyRange)?; // Validate first header against current DB tip - let first = headers[0]; match self.get_chain_tip().map_err(HistoryError::Db)? { None => { // Empty DB - first block is valid as genesis @@ -52,11 +49,8 @@ pub trait HistoryWrite: UnsafeDbWrite + UnsafeHistoryWrite { } } - // Validate each subsequent header extends the previous - for window in headers.windows(2) { - let prev = window[0]; - let curr = window[1]; - + // Validate each subsequent header extends the previous using fold + iter.try_fold(first, |prev, curr| { let expected_number = prev.number + 1; if curr.number != expected_number { return Err(HistoryError::NonContiguousBlock { @@ -72,7 +66,9 @@ pub trait HistoryWrite: UnsafeDbWrite + UnsafeHistoryWrite { got: curr.parent_hash, }); } - } + + Ok(curr) + })?; Ok(()) } @@ -113,7 +109,8 @@ pub trait HistoryWrite: UnsafeDbWrite + UnsafeHistoryWrite { // ═══════════════════════════════════════════════════════════════════ // 1. STREAM AccountChangeSets → restore + filter history in one pass // ═══════════════════════════════════════════════════════════════════ - let mut seen_accounts: HashSet

= HashSet::new(); + // TODO: estimate capacity from block range size for better allocation + let mut seen_accounts: AHashSet
= AHashSet::new(); let mut account_cursor = self.traverse_dual::()?; // Position at first entry @@ -136,8 +133,8 @@ pub trait HistoryWrite: UnsafeDbWrite + UnsafeHistoryWrite { // Filter history index if let Some((shard_key, list)) = self.last_account_history(address)? { self.queue_delete_dual::(&address, &shard_key)?; - let filtered: Vec = list.iter().filter(|&bn| bn <= block).collect(); - if !filtered.is_empty() { + let mut filtered = list.iter().take_while(|&bn| bn <= block).peekable(); + if filtered.peek().is_some() { self.write_account_history( &address, u64::MAX, @@ -153,7 +150,8 @@ pub trait HistoryWrite: UnsafeDbWrite + UnsafeHistoryWrite { // ═══════════════════════════════════════════════════════════════════ // 2. STREAM StorageChangeSets → restore + filter history in one pass // ═══════════════════════════════════════════════════════════════════ - let mut seen_storage: HashSet<(Address, U256)> = HashSet::new(); + // TODO: estimate capacity from block range size for better allocation + let mut seen_storage: AHashSet<(Address, U256)> = AHashSet::new(); let mut storage_cursor = self.traverse_dual::()?; // Position at first entry @@ -176,8 +174,8 @@ pub trait HistoryWrite: UnsafeDbWrite + UnsafeHistoryWrite { // Filter history index if let Some((shard_key, list)) = self.last_storage_history(&address, &slot)? { self.queue_delete_dual::(&address, &shard_key)?; - let filtered: Vec = list.iter().filter(|&bn| bn <= block).collect(); - if !filtered.is_empty() { + let mut filtered = list.iter().take_while(|&bn| bn <= block).peekable(); + if filtered.peek().is_some() { self.write_storage_history( &address, slot, diff --git a/crates/hot/src/db/inconsistent.rs b/crates/hot/src/db/inconsistent.rs index e4af776..24b0297 100644 --- a/crates/hot/src/db/inconsistent.rs +++ b/crates/hot/src/db/inconsistent.rs @@ -3,13 +3,14 @@ use crate::{ model::HotKvWrite, tables, }; +use ahash::AHashMap; use alloy::{ consensus::Header, primitives::{Address, B256, BlockNumber, U256}, }; use itertools::Itertools; use signet_storage_types::{Account, BlockNumberList, SealedHeader, ShardedKey}; -use std::{collections::HashMap, ops::RangeInclusive}; +use std::ops::RangeInclusive; use trevm::revm::{ bytecode::Bytecode, database::{ @@ -23,7 +24,7 @@ use trevm::revm::{ /// Maps address -> (old_account, new_account, storage_changes) /// where storage_changes maps slot (B256) -> (old_value, new_value) pub type BundleInit = - HashMap, Option, HashMap)>; + AHashMap, Option, AHashMap)>; /// Trait for database write operations on standard hot tables. /// @@ -264,16 +265,20 @@ pub trait UnsafeHistoryWrite: UnsafeDbWrite + HistoryRead { /// /// Iterates over entries starting from the first block in the range, /// collecting changes while the block number remains in range. + // TODO: estimate capacity from block range size for better allocation fn changed_accounts_with_range( &self, range: RangeInclusive, - ) -> Result>, Self::Error> { + ) -> Result>, Self::Error> { self.traverse_dual::()? .iter_from(range.start(), &Address::ZERO)? .process_results(|iter| { iter.take_while(|(num, _, _)| range.contains(num)) .map(|(num, addr, _)| (addr, num)) - .into_group_map() + .into_group_map_by(|(addr, _)| *addr) + .into_iter() + .map(|(addr, pairs)| (addr, pairs.into_iter().map(|(_, num)| num).collect())) + .collect() }) } @@ -299,17 +304,21 @@ pub trait UnsafeHistoryWrite: UnsafeDbWrite + HistoryRead { /// /// Iterates over entries starting from the first block in the range, /// collecting changes while the block number remains in range. + // TODO: estimate capacity from block range size for better allocation #[allow(clippy::type_complexity)] fn changed_storages_with_range( &self, range: RangeInclusive, - ) -> Result>, Self::Error> { + ) -> Result>, Self::Error> { self.traverse_dual::()? .iter_from(&(*range.start(), Address::ZERO), &U256::ZERO)? .process_results(|iter| { iter.take_while(|(num_addr, _, _)| range.contains(&num_addr.0)) .map(|(num_addr, slot, _)| ((num_addr.1, slot), num_addr.0)) - .into_group_map() + .into_group_map_by(|(key, _)| *key) + .into_iter() + .map(|(key, pairs)| (key, pairs.into_iter().map(|(_, num)| num).collect())) + .collect() }) } diff --git a/crates/hot/src/db/read.rs b/crates/hot/src/db/read.rs index 9e4ec67..aba5977 100644 --- a/crates/hot/src/db/read.rs +++ b/crates/hot/src/db/read.rs @@ -220,23 +220,11 @@ pub trait HistoryRead: HotDbRead { /// Get headers in a range (inclusive). fn get_headers_range(&self, start: u64, end: u64) -> Result, Self::Error> { - let mut cursor = self.traverse::()?; - let mut headers = Vec::new(); - - if cursor.lower_bound(&start)?.is_none() { - return Ok(headers); - } - - loop { - match cursor.read_next()? { - Some((num, header)) if num <= end => { - headers.push(header); - } - _ => break, - } - } - - Ok(headers) + self.traverse::()? + .iter_from(&start)? + .take_while(|r| r.as_ref().is_ok_and(|(num, _)| *num <= end)) + .map(|r| r.map(|(_, header)| header)) + .collect() } } diff --git a/crates/hot/src/model/traverse.rs b/crates/hot/src/model/traverse.rs deleted file mode 100644 index 5febb69..0000000 --- a/crates/hot/src/model/traverse.rs +++ /dev/null @@ -1,1281 +0,0 @@ -//! Cursor traversal traits and typed wrappers for database navigation. - -use crate::{ - model::{DualKeyValue, HotKvReadError, KeyValue, RawDualKeyValue, RawKeyValue, RawValue}, - ser::{KeySer, MAX_KEY_SIZE}, - tables::{DualKey, SingleKey}, -}; -use core::marker::PhantomData; -use std::borrow::Cow; -use std::ops::{Range, RangeInclusive}; - -/// Raw k2-value pair: (key2, value). -/// -/// This is returned by `iter_k2()` on dual-keyed tables. The caller already -/// knows k1 (they passed it in), so we don't return it redundantly. -pub type RawK2Value<'a> = (Cow<'a, [u8]>, RawValue<'a>); - -/// Typed k2-value pair for a dual-keyed table. -pub type K2Value = (::Key2, ::Value); - -/// An item from a dual-key iterator. -/// -/// This enum avoids cloning k1 for every value when iterating -/// over dual-keyed tables. K1 is only provided when it changes. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum DualKeyItem { - /// First entry for a new k1. - NewK1(K1, K2, V), - /// Additional k2/value for the current k1. - SameK1(K2, V), -} - -impl DualKeyItem { - /// Returns the value, consuming self. - pub fn into_value(self) -> V { - match self { - Self::NewK1(_, _, v) | Self::SameK1(_, v) => v, - } - } - - /// Returns a reference to the value. - pub const fn value(&self) -> &V { - match self { - Self::NewK1(_, _, v) | Self::SameK1(_, v) => v, - } - } - - /// Returns the k2, consuming self. - pub fn into_k2(self) -> K2 { - match self { - Self::NewK1(_, k2, _) | Self::SameK1(k2, _) => k2, - } - } - - /// Returns a reference to k2. - pub const fn k2(&self) -> &K2 { - match self { - Self::NewK1(_, k2, _) | Self::SameK1(k2, _) => k2, - } - } - - /// Returns k1 if this is a NewK1 entry. - pub const fn k1(&self) -> Option<&K1> { - match self { - Self::NewK1(k1, _, _) => Some(k1), - Self::SameK1(_, _) => None, - } - } - - /// Returns true if this item represents a new k1. - pub const fn is_new_k1(&self) -> bool { - matches!(self, Self::NewK1(..)) - } - - /// Convert to a tuple, requiring k1 to be provided for SameK1 variants. - pub fn into_tuple(self, current_k1: K1) -> (K1, K2, V) { - match self { - Self::NewK1(k1, k2, v) => (k1, k2, v), - Self::SameK1(k2, v) => (current_k1, k2, v), - } - } -} - -/// Raw dual-key item: (k1?, k2, value) where k1 is only present on NewK1. -pub type RawDualKeyItem<'a> = DualKeyItem, Cow<'a, [u8]>, Cow<'a, [u8]>>; - -/// Trait for traversing key-value pairs in the database. -pub trait KvTraverse { - /// Set position to the first key-value pair in the database, and return - /// the KV pair. - fn first<'a>(&'a mut self) -> Result>, E>; - - /// Set position to the last key-value pair in the database, and return the - /// KV pair. - fn last<'a>(&'a mut self) -> Result>, E>; - - /// Set the cursor to specific key in the database, and return the EXACT KV - /// pair if it exists. - fn exact<'a>(&'a mut self, key: &[u8]) -> Result>, E>; - - /// Seek to the next key-value pair AT OR ABOVE the specified key in the - /// database, and return that KV pair. - fn lower_bound<'a>(&'a mut self, key: &[u8]) -> Result>, E>; - - /// Get the next key-value pair in the database, and advance the cursor. - /// - /// Returning `Ok(None)` indicates the cursor is past the end of the - /// database. - fn read_next<'a>(&'a mut self) -> Result>, E>; - - /// Get the previous key-value pair in the database, and move the cursor. - /// - /// Returning `Ok(None)` indicates the cursor is before the start of the - /// database. - fn read_prev<'a>(&'a mut self) -> Result>, E>; - - /// Position at first entry and return iterator over all entries. - /// - /// The default implementation uses `first()` followed by repeated - /// `read_next()` calls. Implementations may override this to use - /// more efficient native iterators. - fn iter(&mut self) -> Result, E>> + '_, E> - where - Self: Sized, - { - self.first()?; - Ok(RawKvIter { cursor: self, done: false, _marker: PhantomData }) - } - - /// Position at `key` and return iterator over subsequent entries. - /// - /// The iterator starts at the first entry with key >= `key`. - /// The default implementation uses `lower_bound()` followed by repeated - /// `read_next()` calls. Implementations may override this to use - /// more efficient native iterators. - fn iter_from<'a>( - &'a mut self, - key: &[u8], - ) -> Result, E>> + 'a, E> - where - Self: Sized, - { - self.lower_bound(key)?; - Ok(RawKvIter { cursor: self, done: false, _marker: PhantomData }) - } -} - -/// Trait for traversing key-value pairs in the database with mutation -/// capabilities. -pub trait KvTraverseMut: KvTraverse { - /// Delete the current key-value pair in the database. - fn delete_current(&mut self) -> Result<(), E>; - - /// Delete a range of key-value pairs in the database (exclusive end). - fn delete_range(&mut self, range: Range<&[u8]>) -> Result<(), E> { - let Some((key, _)) = self.lower_bound(range.start)? else { - return Ok(()); - }; - if key.as_ref() >= range.end { - return Ok(()); - } - self.delete_current()?; - - while let Some((key, _)) = self.read_next()? { - if key.as_ref() >= range.end { - break; - } - self.delete_current()?; - } - Ok(()) - } - - /// Delete a range of key-value pairs in the database (inclusive end). - fn delete_range_inclusive(&mut self, start: &[u8], end: &[u8]) -> Result<(), E> { - let Some((key, _)) = self.lower_bound(start)? else { - return Ok(()); - }; - if key.as_ref() > end { - return Ok(()); - } - self.delete_current()?; - - while let Some((key, _)) = self.read_next()? { - if key.as_ref() > end { - break; - } - self.delete_current()?; - } - Ok(()) - } -} - -/// Trait for traversing dual-keyed key-value pairs in the database. -pub trait DualKeyTraverse { - /// Set position to the first key-value pair in the database, and return - /// the KV pair with both keys. - fn first<'a>(&'a mut self) -> Result>, E>; - - /// Set position to the last key-value pair in the database, and return the - /// KV pair with both keys. - fn last<'a>(&'a mut self) -> Result>, E>; - - /// Get the next key-value pair in the database, and advance the cursor. - /// - /// Returning `Ok(None)` indicates the cursor is past the end of the - /// database. - fn read_next<'a>(&'a mut self) -> Result>, E>; - - /// Get the previous key-value pair in the database, and move the cursor. - /// - /// Returning `Ok(None)` indicates the cursor is before the start of the - /// database. - fn read_prev<'a>(&'a mut self) -> Result>, E>; - - /// Set the cursor to specific dual key in the database, and return the - /// EXACT KV pair if it exists. - /// - /// Returning `Ok(None)` indicates the exact dual key does not exist. - fn exact_dual<'a>(&'a mut self, key1: &[u8], key2: &[u8]) -> Result>, E>; - - /// Seek to the next key-value pair AT or ABOVE the specified dual key in - /// the database, and return that KV pair. - /// - /// Returning `Ok(None)` indicates there are no more key-value pairs above - /// the specified dual key. - fn next_dual_above<'a>( - &'a mut self, - key1: &[u8], - key2: &[u8], - ) -> Result>, E>; - - /// Move the cursor to the next distinct key1, and return the first - /// key-value pair with that key1. - /// - /// Returning `Ok(None)` indicates there are no more distinct key1 values. - fn next_k1<'a>(&'a mut self) -> Result>, E>; - - /// Move the cursor to the next distinct key2 for the current key1, and - /// return the first key-value pair with that key2. - fn next_k2<'a>(&'a mut self) -> Result>, E>; - - /// Seek to the LAST key2 entry for the specified key1. - /// - /// This positions the cursor at the last duplicate value for the given key1. - /// Returning `Ok(None)` indicates the key1 does not exist. - fn last_of_k1<'a>(&'a mut self, key1: &[u8]) -> Result>, E>; - - /// Move the cursor to the LAST key2 entry of the PREVIOUS key1. - /// - /// This is the reverse of `next_k1` - it moves backward to the previous distinct - /// key1 and positions at its last key2 entry. - /// Returning `Ok(None)` indicates there is no previous key1. - fn previous_k1<'a>(&'a mut self) -> Result>, E>; - - /// Move the cursor to the PREVIOUS key2 entry for the CURRENT key1. - /// - /// This is the reverse of `next_k2` - it moves backward within the current key1's - /// duplicate values. - /// Returning `Ok(None)` indicates there is no previous key2 for this key1. - fn previous_k2<'a>(&'a mut self) -> Result>, E>; - - /// Position at first entry and return iterator over all entries. - /// - /// The default implementation uses `first()` followed by repeated - /// `read_next()` calls. Implementations may override this to use - /// more efficient native iterators. - fn iter(&mut self) -> Result, E>> + '_, E> - where - Self: Sized, - { - self.first()?; - Ok(RawDualKeyIter { cursor: self, done: false, _marker: PhantomData }) - } - - /// Position at (k1, k2) and return iterator over subsequent entries. - /// - /// The iterator starts at the first entry with (k1, k2) >= the specified - /// keys and continues through all subsequent entries, crossing k1 boundaries. - fn iter_from<'a>( - &'a mut self, - k1: &[u8], - k2: &[u8], - ) -> Result, E>> + 'a, E> - where - Self: Sized, - { - self.next_dual_above(k1, k2)?; - Ok(RawDualKeyIter { cursor: self, done: false, _marker: PhantomData }) - } - - /// Iterate all k2 entries within a single k1. - /// - /// The iterator yields `(k2, value)` pairs for the specified k1, starting - /// from the first k2 value, and stops when k1 changes or the table is - /// exhausted. - /// - /// Note: k1 is not included in the output since the caller already knows - /// it (they passed it in). This avoids redundant allocations. - fn iter_k2<'a>( - &'a mut self, - k1: &[u8], - ) -> Result, E>> + 'a, E> - where - Self: Sized, - { - // Position at first entry for this k1 (using empty slice as minimum k2) - let entry = self.next_dual_above(k1, &[])?; - let Some((found_k1, _, _)) = entry else { - return Ok(RawDualKeyK2Iter { cursor: self, done: true, _marker: PhantomData }); - }; - // If the found k1 doesn't match, we're done - let done = found_k1.as_ref() != k1; - Ok(RawDualKeyK2Iter { cursor: self, done, _marker: PhantomData }) - } - - /// Position at first entry and return iterator yielding [`DualKeyItem`]. - /// - /// This is more efficient than [`Self::iter()`] as it avoids cloning k1 for - /// every entry within the same k1 group. The iterator yields - /// [`DualKeyItem::NewK1`] when k1 changes and [`DualKeyItem::SameK1`] for - /// subsequent entries with the same k1. - /// - /// Backends implement this natively to leverage efficient "new key" - /// notifications from the underlying database. - fn iter_items(&mut self) -> Result, E>> + '_, E> - where - Self: Sized; -} - -/// Trait for traversing dual-keyed key-value pairs with mutation capabilities. -pub trait DualKeyTraverseMut: DualKeyTraverse { - /// Delete all K2 entries for the specified K1. - /// - /// This positions the cursor at the given K1 and removes all associated - /// K2 entries in a single operation. - /// - /// Returns `Ok(())` if the K1 was cleared or didn't exist. - fn clear_k1(&mut self, key1: &[u8]) -> Result<(), E>; - - /// Delete the current dual-keyed entry. - fn delete_current(&mut self) -> Result<(), E>; - - /// Delete a range of dual-keyed entries (inclusive). - /// - /// Deletes all entries where `(k1, k2) >= (start_k1, start_k2)` and - /// `(k1, k2) <= (end_k1, end_k2)`. - fn delete_range( - &mut self, - start_k1: &[u8], - start_k2: &[u8], - end_k1: &[u8], - end_k2: &[u8], - ) -> Result<(), E> { - let Some((k1, k2, _)) = self.next_dual_above(start_k1, start_k2)? else { - return Ok(()); - }; - - // Check if first entry is past end of range - if k1.as_ref() > end_k1 || (k1.as_ref() == end_k1 && k2.as_ref() > end_k2) { - return Ok(()); - } - - self.delete_current()?; - - while let Some((k1, k2, _)) = self.read_next()? { - // Check if we're past end of range - if k1.as_ref() > end_k1 || (k1.as_ref() == end_k1 && k2.as_ref() > end_k2) { - break; - } - self.delete_current()?; - } - - Ok(()) - } -} - -// ============================================================================ -// Typed Extension Traits -// ============================================================================ - -/// Extension trait for typed table traversal. -/// -/// This trait provides type-safe access to table entries by encoding keys -/// and decoding values according to the table's schema. -pub trait TableTraverse: KvTraverse { - /// Get the first key-value pair in the table. - fn first(&mut self) -> Result>, E> { - KvTraverse::first(self)?.map(T::decode_kv_tuple).transpose().map_err(Into::into) - } - - /// Get the last key-value pair in the table. - fn last(&mut self) -> Result>, E> { - KvTraverse::last(self)?.map(T::decode_kv_tuple).transpose().map_err(Into::into) - } - - /// Set the cursor to a specific key and return the EXACT value if it exists. - fn exact(&mut self, key: &T::Key) -> Result, E> { - let mut key_buf = [0u8; MAX_KEY_SIZE]; - let key_bytes = key.encode_key(&mut key_buf); - - KvTraverse::exact(self, key_bytes)?.map(T::decode_value).transpose().map_err(Into::into) - } - - /// Seek to the next key-value pair AT OR ABOVE the specified key. - fn lower_bound(&mut self, key: &T::Key) -> Result>, E> { - let mut key_buf = [0u8; MAX_KEY_SIZE]; - let key_bytes = key.encode_key(&mut key_buf); - - KvTraverse::lower_bound(self, key_bytes)? - .map(T::decode_kv_tuple) - .transpose() - .map_err(Into::into) - } - - /// Get the next key-value pair and advance the cursor. - fn read_next(&mut self) -> Result>, E> { - KvTraverse::read_next(self)?.map(T::decode_kv_tuple).transpose().map_err(Into::into) - } - - /// Get the previous key-value pair and move the cursor backward. - fn read_prev(&mut self) -> Result>, E> { - KvTraverse::read_prev(self)?.map(T::decode_kv_tuple).transpose().map_err(Into::into) - } - - /// Position at `key` and return iterator over subsequent entries. - /// - /// The iterator starts at the first entry with key >= `key`. - fn iter_from( - &mut self, - key: &T::Key, - ) -> Result, E>> + '_, E> - where - Self: Sized, - { - // Position the cursor at the key - TableTraverse::::lower_bound(self, key)?; - // Return iterator that decodes from raw - Ok(KvTraverse::iter(self)? - .map(|r| r.and_then(|kv| T::decode_kv_tuple(kv).map_err(Into::into)))) - } - - /// Position at first entry and return iterator over all entries. - fn iter(&mut self) -> Result, E>> + '_, E> - where - Self: Sized, - { - Ok(KvTraverse::iter(self)? - .map(|r| r.and_then(|kv| T::decode_kv_tuple(kv).map_err(Into::into)))) - } -} - -/// Blanket implementation of `TableTraverse` for any cursor that implements `KvTraverse`. -impl TableTraverse for C -where - C: KvTraverse, - T: SingleKey, - E: HotKvReadError, -{ -} - -/// Extension trait for typed table traversal with mutation capabilities. -pub trait TableTraverseMut: KvTraverseMut { - /// Delete the current key-value pair. - fn delete_current(&mut self) -> Result<(), E> { - KvTraverseMut::delete_current(self) - } - - /// Delete a range of key-value pairs (exclusive end). - fn delete_range(&mut self, range: Range) -> Result<(), E> { - let mut start_key_buf = [0u8; MAX_KEY_SIZE]; - let mut end_key_buf = [0u8; MAX_KEY_SIZE]; - let start_key_bytes = range.start.encode_key(&mut start_key_buf); - let end_key_bytes = range.end.encode_key(&mut end_key_buf); - - KvTraverseMut::delete_range(self, start_key_bytes..end_key_bytes) - } - - /// Delete a range of key-value pairs (inclusive end). - fn delete_range_inclusive(&mut self, range: RangeInclusive) -> Result<(), E> - where - Self: Sized, - { - let Some((key, _)) = TableTraverse::::lower_bound(self, range.start())? else { - return Ok(()); - }; - if !range.contains(&key) { - return Ok(()); - } - KvTraverseMut::delete_current(self)?; - - while let Some((key, _)) = TableTraverse::::read_next(self)? { - if !range.contains(&key) { - break; - } - KvTraverseMut::delete_current(self)?; - } - Ok(()) - } - - /// Delete a range of key-value pairs and return the removed entries. - fn take_range(&mut self, range: RangeInclusive) -> Result>, E> - where - Self: Sized, - { - let mut result = Vec::new(); - - let Some((key, value)) = TableTraverse::::lower_bound(self, range.start())? else { - return Ok(result); - }; - if !range.contains(&key) { - return Ok(result); - } - result.push((key, value)); - KvTraverseMut::delete_current(self)?; - - while let Some((key, value)) = TableTraverse::::read_next(self)? { - if !range.contains(&key) { - break; - } - result.push((key, value)); - KvTraverseMut::delete_current(self)?; - } - - Ok(result) - } -} - -/// Blanket implementation of [`TableTraverseMut`] for any cursor that implements [`KvTraverseMut`]. -impl TableTraverseMut for C -where - C: KvTraverseMut, - T: SingleKey, - E: HotKvReadError, -{ -} - -/// A typed cursor wrapper for traversing dual-keyed tables. -/// -/// This is an extension trait rather than a wrapper struct because MDBX -/// requires specialized implementations for DUPSORT tables that need access -/// to the table type `T` to handle fixed-size values correctly. -pub trait DualTableTraverse: DualKeyTraverse { - /// Get the first key-value pair in the table. - fn first(&mut self) -> Result>, E> { - DualKeyTraverse::first(self)?.map(T::decode_kkv_tuple).transpose().map_err(Into::into) - } - - /// Get the last key-value pair in the table. - fn last(&mut self) -> Result>, E> { - DualKeyTraverse::last(self)?.map(T::decode_kkv_tuple).transpose().map_err(Into::into) - } - - /// Get the next key-value pair and advance the cursor. - fn read_next(&mut self) -> Result>, E> { - DualKeyTraverse::read_next(self)?.map(T::decode_kkv_tuple).transpose().map_err(Into::into) - } - - /// Get the previous key-value pair and move the cursor backward. - fn read_prev(&mut self) -> Result>, E> { - DualKeyTraverse::read_prev(self)?.map(T::decode_kkv_tuple).transpose().map_err(Into::into) - } - - /// Return the EXACT value for the specified dual key if it exists. - fn exact_dual(&mut self, key1: &T::Key, key2: &T::Key2) -> Result, E> { - let Some((k1, k2, v)) = DualTableTraverse::next_dual_above(self, key1, key2)? else { - return Ok(None); - }; - - if k1 == *key1 && k2 == *key2 { Ok(Some(v)) } else { Ok(None) } - } - - /// Seek to the next key-value pair AT or ABOVE the specified dual key. - fn next_dual_above( - &mut self, - key1: &T::Key, - key2: &T::Key2, - ) -> Result>, E>; - - /// Seek to the next distinct key1, and return the first key-value pair with that key1. - fn next_k1(&mut self) -> Result>, E>; - - /// Seek to the next distinct key2 for the current key1. - fn next_k2(&mut self) -> Result>, E>; - - /// Seek to the LAST key2 entry for the specified key1. - fn last_of_k1(&mut self, key1: &T::Key) -> Result>, E>; - - /// Move to the LAST key2 entry of the PREVIOUS key1. - fn previous_k1(&mut self) -> Result>, E>; - - /// Move to the PREVIOUS key2 entry for the CURRENT key1. - fn previous_k2(&mut self) -> Result>, E>; - - /// Position at (k1, k2) and iterate forward, crossing k1 boundaries. - /// - /// The iterator starts at the first entry with (k1, k2) >= the specified - /// keys and continues through all subsequent entries. - fn iter_from( - &mut self, - k1: &T::Key, - k2: &T::Key2, - ) -> Result, E>> + '_, E> - where - T::Key: PartialEq; - - /// Position at first entry and return iterator over all entries. - fn iter(&mut self) -> Result, E>> + '_, E> - where - T::Key: PartialEq; - - /// Iterate all k2 entries within a single k1. - /// - /// The iterator yields `(k2, value)` pairs for the specified k1, starting - /// from the first k2 value, and stops when k1 changes or the table is - /// exhausted. - /// - /// Note: k1 is not included in the output since the caller already knows - /// it (they passed it in). This avoids redundant allocations. - fn iter_k2( - &mut self, - k1: &T::Key, - ) -> Result, E>> + '_, E> - where - T::Key: PartialEq; -} - -impl DualTableTraverse for C -where - C: DualKeyTraverse, - T: DualKey, - E: HotKvReadError, -{ - fn next_dual_above( - &mut self, - key1: &T::Key, - key2: &T::Key2, - ) -> Result>, E> { - let mut key1_buf = [0u8; MAX_KEY_SIZE]; - let mut key2_buf = [0u8; MAX_KEY_SIZE]; - let key1_bytes = key1.encode_key(&mut key1_buf); - let key2_bytes = key2.encode_key(&mut key2_buf); - - DualKeyTraverse::next_dual_above(self, key1_bytes, key2_bytes)? - .map(T::decode_kkv_tuple) - .transpose() - .map_err(Into::into) - } - - fn next_k1(&mut self) -> Result>, E> { - DualKeyTraverse::next_k1(self)?.map(T::decode_kkv_tuple).transpose().map_err(Into::into) - } - - fn next_k2(&mut self) -> Result>, E> { - DualKeyTraverse::next_k2(self)?.map(T::decode_kkv_tuple).transpose().map_err(Into::into) - } - - fn last_of_k1(&mut self, key1: &T::Key) -> Result>, E> { - let mut key1_buf = [0u8; MAX_KEY_SIZE]; - let key1_bytes = key1.encode_key(&mut key1_buf); - - DualKeyTraverse::last_of_k1(self, key1_bytes)? - .map(T::decode_kkv_tuple) - .transpose() - .map_err(Into::into) - } - - fn previous_k1(&mut self) -> Result>, E> { - DualKeyTraverse::previous_k1(self)?.map(T::decode_kkv_tuple).transpose().map_err(Into::into) - } - - fn previous_k2(&mut self) -> Result>, E> { - DualKeyTraverse::previous_k2(self)?.map(T::decode_kkv_tuple).transpose().map_err(Into::into) - } - - fn iter_from( - &mut self, - k1: &T::Key, - k2: &T::Key2, - ) -> Result, E>> + '_, E> - where - T::Key: PartialEq, - { - // Position cursor at (k1, k2) - DualTableTraverse::::next_dual_above(self, k1, k2)?; - // Return iterator that decodes from raw - Ok(DualKeyTraverse::iter(self)? - .map(|r| r.and_then(|kkv| T::decode_kkv_tuple(kkv).map_err(Into::into)))) - } - - fn iter(&mut self) -> Result, E>> + '_, E> - where - T::Key: PartialEq, - { - Ok(DualKeyTraverse::iter(self)? - .map(|r| r.and_then(|kkv| T::decode_kkv_tuple(kkv).map_err(Into::into)))) - } - - fn iter_k2( - &mut self, - k1: &T::Key, - ) -> Result, E>> + '_, E> - where - T::Key: PartialEq, - { - // Position cursor at first entry for this k1 using raw interface with empty k2 - let mut key1_buf = [0u8; MAX_KEY_SIZE]; - let key1_bytes = k1.encode_key(&mut key1_buf); - let entry = DualKeyTraverse::next_dual_above(self, key1_bytes, &[])?; - let Some((found_k1, _, _)) = entry else { - return Ok(DualTableK2Iter::<'_, C, T, E> { - cursor: self, - done: true, - _marker: PhantomData, - }); - }; - // Decode the found k1 to check if it matches - let decoded_k1 = T::decode_key(found_k1)?; - // If the found k1 doesn't match, we're done - let done = decoded_k1 != *k1; - Ok(DualTableK2Iter::<'_, C, T, E> { cursor: self, done, _marker: PhantomData }) - } -} - -/// Extension trait for typed dual table traversal with mutation capabilities. -pub trait DualTableTraverseMut: DualKeyTraverseMut { - /// Delete all K2 entries for the specified K1. - fn clear_k1(&mut self, key1: &T::Key) -> Result<(), E> { - let mut key1_buf = [0u8; MAX_KEY_SIZE]; - let key1_bytes = key1.encode_key(&mut key1_buf); - DualKeyTraverseMut::clear_k1(self, key1_bytes) - } - - /// Delete the current dual-keyed entry. - fn delete_current(&mut self) -> Result<(), E> { - DualKeyTraverseMut::delete_current(self) - } - - /// Delete a range of dual-keyed entries (inclusive). - fn delete_range(&mut self, range: RangeInclusive<(T::Key, T::Key2)>) -> Result<(), E> - where - Self: Sized, - { - let (start_k1, start_k2) = range.start(); - let (end_k1, end_k2) = range.end(); - - // Loop using next_dual_above to handle backends where delete auto-advances - // the cursor (e.g., MDBX) vs those that don't (e.g., in-memory). - loop { - let Some((k1, k2, _)) = - DualTableTraverse::::next_dual_above(self, start_k1, start_k2)? - else { - break; - }; - - // Check if entry is past end of range (typed comparison) - if &k1 > end_k1 || (&k1 == end_k1 && &k2 > end_k2) { - break; - } - - DualKeyTraverseMut::delete_current(self)?; - } - - Ok(()) - } - - /// Delete a range of dual-keyed entries and return the removed entries. - fn take_range( - &mut self, - range: RangeInclusive<(T::Key, T::Key2)>, - ) -> Result>, E> - where - Self: Sized, - { - let (start_k1, start_k2) = range.start(); - let (end_k1, end_k2) = range.end(); - - let mut result = Vec::new(); - - // Loop using next_dual_above to handle backends where delete auto-advances - // the cursor (e.g., MDBX) vs those that don't (e.g., in-memory). - loop { - let Some((k1, k2, value)) = - DualTableTraverse::::next_dual_above(self, start_k1, start_k2)? - else { - break; - }; - - // Check if entry is past end of range (typed comparison) - if &k1 > end_k1 || (&k1 == end_k1 && &k2 > end_k2) { - break; - } - - result.push((k1, k2, value)); - DualKeyTraverseMut::delete_current(self)?; - } - - Ok(result) - } -} - -/// Blanket implementation of `DualTableTraverseMut`. -impl DualTableTraverseMut for C -where - C: DualKeyTraverseMut, - T: DualKey, - E: HotKvReadError, -{ -} - -// ============================================================================ -// Raw Iterator Structs (for base traits) -// ============================================================================ - -/// Default forward iterator over raw key-value entries. -/// -/// This iterator wraps a cursor implementing `KvTraverse` and yields entries -/// by calling `read_next` on each iteration. -pub struct RawKvIter<'a, C, E> { - cursor: &'a mut C, - done: bool, - _marker: PhantomData E>, -} - -impl<'a, C, E> Iterator for RawKvIter<'a, C, E> -where - C: KvTraverse, - E: HotKvReadError, -{ - type Item = Result, E>; - - fn next(&mut self) -> Option { - if self.done { - return None; - } - // SAFETY: We're using a mutable borrow of the cursor, so the lifetime - // of the returned data is correctly tied to this iterator. - // We transmute the lifetime because the borrow checker can't see - // that the cursor's internal state keeps the data alive. - let result = unsafe { - std::mem::transmute::< - Result>, E>, - Result>, E>, - >(self.cursor.read_next()) - }; - match result { - Ok(Some(kv)) => Some(Ok(kv)), - Ok(None) => { - self.done = true; - None - } - Err(e) => { - self.done = true; - Some(Err(e)) - } - } - } -} - -/// Default forward iterator over raw dual-keyed entries. -/// -/// This iterator wraps a cursor implementing `DualKeyTraverse` and yields -/// entries by calling `read_next` on each iteration. -pub struct RawDualKeyIter<'a, C, E> { - cursor: &'a mut C, - done: bool, - _marker: PhantomData E>, -} - -impl<'a, C, E> Iterator for RawDualKeyIter<'a, C, E> -where - C: DualKeyTraverse, - E: HotKvReadError, -{ - type Item = Result, E>; - - fn next(&mut self) -> Option { - if self.done { - return None; - } - // SAFETY: Same rationale as RawKvIter - the cursor's internal state - // keeps the data alive for the duration of iteration. - let result = unsafe { - std::mem::transmute::< - Result>, E>, - Result>, E>, - >(self.cursor.read_next()) - }; - match result { - Ok(Some(kv)) => Some(Ok(kv)), - Ok(None) => { - self.done = true; - None - } - Err(e) => { - self.done = true; - Some(Err(e)) - } - } - } -} - -/// Default forward iterator over raw k2-value entries within a single k1. -/// -/// This iterator wraps a cursor implementing `DualKeyTraverse` and yields -/// `(k2, value)` pairs while k1 remains unchanged. The iterator stops when k1 -/// changes or the table is exhausted. -pub struct RawDualKeyK2Iter<'a, C, E> { - cursor: &'a mut C, - done: bool, - _marker: PhantomData E>, -} - -impl<'a, C, E> Iterator for RawDualKeyK2Iter<'a, C, E> -where - C: DualKeyTraverse, - E: HotKvReadError, -{ - type Item = Result, E>; - - fn next(&mut self) -> Option { - if self.done { - return None; - } - // SAFETY: Same rationale as RawKvIter - let result = unsafe { - std::mem::transmute::< - Result>, E>, - Result>, E>, - >(self.cursor.next_k2()) - }; - match result { - Ok(Some((_k1, k2, v))) => Some(Ok((k2, v))), - Ok(None) => { - self.done = true; - None - } - Err(e) => { - self.done = true; - Some(Err(e)) - } - } - } -} - -// ============================================================================ -// Typed Iterator Structs (for extension traits) -// ============================================================================ - -/// Forward iterator over k2-value pairs within a single k1. -/// -/// This iterator wraps a cursor and yields `(k2, value)` pairs by calling -/// `next_k2()` on each iteration. The iterator stops when there are no more -/// k2 entries for the current k1 or when an error occurs. -pub struct DualTableK2Iter<'a, C, T, E> -where - T: DualKey, -{ - cursor: &'a mut C, - done: bool, - _marker: PhantomData (T, E)>, -} - -impl<'a, C, T, E> Iterator for DualTableK2Iter<'a, C, T, E> -where - C: DualKeyTraverse, - T: DualKey, - E: HotKvReadError, -{ - type Item = Result, E>; - - fn next(&mut self) -> Option { - if self.done { - return None; - } - match DualTableTraverse::::next_k2(self.cursor) { - Ok(Some((_k1, k2, v))) => Some(Ok((k2, v))), - Ok(None) => { - self.done = true; - None - } - Err(e) => { - self.done = true; - Some(Err(e)) - } - } - } -} - -// ============================================================================ -// Wrapper Structs -// ============================================================================ - -/// A wrapper struct for typed table traversal. -/// -/// This struct wraps a raw cursor and provides type-safe access to table -/// entries. It implements `TableTraverse` by delegating to the inner -/// cursor. -#[derive(Debug)] -pub struct TableCursor { - inner: C, - _marker: PhantomData (T, E)>, -} - -impl TableCursor { - /// Create a new typed table cursor wrapper. - pub const fn new(cursor: C) -> Self { - Self { inner: cursor, _marker: PhantomData } - } - - /// Get a reference to the inner cursor. - pub const fn inner(&self) -> &C { - &self.inner - } - - /// Get a mutable reference to the inner cursor. - pub const fn inner_mut(&mut self) -> &mut C { - &mut self.inner - } - - /// Consume the wrapper and return the inner cursor. - pub fn into_inner(self) -> C { - self.inner - } -} - -impl TableCursor -where - C: KvTraverse, - T: SingleKey, - E: HotKvReadError, -{ - /// Get the first key-value pair in the table. - pub fn first(&mut self) -> Result>, E> { - TableTraverse::::first(&mut self.inner) - } - - /// Get the last key-value pair in the table. - pub fn last(&mut self) -> Result>, E> { - TableTraverse::::last(&mut self.inner) - } - - /// Set the cursor to a specific key and return the EXACT value if it exists. - pub fn exact(&mut self, key: &T::Key) -> Result, E> { - TableTraverse::::exact(&mut self.inner, key) - } - - /// Seek to the next key-value pair AT OR ABOVE the specified key. - pub fn lower_bound(&mut self, key: &T::Key) -> Result>, E> { - TableTraverse::::lower_bound(&mut self.inner, key) - } - - /// Get the next key-value pair and advance the cursor. - pub fn read_next(&mut self) -> Result>, E> { - TableTraverse::::read_next(&mut self.inner) - } - - /// Get the previous key-value pair and move the cursor backward. - pub fn read_prev(&mut self) -> Result>, E> { - TableTraverse::::read_prev(&mut self.inner) - } - - /// Position at `key` and return iterator over subsequent entries. - /// - /// The iterator starts at the first entry with key >= `key`. - pub fn iter_from( - &mut self, - key: &T::Key, - ) -> Result, E>> + '_, E> { - TableTraverse::::iter_from(&mut self.inner, key) - } - - /// Position at first entry and return iterator over all entries. - pub fn iter(&mut self) -> Result, E>> + '_, E> { - TableTraverse::::iter(&mut self.inner) - } -} - -impl TableCursor -where - C: KvTraverseMut, - T: SingleKey, - E: HotKvReadError, -{ - /// Delete the current key-value pair. - pub fn delete_current(&mut self) -> Result<(), E> { - TableTraverseMut::::delete_current(&mut self.inner) - } - - /// Delete a range of key-value pairs (exclusive end). - pub fn delete_range(&mut self, range: Range) -> Result<(), E> { - TableTraverseMut::::delete_range(&mut self.inner, range) - } - - /// Delete a range of key-value pairs (inclusive end). - pub fn delete_range_inclusive(&mut self, range: RangeInclusive) -> Result<(), E> { - TableTraverseMut::::delete_range_inclusive(&mut self.inner, range) - } - - /// Delete a range of key-value pairs and return the removed entries. - pub fn take_range(&mut self, range: RangeInclusive) -> Result>, E> { - TableTraverseMut::::take_range(&mut self.inner, range) - } -} - -/// A wrapper struct for typed dual-keyed table traversal. -/// -/// This struct wraps a raw cursor and provides type-safe access to dual-keyed -/// table entries. It delegates to the `DualTableTraverse` trait -/// implementation on the inner cursor. -#[derive(Debug)] -pub struct DualTableCursor { - inner: C, - _marker: PhantomData (T, E)>, -} - -impl DualTableCursor { - /// Create a new typed dual-keyed table cursor wrapper. - pub const fn new(cursor: C) -> Self { - Self { inner: cursor, _marker: PhantomData } - } - - /// Get a reference to the inner cursor. - pub const fn inner(&self) -> &C { - &self.inner - } - - /// Get a mutable reference to the inner cursor. - pub const fn inner_mut(&mut self) -> &mut C { - &mut self.inner - } - - /// Consume the wrapper and return the inner cursor. - pub fn into_inner(self) -> C { - self.inner - } -} - -impl DualTableCursor -where - C: DualTableTraverse, - T: DualKey, - E: HotKvReadError, -{ - /// Return the EXACT value for the specified dual key if it exists. - pub fn exact_dual(&mut self, key1: &T::Key, key2: &T::Key2) -> Result, E> { - DualTableTraverse::::exact_dual(&mut self.inner, key1, key2) - } - - /// Seek to the next key-value pair AT or ABOVE the specified dual key. - pub fn next_dual_above( - &mut self, - key1: &T::Key, - key2: &T::Key2, - ) -> Result>, E> { - DualTableTraverse::::next_dual_above(&mut self.inner, key1, key2) - } - - /// Seek to the next distinct key1, and return the first key-value pair with that key1. - pub fn next_k1(&mut self) -> Result>, E> { - DualTableTraverse::::next_k1(&mut self.inner) - } - - /// Seek to the next distinct key2 for the current key1. - pub fn next_k2(&mut self) -> Result>, E> { - DualTableTraverse::::next_k2(&mut self.inner) - } - - /// Seek to the LAST key2 entry for the specified key1. - pub fn last_of_k1(&mut self, key1: &T::Key) -> Result>, E> { - DualTableTraverse::::last_of_k1(&mut self.inner, key1) - } - - /// Move to the LAST key2 entry of the PREVIOUS key1. - pub fn previous_k1(&mut self) -> Result>, E> { - DualTableTraverse::::previous_k1(&mut self.inner) - } - - /// Move to the PREVIOUS key2 entry for the CURRENT key1. - pub fn previous_k2(&mut self) -> Result>, E> { - DualTableTraverse::::previous_k2(&mut self.inner) - } -} - -// Also provide access to first/last/read_next/read_prev methods for dual-keyed cursors -impl DualTableCursor -where - C: DualTableTraverse, - T: DualKey, - E: HotKvReadError, -{ - /// Get the first key-value pair in the table. - pub fn first(&mut self) -> Result>, E> { - DualTableTraverse::::first(&mut self.inner) - } - - /// Get the last key-value pair in the table. - pub fn last(&mut self) -> Result>, E> { - DualTableTraverse::::last(&mut self.inner) - } - - /// Get the next key-value pair and advance the cursor. - pub fn read_next(&mut self) -> Result>, E> { - DualTableTraverse::::read_next(&mut self.inner) - } - - /// Get the previous key-value pair and move the cursor backward. - pub fn read_prev(&mut self) -> Result>, E> { - DualTableTraverse::::read_prev(&mut self.inner) - } -} - -// Iterator methods for DualTableCursor - require T::Key: PartialEq for k2 iteration -impl DualTableCursor -where - C: DualTableTraverse, - T: DualKey, - T::Key: PartialEq, - E: HotKvReadError, -{ - /// Position at (k1, k2) and iterate forward, crossing k1 boundaries. - /// - /// The iterator starts at the first entry with (k1, k2) >= the specified - /// keys and continues through all subsequent entries. - pub fn iter_from( - &mut self, - k1: &T::Key, - k2: &T::Key2, - ) -> Result, E>> + '_, E> { - DualTableTraverse::::iter_from(&mut self.inner, k1, k2) - } - - /// Position at first entry and return iterator over all entries. - pub fn iter(&mut self) -> Result, E>> + '_, E> { - DualTableTraverse::::iter(&mut self.inner) - } - - /// Iterate all k2 entries within a single k1. - /// - /// The iterator yields `(k2, value)` pairs for the specified k1, starting - /// from the first k2 value, and stops when k1 changes or the table is - /// exhausted. - /// - /// Note: k1 is not included in the output since the caller already knows - /// it (they passed it in). This avoids redundant allocations. - pub fn iter_k2( - &mut self, - k1: &T::Key, - ) -> Result, E>> + '_, E> { - DualTableTraverse::::iter_k2(&mut self.inner, k1) - } -} - -impl DualTableCursor -where - C: DualTableTraverseMut, - T: DualKey, - E: HotKvReadError, -{ - /// Delete the current key-value pair. - pub fn delete_current(&mut self) -> Result<(), E> { - DualTableTraverseMut::::delete_current(&mut self.inner) - } - - /// Delete all K2 entries for the specified K1. - pub fn clear_k1(&mut self, key1: &T::Key) -> Result<(), E> { - DualTableTraverseMut::::clear_k1(&mut self.inner, key1) - } - - /// Delete a range of dual-keyed entries (inclusive). - pub fn delete_range(&mut self, range: RangeInclusive<(T::Key, T::Key2)>) -> Result<(), E> { - DualTableTraverseMut::::delete_range(&mut self.inner, range) - } - - /// Delete a range of dual-keyed entries and return the removed entries. - pub fn take_range( - &mut self, - range: RangeInclusive<(T::Key, T::Key2)>, - ) -> Result>, E> { - DualTableTraverseMut::::take_range(&mut self.inner, range) - } -} diff --git a/crates/hot/src/model/traverse/cursor.rs b/crates/hot/src/model/traverse/cursor.rs new file mode 100644 index 0000000..12daaad --- /dev/null +++ b/crates/hot/src/model/traverse/cursor.rs @@ -0,0 +1,310 @@ +//! Typed cursor wrapper structs. + +use super::{ + DualKeyValue, HotKvReadError, KeyValue, + dual_table::{DualTableTraverse, DualTableTraverseMut}, + kv::{KvTraverse, KvTraverseMut}, + table::{TableTraverse, TableTraverseMut}, + types::K2Value, +}; +use crate::tables::{DualKey, SingleKey}; +use core::marker::PhantomData; +use std::ops::{Range, RangeInclusive}; + +// ============================================================================ +// TableCursor +// ============================================================================ + +/// A wrapper struct for typed table traversal. +/// +/// This struct wraps a raw cursor and provides type-safe access to table +/// entries. It implements `TableTraverse` by delegating to the inner +/// cursor. +#[derive(Debug)] +pub struct TableCursor { + inner: C, + _marker: PhantomData (T, E)>, +} + +impl TableCursor { + /// Create a new typed table cursor wrapper. + pub const fn new(cursor: C) -> Self { + Self { inner: cursor, _marker: PhantomData } + } + + /// Get a reference to the inner cursor. + pub const fn inner(&self) -> &C { + &self.inner + } + + /// Get a mutable reference to the inner cursor. + pub const fn inner_mut(&mut self) -> &mut C { + &mut self.inner + } + + /// Consume the wrapper and return the inner cursor. + pub fn into_inner(self) -> C { + self.inner + } +} + +impl TableCursor +where + C: KvTraverse, + T: SingleKey, + E: HotKvReadError, +{ + /// Get the first key-value pair in the table. + pub fn first(&mut self) -> Result>, E> { + TableTraverse::::first(&mut self.inner) + } + + /// Get the last key-value pair in the table. + pub fn last(&mut self) -> Result>, E> { + TableTraverse::::last(&mut self.inner) + } + + /// Set the cursor to a specific key and return the EXACT value if it exists. + pub fn exact(&mut self, key: &T::Key) -> Result, E> { + TableTraverse::::exact(&mut self.inner, key) + } + + /// Seek to the next key-value pair AT OR ABOVE the specified key. + pub fn lower_bound(&mut self, key: &T::Key) -> Result>, E> { + TableTraverse::::lower_bound(&mut self.inner, key) + } + + /// Get the next key-value pair and advance the cursor. + pub fn read_next(&mut self) -> Result>, E> { + TableTraverse::::read_next(&mut self.inner) + } + + /// Get the previous key-value pair and move the cursor backward. + pub fn read_prev(&mut self) -> Result>, E> { + TableTraverse::::read_prev(&mut self.inner) + } + + /// Position at `key` and return iterator over subsequent entries. + /// + /// The iterator starts at the first entry with key >= `key`. + pub fn iter_from( + &mut self, + key: &T::Key, + ) -> Result, E>> + '_, E> { + TableTraverse::::iter_from(&mut self.inner, key) + } + + /// Position at first entry and return iterator over all entries. + pub fn iter(&mut self) -> Result, E>> + '_, E> { + TableTraverse::::iter(&mut self.inner) + } +} + +impl TableCursor +where + C: KvTraverseMut, + T: SingleKey, + E: HotKvReadError, +{ + /// Delete the current key-value pair. + pub fn delete_current(&mut self) -> Result<(), E> { + TableTraverseMut::::delete_current(&mut self.inner) + } + + /// Delete a range of key-value pairs (exclusive end). + pub fn delete_range(&mut self, range: Range) -> Result<(), E> { + TableTraverseMut::::delete_range(&mut self.inner, range) + } + + /// Delete a range of key-value pairs (inclusive end). + pub fn delete_range_inclusive(&mut self, range: RangeInclusive) -> Result<(), E> { + TableTraverseMut::::delete_range_inclusive(&mut self.inner, range) + } + + /// Delete a range of key-value pairs and return the removed entries. + pub fn take_range(&mut self, range: RangeInclusive) -> Result>, E> { + TableTraverseMut::::take_range(&mut self.inner, range) + } +} + +// ============================================================================ +// DualTableCursor +// ============================================================================ + +/// A wrapper struct for typed dual-keyed table traversal. +/// +/// This struct wraps a raw cursor and provides type-safe access to dual-keyed +/// table entries. It delegates to the `DualTableTraverse` trait +/// implementation on the inner cursor. +#[derive(Debug)] +pub struct DualTableCursor { + inner: C, + _marker: PhantomData (T, E)>, +} + +impl DualTableCursor { + /// Create a new typed dual-keyed table cursor wrapper. + pub const fn new(cursor: C) -> Self { + Self { inner: cursor, _marker: PhantomData } + } + + /// Get a reference to the inner cursor. + pub const fn inner(&self) -> &C { + &self.inner + } + + /// Get a mutable reference to the inner cursor. + pub const fn inner_mut(&mut self) -> &mut C { + &mut self.inner + } + + /// Consume the wrapper and return the inner cursor. + pub fn into_inner(self) -> C { + self.inner + } +} + +impl DualTableCursor +where + C: DualTableTraverse, + T: DualKey, + E: HotKvReadError, +{ + /// Return the EXACT value for the specified dual key if it exists. + pub fn exact_dual(&mut self, key1: &T::Key, key2: &T::Key2) -> Result, E> { + DualTableTraverse::::exact_dual(&mut self.inner, key1, key2) + } + + /// Seek to the next key-value pair AT or ABOVE the specified dual key. + pub fn next_dual_above( + &mut self, + key1: &T::Key, + key2: &T::Key2, + ) -> Result>, E> { + DualTableTraverse::::next_dual_above(&mut self.inner, key1, key2) + } + + /// Seek to the next distinct key1, and return the first key-value pair with that key1. + pub fn next_k1(&mut self) -> Result>, E> { + DualTableTraverse::::next_k1(&mut self.inner) + } + + /// Seek to the next distinct key2 for the current key1. + pub fn next_k2(&mut self) -> Result>, E> { + DualTableTraverse::::next_k2(&mut self.inner) + } + + /// Seek to the LAST key2 entry for the specified key1. + pub fn last_of_k1(&mut self, key1: &T::Key) -> Result>, E> { + DualTableTraverse::::last_of_k1(&mut self.inner, key1) + } + + /// Move to the LAST key2 entry of the PREVIOUS key1. + pub fn previous_k1(&mut self) -> Result>, E> { + DualTableTraverse::::previous_k1(&mut self.inner) + } + + /// Move to the PREVIOUS key2 entry for the CURRENT key1. + pub fn previous_k2(&mut self) -> Result>, E> { + DualTableTraverse::::previous_k2(&mut self.inner) + } +} + +// Also provide access to first/last/read_next/read_prev methods for dual-keyed cursors +impl DualTableCursor +where + C: DualTableTraverse, + T: DualKey, + E: HotKvReadError, +{ + /// Get the first key-value pair in the table. + pub fn first(&mut self) -> Result>, E> { + DualTableTraverse::::first(&mut self.inner) + } + + /// Get the last key-value pair in the table. + pub fn last(&mut self) -> Result>, E> { + DualTableTraverse::::last(&mut self.inner) + } + + /// Get the next key-value pair and advance the cursor. + pub fn read_next(&mut self) -> Result>, E> { + DualTableTraverse::::read_next(&mut self.inner) + } + + /// Get the previous key-value pair and move the cursor backward. + pub fn read_prev(&mut self) -> Result>, E> { + DualTableTraverse::::read_prev(&mut self.inner) + } +} + +// Iterator methods for DualTableCursor - require T::Key: PartialEq for k2 iteration +impl DualTableCursor +where + C: DualTableTraverse, + T: DualKey, + T::Key: PartialEq, + E: HotKvReadError, +{ + /// Position at (k1, k2) and iterate forward, crossing k1 boundaries. + /// + /// The iterator starts at the first entry with (k1, k2) >= the specified + /// keys and continues through all subsequent entries. + pub fn iter_from( + &mut self, + k1: &T::Key, + k2: &T::Key2, + ) -> Result, E>> + '_, E> { + DualTableTraverse::::iter_from(&mut self.inner, k1, k2) + } + + /// Position at first entry and return iterator over all entries. + pub fn iter(&mut self) -> Result, E>> + '_, E> { + DualTableTraverse::::iter(&mut self.inner) + } + + /// Iterate all k2 entries within a single k1. + /// + /// The iterator yields `(k2, value)` pairs for the specified k1, starting + /// from the first k2 value, and stops when k1 changes or the table is + /// exhausted. + /// + /// Note: k1 is not included in the output since the caller already knows + /// it (they passed it in). This avoids redundant allocations. + pub fn iter_k2( + &mut self, + k1: &T::Key, + ) -> Result, E>> + '_, E> { + DualTableTraverse::::iter_k2(&mut self.inner, k1) + } +} + +impl DualTableCursor +where + C: DualTableTraverseMut, + T: DualKey, + E: HotKvReadError, +{ + /// Delete the current key-value pair. + pub fn delete_current(&mut self) -> Result<(), E> { + DualTableTraverseMut::::delete_current(&mut self.inner) + } + + /// Delete all K2 entries for the specified K1. + pub fn clear_k1(&mut self, key1: &T::Key) -> Result<(), E> { + DualTableTraverseMut::::clear_k1(&mut self.inner, key1) + } + + /// Delete a range of dual-keyed entries (inclusive). + pub fn delete_range(&mut self, range: RangeInclusive<(T::Key, T::Key2)>) -> Result<(), E> { + DualTableTraverseMut::::delete_range(&mut self.inner, range) + } + + /// Delete a range of dual-keyed entries and return the removed entries. + pub fn take_range( + &mut self, + range: RangeInclusive<(T::Key, T::Key2)>, + ) -> Result>, E> { + DualTableTraverseMut::::take_range(&mut self.inner, range) + } +} diff --git a/crates/hot/src/model/traverse/dual_key.rs b/crates/hot/src/model/traverse/dual_key.rs new file mode 100644 index 0000000..42f08fd --- /dev/null +++ b/crates/hot/src/model/traverse/dual_key.rs @@ -0,0 +1,194 @@ +//! Dual-key cursor traversal traits. + +use super::{ + HotKvReadError, RawDualKeyValue, RawValue, + iter::{RawDualKeyIter, RawDualKeyK2Iter}, + types::{RawDualKeyItem, RawK2Value}, +}; +use core::marker::PhantomData; + +/// Trait for traversing dual-keyed key-value pairs in the database. +pub trait DualKeyTraverse { + /// Set position to the first key-value pair in the database, and return + /// the KV pair with both keys. + fn first<'a>(&'a mut self) -> Result>, E>; + + /// Set position to the last key-value pair in the database, and return the + /// KV pair with both keys. + fn last<'a>(&'a mut self) -> Result>, E>; + + /// Get the next key-value pair in the database, and advance the cursor. + /// + /// Returning `Ok(None)` indicates the cursor is past the end of the + /// database. + fn read_next<'a>(&'a mut self) -> Result>, E>; + + /// Get the previous key-value pair in the database, and move the cursor. + /// + /// Returning `Ok(None)` indicates the cursor is before the start of the + /// database. + fn read_prev<'a>(&'a mut self) -> Result>, E>; + + /// Set the cursor to specific dual key in the database, and return the + /// EXACT KV pair if it exists. + /// + /// Returning `Ok(None)` indicates the exact dual key does not exist. + fn exact_dual<'a>(&'a mut self, key1: &[u8], key2: &[u8]) -> Result>, E>; + + /// Seek to the next key-value pair AT or ABOVE the specified dual key in + /// the database, and return that KV pair. + /// + /// Returning `Ok(None)` indicates there are no more key-value pairs above + /// the specified dual key. + fn next_dual_above<'a>( + &'a mut self, + key1: &[u8], + key2: &[u8], + ) -> Result>, E>; + + /// Move the cursor to the next distinct key1, and return the first + /// key-value pair with that key1. + /// + /// Returning `Ok(None)` indicates there are no more distinct key1 values. + fn next_k1<'a>(&'a mut self) -> Result>, E>; + + /// Move the cursor to the next distinct key2 for the current key1, and + /// return the first key-value pair with that key2. + fn next_k2<'a>(&'a mut self) -> Result>, E>; + + /// Seek to the LAST key2 entry for the specified key1. + /// + /// This positions the cursor at the last duplicate value for the given key1. + /// Returning `Ok(None)` indicates the key1 does not exist. + fn last_of_k1<'a>(&'a mut self, key1: &[u8]) -> Result>, E>; + + /// Move the cursor to the LAST key2 entry of the PREVIOUS key1. + /// + /// This is the reverse of `next_k1` - it moves backward to the previous distinct + /// key1 and positions at its last key2 entry. + /// Returning `Ok(None)` indicates there is no previous key1. + fn previous_k1<'a>(&'a mut self) -> Result>, E>; + + /// Move the cursor to the PREVIOUS key2 entry for the CURRENT key1. + /// + /// This is the reverse of `next_k2` - it moves backward within the current key1's + /// duplicate values. + /// Returning `Ok(None)` indicates there is no previous key2 for this key1. + fn previous_k2<'a>(&'a mut self) -> Result>, E>; + + /// Position at first entry and return iterator over all entries. + /// + /// The default implementation uses `first()` followed by repeated + /// `read_next()` calls. Implementations may override this to use + /// more efficient native iterators. + fn iter(&mut self) -> Result, E>> + '_, E> + where + Self: Sized, + { + self.first()?; + Ok(RawDualKeyIter { cursor: self, done: false, _marker: PhantomData }) + } + + /// Position at (k1, k2) and return iterator over subsequent entries. + /// + /// The iterator starts at the first entry with (k1, k2) >= the specified + /// keys and continues through all subsequent entries, crossing k1 boundaries. + fn iter_from<'a>( + &'a mut self, + k1: &[u8], + k2: &[u8], + ) -> Result, E>> + 'a, E> + where + Self: Sized, + { + self.next_dual_above(k1, k2)?; + Ok(RawDualKeyIter { cursor: self, done: false, _marker: PhantomData }) + } + + /// Iterate all k2 entries within a single k1. + /// + /// The iterator yields `(k2, value)` pairs for the specified k1, starting + /// from the first k2 value, and stops when k1 changes or the table is + /// exhausted. + /// + /// Note: k1 is not included in the output since the caller already knows + /// it (they passed it in). This avoids redundant allocations. + fn iter_k2<'a>( + &'a mut self, + k1: &[u8], + ) -> Result, E>> + 'a, E> + where + Self: Sized, + { + // Position at first entry for this k1 (using empty slice as minimum k2) + let entry = self.next_dual_above(k1, &[])?; + let Some((found_k1, _, _)) = entry else { + return Ok(RawDualKeyK2Iter { cursor: self, done: true, _marker: PhantomData }); + }; + // If the found k1 doesn't match, we're done + let done = found_k1.as_ref() != k1; + Ok(RawDualKeyK2Iter { cursor: self, done, _marker: PhantomData }) + } + + /// Position at first entry and return iterator yielding [`DualKeyItem`]. + /// + /// This is more efficient than [`Self::iter()`] as it avoids cloning k1 for + /// every entry within the same k1 group. The iterator yields + /// [`DualKeyItem::NewK1`] when k1 changes and [`DualKeyItem::SameK1`] for + /// subsequent entries with the same k1. + /// + /// Backends implement this natively to leverage efficient "new key" + /// notifications from the underlying database. + /// + /// [`DualKeyItem`]: super::DualKeyItem + fn iter_items(&mut self) -> Result, E>> + '_, E> + where + Self: Sized; +} + +/// Trait for traversing dual-keyed key-value pairs with mutation capabilities. +pub trait DualKeyTraverseMut: DualKeyTraverse { + /// Delete all K2 entries for the specified K1. + /// + /// This positions the cursor at the given K1 and removes all associated + /// K2 entries in a single operation. + /// + /// Returns `Ok(())` if the K1 was cleared or didn't exist. + fn clear_k1(&mut self, key1: &[u8]) -> Result<(), E>; + + /// Delete the current dual-keyed entry. + fn delete_current(&mut self) -> Result<(), E>; + + /// Delete a range of dual-keyed entries (inclusive). + /// + /// Deletes all entries where `(k1, k2) >= (start_k1, start_k2)` and + /// `(k1, k2) <= (end_k1, end_k2)`. + fn delete_range( + &mut self, + start_k1: &[u8], + start_k2: &[u8], + end_k1: &[u8], + end_k2: &[u8], + ) -> Result<(), E> { + let Some((k1, k2, _)) = self.next_dual_above(start_k1, start_k2)? else { + return Ok(()); + }; + + // Check if first entry is past end of range + if k1.as_ref() > end_k1 || (k1.as_ref() == end_k1 && k2.as_ref() > end_k2) { + return Ok(()); + } + + self.delete_current()?; + + while let Some((k1, k2, _)) = self.read_next()? { + // Check if we're past end of range + if k1.as_ref() > end_k1 || (k1.as_ref() == end_k1 && k2.as_ref() > end_k2) { + break; + } + self.delete_current()?; + } + + Ok(()) + } +} diff --git a/crates/hot/src/model/traverse/dual_table.rs b/crates/hot/src/model/traverse/dual_table.rs new file mode 100644 index 0000000..d3eca6e --- /dev/null +++ b/crates/hot/src/model/traverse/dual_table.rs @@ -0,0 +1,284 @@ +//! Typed dual-key table traversal traits. + +use super::{ + DualKeyValue, HotKvReadError, + dual_key::{DualKeyTraverse, DualKeyTraverseMut}, + iter::DualTableK2Iter, + types::K2Value, +}; +use crate::{ser::KeySer, ser::MAX_KEY_SIZE, tables::DualKey}; +use core::marker::PhantomData; +use std::ops::RangeInclusive; + +/// A typed cursor wrapper for traversing dual-keyed tables. +/// +/// This is an extension trait rather than a wrapper struct because MDBX +/// requires specialized implementations for DUPSORT tables that need access +/// to the table type `T` to handle fixed-size values correctly. +pub trait DualTableTraverse: DualKeyTraverse { + /// Get the first key-value pair in the table. + fn first(&mut self) -> Result>, E> { + DualKeyTraverse::first(self)?.map(T::decode_kkv_tuple).transpose().map_err(Into::into) + } + + /// Get the last key-value pair in the table. + fn last(&mut self) -> Result>, E> { + DualKeyTraverse::last(self)?.map(T::decode_kkv_tuple).transpose().map_err(Into::into) + } + + /// Get the next key-value pair and advance the cursor. + fn read_next(&mut self) -> Result>, E> { + DualKeyTraverse::read_next(self)?.map(T::decode_kkv_tuple).transpose().map_err(Into::into) + } + + /// Get the previous key-value pair and move the cursor backward. + fn read_prev(&mut self) -> Result>, E> { + DualKeyTraverse::read_prev(self)?.map(T::decode_kkv_tuple).transpose().map_err(Into::into) + } + + /// Return the EXACT value for the specified dual key if it exists. + fn exact_dual(&mut self, key1: &T::Key, key2: &T::Key2) -> Result, E> { + let Some((k1, k2, v)) = DualTableTraverse::next_dual_above(self, key1, key2)? else { + return Ok(None); + }; + + if k1 == *key1 && k2 == *key2 { Ok(Some(v)) } else { Ok(None) } + } + + /// Seek to the next key-value pair AT or ABOVE the specified dual key. + fn next_dual_above( + &mut self, + key1: &T::Key, + key2: &T::Key2, + ) -> Result>, E>; + + /// Seek to the next distinct key1, and return the first key-value pair with that key1. + fn next_k1(&mut self) -> Result>, E>; + + /// Seek to the next distinct key2 for the current key1. + fn next_k2(&mut self) -> Result>, E>; + + /// Seek to the LAST key2 entry for the specified key1. + fn last_of_k1(&mut self, key1: &T::Key) -> Result>, E>; + + /// Move to the LAST key2 entry of the PREVIOUS key1. + fn previous_k1(&mut self) -> Result>, E>; + + /// Move to the PREVIOUS key2 entry for the CURRENT key1. + fn previous_k2(&mut self) -> Result>, E>; + + /// Position at (k1, k2) and iterate forward, crossing k1 boundaries. + /// + /// The iterator starts at the first entry with (k1, k2) >= the specified + /// keys and continues through all subsequent entries. + fn iter_from( + &mut self, + k1: &T::Key, + k2: &T::Key2, + ) -> Result, E>> + '_, E> + where + T::Key: PartialEq; + + /// Position at first entry and return iterator over all entries. + fn iter(&mut self) -> Result, E>> + '_, E> + where + T::Key: PartialEq; + + /// Iterate all k2 entries within a single k1. + /// + /// The iterator yields `(k2, value)` pairs for the specified k1, starting + /// from the first k2 value, and stops when k1 changes or the table is + /// exhausted. + /// + /// Note: k1 is not included in the output since the caller already knows + /// it (they passed it in). This avoids redundant allocations. + fn iter_k2( + &mut self, + k1: &T::Key, + ) -> Result, E>> + '_, E> + where + T::Key: PartialEq; +} + +impl DualTableTraverse for C +where + C: DualKeyTraverse, + T: DualKey, + E: HotKvReadError, +{ + fn next_dual_above( + &mut self, + key1: &T::Key, + key2: &T::Key2, + ) -> Result>, E> { + let mut key1_buf = [0u8; MAX_KEY_SIZE]; + let mut key2_buf = [0u8; MAX_KEY_SIZE]; + let key1_bytes = key1.encode_key(&mut key1_buf); + let key2_bytes = key2.encode_key(&mut key2_buf); + + DualKeyTraverse::next_dual_above(self, key1_bytes, key2_bytes)? + .map(T::decode_kkv_tuple) + .transpose() + .map_err(Into::into) + } + + fn next_k1(&mut self) -> Result>, E> { + DualKeyTraverse::next_k1(self)?.map(T::decode_kkv_tuple).transpose().map_err(Into::into) + } + + fn next_k2(&mut self) -> Result>, E> { + DualKeyTraverse::next_k2(self)?.map(T::decode_kkv_tuple).transpose().map_err(Into::into) + } + + fn last_of_k1(&mut self, key1: &T::Key) -> Result>, E> { + let mut key1_buf = [0u8; MAX_KEY_SIZE]; + let key1_bytes = key1.encode_key(&mut key1_buf); + + DualKeyTraverse::last_of_k1(self, key1_bytes)? + .map(T::decode_kkv_tuple) + .transpose() + .map_err(Into::into) + } + + fn previous_k1(&mut self) -> Result>, E> { + DualKeyTraverse::previous_k1(self)?.map(T::decode_kkv_tuple).transpose().map_err(Into::into) + } + + fn previous_k2(&mut self) -> Result>, E> { + DualKeyTraverse::previous_k2(self)?.map(T::decode_kkv_tuple).transpose().map_err(Into::into) + } + + fn iter_from( + &mut self, + k1: &T::Key, + k2: &T::Key2, + ) -> Result, E>> + '_, E> + where + T::Key: PartialEq, + { + // Position cursor at (k1, k2) + DualTableTraverse::::next_dual_above(self, k1, k2)?; + // Return iterator that decodes from raw + Ok(DualKeyTraverse::iter(self)? + .map(|r| r.and_then(|kkv| T::decode_kkv_tuple(kkv).map_err(Into::into)))) + } + + fn iter(&mut self) -> Result, E>> + '_, E> + where + T::Key: PartialEq, + { + Ok(DualKeyTraverse::iter(self)? + .map(|r| r.and_then(|kkv| T::decode_kkv_tuple(kkv).map_err(Into::into)))) + } + + fn iter_k2( + &mut self, + k1: &T::Key, + ) -> Result, E>> + '_, E> + where + T::Key: PartialEq, + { + // Position cursor at first entry for this k1 using raw interface with empty k2 + let mut key1_buf = [0u8; MAX_KEY_SIZE]; + let key1_bytes = k1.encode_key(&mut key1_buf); + let entry = DualKeyTraverse::next_dual_above(self, key1_bytes, &[])?; + let Some((found_k1, _, _)) = entry else { + return Ok(DualTableK2Iter::<'_, C, T, E> { + cursor: self, + done: true, + _marker: PhantomData, + }); + }; + // Decode the found k1 to check if it matches + let decoded_k1 = T::decode_key(found_k1)?; + // If the found k1 doesn't match, we're done + let done = decoded_k1 != *k1; + Ok(DualTableK2Iter::<'_, C, T, E> { cursor: self, done, _marker: PhantomData }) + } +} + +/// Extension trait for typed dual table traversal with mutation capabilities. +pub trait DualTableTraverseMut: DualKeyTraverseMut { + /// Delete all K2 entries for the specified K1. + fn clear_k1(&mut self, key1: &T::Key) -> Result<(), E> { + let mut key1_buf = [0u8; MAX_KEY_SIZE]; + let key1_bytes = key1.encode_key(&mut key1_buf); + DualKeyTraverseMut::clear_k1(self, key1_bytes) + } + + /// Delete the current dual-keyed entry. + fn delete_current(&mut self) -> Result<(), E> { + DualKeyTraverseMut::delete_current(self) + } + + /// Delete a range of dual-keyed entries (inclusive). + fn delete_range(&mut self, range: RangeInclusive<(T::Key, T::Key2)>) -> Result<(), E> + where + Self: Sized, + { + let (start_k1, start_k2) = range.start(); + let (end_k1, end_k2) = range.end(); + + // Loop using next_dual_above to handle backends where delete auto-advances + // the cursor (e.g., MDBX) vs those that don't (e.g., in-memory). + loop { + let Some((k1, k2, _)) = + DualTableTraverse::::next_dual_above(self, start_k1, start_k2)? + else { + break; + }; + + // Check if entry is past end of range (typed comparison) + if &k1 > end_k1 || (&k1 == end_k1 && &k2 > end_k2) { + break; + } + + DualKeyTraverseMut::delete_current(self)?; + } + + Ok(()) + } + + /// Delete a range of dual-keyed entries and return the removed entries. + fn take_range( + &mut self, + range: RangeInclusive<(T::Key, T::Key2)>, + ) -> Result>, E> + where + Self: Sized, + { + let (start_k1, start_k2) = range.start(); + let (end_k1, end_k2) = range.end(); + + let mut result = Vec::new(); + + // Loop using next_dual_above to handle backends where delete auto-advances + // the cursor (e.g., MDBX) vs those that don't (e.g., in-memory). + loop { + let Some((k1, k2, value)) = + DualTableTraverse::::next_dual_above(self, start_k1, start_k2)? + else { + break; + }; + + // Check if entry is past end of range (typed comparison) + if &k1 > end_k1 || (&k1 == end_k1 && &k2 > end_k2) { + break; + } + + result.push((k1, k2, value)); + DualKeyTraverseMut::delete_current(self)?; + } + + Ok(result) + } +} + +/// Blanket implementation of `DualTableTraverseMut`. +impl DualTableTraverseMut for C +where + C: DualKeyTraverseMut, + T: DualKey, + E: HotKvReadError, +{ +} diff --git a/crates/hot/src/model/traverse/iter.rs b/crates/hot/src/model/traverse/iter.rs new file mode 100644 index 0000000..05fc435 --- /dev/null +++ b/crates/hot/src/model/traverse/iter.rs @@ -0,0 +1,190 @@ +//! Iterator structs for cursor traversal. + +use super::{ + HotKvReadError, RawDualKeyValue, RawKeyValue, + dual_key::DualKeyTraverse, + dual_table::DualTableTraverse, + kv::KvTraverse, + types::{K2Value, RawK2Value}, +}; +use crate::tables::DualKey; +use core::marker::PhantomData; + +// ============================================================================ +// Raw Iterator Structs (for base traits) +// ============================================================================ + +/// Default forward iterator over raw key-value entries. +/// +/// This iterator wraps a cursor implementing `KvTraverse` and yields entries +/// by calling `read_next` on each iteration. +pub struct RawKvIter<'a, C, E> { + pub(super) cursor: &'a mut C, + pub(super) done: bool, + pub(super) _marker: PhantomData E>, +} + +impl<'a, C, E> Iterator for RawKvIter<'a, C, E> +where + C: KvTraverse, + E: HotKvReadError, +{ + type Item = Result, E>; + + fn next(&mut self) -> Option { + if self.done { + return None; + } + // SAFETY: We're using a mutable borrow of the cursor, so the lifetime + // of the returned data is correctly tied to this iterator. + // We transmute the lifetime because the borrow checker can't see + // that the cursor's internal state keeps the data alive. + let result = unsafe { + std::mem::transmute::< + Result>, E>, + Result>, E>, + >(self.cursor.read_next()) + }; + match result { + Ok(Some(kv)) => Some(Ok(kv)), + Ok(None) => { + self.done = true; + None + } + Err(e) => { + self.done = true; + Some(Err(e)) + } + } + } +} + +/// Default forward iterator over raw dual-keyed entries. +/// +/// This iterator wraps a cursor implementing `DualKeyTraverse` and yields +/// entries by calling `read_next` on each iteration. +pub struct RawDualKeyIter<'a, C, E> { + pub(super) cursor: &'a mut C, + pub(super) done: bool, + pub(super) _marker: PhantomData E>, +} + +impl<'a, C, E> Iterator for RawDualKeyIter<'a, C, E> +where + C: DualKeyTraverse, + E: HotKvReadError, +{ + type Item = Result, E>; + + fn next(&mut self) -> Option { + if self.done { + return None; + } + // SAFETY: Same rationale as RawKvIter - the cursor's internal state + // keeps the data alive for the duration of iteration. + let result = unsafe { + std::mem::transmute::< + Result>, E>, + Result>, E>, + >(self.cursor.read_next()) + }; + match result { + Ok(Some(kv)) => Some(Ok(kv)), + Ok(None) => { + self.done = true; + None + } + Err(e) => { + self.done = true; + Some(Err(e)) + } + } + } +} + +/// Default forward iterator over raw k2-value entries within a single k1. +/// +/// This iterator wraps a cursor implementing `DualKeyTraverse` and yields +/// `(k2, value)` pairs while k1 remains unchanged. The iterator stops when k1 +/// changes or the table is exhausted. +pub struct RawDualKeyK2Iter<'a, C, E> { + pub(super) cursor: &'a mut C, + pub(super) done: bool, + pub(super) _marker: PhantomData E>, +} + +impl<'a, C, E> Iterator for RawDualKeyK2Iter<'a, C, E> +where + C: DualKeyTraverse, + E: HotKvReadError, +{ + type Item = Result, E>; + + fn next(&mut self) -> Option { + if self.done { + return None; + } + // SAFETY: Same rationale as RawKvIter + let result = unsafe { + std::mem::transmute::< + Result>, E>, + Result>, E>, + >(self.cursor.next_k2()) + }; + match result { + Ok(Some((_k1, k2, v))) => Some(Ok((k2, v))), + Ok(None) => { + self.done = true; + None + } + Err(e) => { + self.done = true; + Some(Err(e)) + } + } + } +} + +// ============================================================================ +// Typed Iterator Structs (for extension traits) +// ============================================================================ + +/// Forward iterator over k2-value pairs within a single k1. +/// +/// This iterator wraps a cursor and yields `(k2, value)` pairs by calling +/// `next_k2()` on each iteration. The iterator stops when there are no more +/// k2 entries for the current k1 or when an error occurs. +pub struct DualTableK2Iter<'a, C, T, E> +where + T: DualKey, +{ + pub(super) cursor: &'a mut C, + pub(super) done: bool, + pub(super) _marker: PhantomData (T, E)>, +} + +impl<'a, C, T, E> Iterator for DualTableK2Iter<'a, C, T, E> +where + C: DualKeyTraverse, + T: DualKey, + E: HotKvReadError, +{ + type Item = Result, E>; + + fn next(&mut self) -> Option { + if self.done { + return None; + } + match DualTableTraverse::::next_k2(self.cursor) { + Ok(Some((_k1, k2, v))) => Some(Ok((k2, v))), + Ok(None) => { + self.done = true; + None + } + Err(e) => { + self.done = true; + Some(Err(e)) + } + } + } +} diff --git a/crates/hot/src/model/traverse/kv.rs b/crates/hot/src/model/traverse/kv.rs new file mode 100644 index 0000000..b563365 --- /dev/null +++ b/crates/hot/src/model/traverse/kv.rs @@ -0,0 +1,111 @@ +//! Single-key cursor traversal traits. + +use super::{HotKvReadError, RawKeyValue, RawValue, iter::RawKvIter}; +use core::marker::PhantomData; +use std::ops::Range; + +/// Trait for traversing key-value pairs in the database. +pub trait KvTraverse { + /// Set position to the first key-value pair in the database, and return + /// the KV pair. + fn first<'a>(&'a mut self) -> Result>, E>; + + /// Set position to the last key-value pair in the database, and return the + /// KV pair. + fn last<'a>(&'a mut self) -> Result>, E>; + + /// Set the cursor to specific key in the database, and return the EXACT KV + /// pair if it exists. + fn exact<'a>(&'a mut self, key: &[u8]) -> Result>, E>; + + /// Seek to the next key-value pair AT OR ABOVE the specified key in the + /// database, and return that KV pair. + fn lower_bound<'a>(&'a mut self, key: &[u8]) -> Result>, E>; + + /// Get the next key-value pair in the database, and advance the cursor. + /// + /// Returning `Ok(None)` indicates the cursor is past the end of the + /// database. + fn read_next<'a>(&'a mut self) -> Result>, E>; + + /// Get the previous key-value pair in the database, and move the cursor. + /// + /// Returning `Ok(None)` indicates the cursor is before the start of the + /// database. + fn read_prev<'a>(&'a mut self) -> Result>, E>; + + /// Position at first entry and return iterator over all entries. + /// + /// The default implementation uses `first()` followed by repeated + /// `read_next()` calls. Implementations may override this to use + /// more efficient native iterators. + fn iter(&mut self) -> Result, E>> + '_, E> + where + Self: Sized, + { + self.first()?; + Ok(RawKvIter { cursor: self, done: false, _marker: PhantomData }) + } + + /// Position at `key` and return iterator over subsequent entries. + /// + /// The iterator starts at the first entry with key >= `key`. + /// The default implementation uses `lower_bound()` followed by repeated + /// `read_next()` calls. Implementations may override this to use + /// more efficient native iterators. + fn iter_from<'a>( + &'a mut self, + key: &[u8], + ) -> Result, E>> + 'a, E> + where + Self: Sized, + { + self.lower_bound(key)?; + Ok(RawKvIter { cursor: self, done: false, _marker: PhantomData }) + } +} + +/// Trait for traversing key-value pairs in the database with mutation +/// capabilities. +pub trait KvTraverseMut: KvTraverse { + /// Delete the current key-value pair in the database. + fn delete_current(&mut self) -> Result<(), E>; + + /// Delete a range of key-value pairs in the database (exclusive end). + fn delete_range(&mut self, range: Range<&[u8]>) -> Result<(), E> { + let Some((key, _)) = self.lower_bound(range.start)? else { + return Ok(()); + }; + if key.as_ref() >= range.end { + return Ok(()); + } + self.delete_current()?; + + while let Some((key, _)) = self.read_next()? { + if key.as_ref() >= range.end { + break; + } + self.delete_current()?; + } + Ok(()) + } + + /// Delete a range of key-value pairs in the database (inclusive end). + fn delete_range_inclusive(&mut self, start: &[u8], end: &[u8]) -> Result<(), E> { + let Some((key, _)) = self.lower_bound(start)? else { + return Ok(()); + }; + if key.as_ref() > end { + return Ok(()); + } + self.delete_current()?; + + while let Some((key, _)) = self.read_next()? { + if key.as_ref() > end { + break; + } + self.delete_current()?; + } + Ok(()) + } +} diff --git a/crates/hot/src/model/traverse/mod.rs b/crates/hot/src/model/traverse/mod.rs new file mode 100644 index 0000000..ff3637c --- /dev/null +++ b/crates/hot/src/model/traverse/mod.rs @@ -0,0 +1,101 @@ +//! Cursor traversal traits and typed wrappers for database navigation. +//! +//! This module provides a layered abstraction for database cursor traversal: +//! +//! 1. **Raw traits** (`KvTraverse`, `DualKeyTraverse`) operate on raw bytes +//! 2. **Typed extension traits** (`TableTraverse`, `DualTableTraverse`) add +//! automatic (de)serialization +//! 3. **Cursor wrappers** (`TableCursor`, `DualTableCursor`) provide convenient +//! method-based access +//! +//! ## Type Hierarchy +//! +//! | Category | Item | Description | +//! |----------|------|-------------| +//! | **Raw Traits** | | | +//! | | [`KvTraverse`] | Single-key cursor traversal | +//! | | [`KvTraverseMut`] | Single-key traversal + mutation | +//! | | [`DualKeyTraverse`] | Dual-key cursor traversal | +//! | | [`DualKeyTraverseMut`] | Dual-key traversal + mutation | +//! | **Typed Extension Traits** | | | +//! | | [`TableTraverse`] | Typed single-key traversal (blanket impl) | +//! | | [`TableTraverseMut`] | Typed single-key mutation (blanket impl) | +//! | | [`DualTableTraverse`] | Typed dual-key traversal (blanket impl) | +//! | | [`DualTableTraverseMut`] | Typed dual-key mutation (blanket impl) | +//! | **Cursor Wrappers** | | | +//! | | [`TableCursor`] | Typed wrapper for single-key cursors | +//! | | [`DualTableCursor`] | Typed wrapper for dual-key cursors | +//! | **Types** | | | +//! | | [`DualKeyItem`] | Enum avoiding k1 clones in iteration | +//! | | [`RawK2Value`] | Raw (k2, value) pair type alias | +//! | | [`K2Value`] | Typed (k2, value) pair type alias | +//! | | [`RawDualKeyItem`] | Raw dual-key item type alias | +//! +//! ## Implementation Guide +//! +//! ### Required Methods (must implement) +//! +//! **`KvTraverse`:** +//! - `first`, `last`, `exact`, `lower_bound`, `read_next`, `read_prev` +//! +//! **`KvTraverseMut`:** +//! - `delete_current` +//! +//! **`DualKeyTraverse`:** +//! - `first`, `last`, `read_next`, `read_prev`, `exact_dual`, `next_dual_above` +//! - `next_k1`, `next_k2`, `last_of_k1`, `previous_k1`, `previous_k2`, +//! `iter_items` +//! +//! **`DualKeyTraverseMut`:** +//! - `clear_k1`, `delete_current` +//! +//! ### Optional Methods (can override defaults) +//! +//! **`KvTraverse`:** +//! - `iter` - default uses `first()` + repeated `read_next()` +//! - `iter_from` - default uses `lower_bound()` + repeated `read_next()` +//! +//! **`KvTraverseMut`:** +//! - `delete_range` - default iterates and deletes +//! - `delete_range_inclusive` - default iterates and deletes +//! +//! **`DualKeyTraverse`:** +//! - `iter` - default uses `first()` + repeated `read_next()` +//! - `iter_from` - default uses `next_dual_above()` + repeated `read_next()` +//! - `iter_k2` - default uses `next_dual_above()` + repeated `next_k2()` +//! +//! **`DualKeyTraverseMut`:** +//! - `delete_range` - default iterates and deletes +//! +//! ### Blanket Implementations +//! +//! The typed extension traits have blanket implementations: +//! - `impl TableTraverse for C where C: KvTraverse, T: +//! SingleKey` +//! - `impl TableTraverseMut for C where C: KvTraverseMut, T: +//! SingleKey` +//! - `impl DualTableTraverse for C where C: DualKeyTraverse, +//! T: DualKey` +//! - `impl DualTableTraverseMut for C where C: +//! DualKeyTraverseMut, T: DualKey` +//! +//! Implementations only need to implement the raw traits to get typed access. + +mod cursor; +mod dual_key; +mod dual_table; +mod iter; +mod kv; +mod table; +mod types; + +// Re-export types from parent module needed by submodules +use super::{DualKeyValue, HotKvReadError, KeyValue, RawDualKeyValue, RawKeyValue, RawValue}; + +// Re-export all public items +pub use cursor::{DualTableCursor, TableCursor}; +pub use dual_key::{DualKeyTraverse, DualKeyTraverseMut}; +pub use dual_table::{DualTableTraverse, DualTableTraverseMut}; +pub use kv::{KvTraverse, KvTraverseMut}; +pub use table::{TableTraverse, TableTraverseMut}; +pub use types::{DualKeyItem, K2Value, RawDualKeyItem, RawK2Value}; diff --git a/crates/hot/src/model/traverse/table.rs b/crates/hot/src/model/traverse/table.rs new file mode 100644 index 0000000..01b1611 --- /dev/null +++ b/crates/hot/src/model/traverse/table.rs @@ -0,0 +1,161 @@ +//! Typed single-key table traversal traits. + +use super::{HotKvReadError, KeyValue, kv::KvTraverse, kv::KvTraverseMut}; +use crate::{ser::KeySer, ser::MAX_KEY_SIZE, tables::SingleKey}; +use std::ops::{Range, RangeInclusive}; + +/// Extension trait for typed table traversal. +/// +/// This trait provides type-safe access to table entries by encoding keys +/// and decoding values according to the table's schema. +pub trait TableTraverse: KvTraverse { + /// Get the first key-value pair in the table. + fn first(&mut self) -> Result>, E> { + KvTraverse::first(self)?.map(T::decode_kv_tuple).transpose().map_err(Into::into) + } + + /// Get the last key-value pair in the table. + fn last(&mut self) -> Result>, E> { + KvTraverse::last(self)?.map(T::decode_kv_tuple).transpose().map_err(Into::into) + } + + /// Set the cursor to a specific key and return the EXACT value if it exists. + fn exact(&mut self, key: &T::Key) -> Result, E> { + let mut key_buf = [0u8; MAX_KEY_SIZE]; + let key_bytes = key.encode_key(&mut key_buf); + + KvTraverse::exact(self, key_bytes)?.map(T::decode_value).transpose().map_err(Into::into) + } + + /// Seek to the next key-value pair AT OR ABOVE the specified key. + fn lower_bound(&mut self, key: &T::Key) -> Result>, E> { + let mut key_buf = [0u8; MAX_KEY_SIZE]; + let key_bytes = key.encode_key(&mut key_buf); + + KvTraverse::lower_bound(self, key_bytes)? + .map(T::decode_kv_tuple) + .transpose() + .map_err(Into::into) + } + + /// Get the next key-value pair and advance the cursor. + fn read_next(&mut self) -> Result>, E> { + KvTraverse::read_next(self)?.map(T::decode_kv_tuple).transpose().map_err(Into::into) + } + + /// Get the previous key-value pair and move the cursor backward. + fn read_prev(&mut self) -> Result>, E> { + KvTraverse::read_prev(self)?.map(T::decode_kv_tuple).transpose().map_err(Into::into) + } + + /// Position at `key` and return iterator over subsequent entries. + /// + /// The iterator starts at the first entry with key >= `key`. + fn iter_from( + &mut self, + key: &T::Key, + ) -> Result, E>> + '_, E> + where + Self: Sized, + { + // Position the cursor at the key + TableTraverse::::lower_bound(self, key)?; + // Return iterator that decodes from raw + Ok(KvTraverse::iter(self)? + .map(|r| r.and_then(|kv| T::decode_kv_tuple(kv).map_err(Into::into)))) + } + + /// Position at first entry and return iterator over all entries. + fn iter(&mut self) -> Result, E>> + '_, E> + where + Self: Sized, + { + Ok(KvTraverse::iter(self)? + .map(|r| r.and_then(|kv| T::decode_kv_tuple(kv).map_err(Into::into)))) + } +} + +/// Blanket implementation of `TableTraverse` for any cursor that implements `KvTraverse`. +impl TableTraverse for C +where + C: KvTraverse, + T: SingleKey, + E: HotKvReadError, +{ +} + +/// Extension trait for typed table traversal with mutation capabilities. +pub trait TableTraverseMut: KvTraverseMut { + /// Delete the current key-value pair. + fn delete_current(&mut self) -> Result<(), E> { + KvTraverseMut::delete_current(self) + } + + /// Delete a range of key-value pairs (exclusive end). + fn delete_range(&mut self, range: Range) -> Result<(), E> { + let mut start_key_buf = [0u8; MAX_KEY_SIZE]; + let mut end_key_buf = [0u8; MAX_KEY_SIZE]; + let start_key_bytes = range.start.encode_key(&mut start_key_buf); + let end_key_bytes = range.end.encode_key(&mut end_key_buf); + + KvTraverseMut::delete_range(self, start_key_bytes..end_key_bytes) + } + + /// Delete a range of key-value pairs (inclusive end). + fn delete_range_inclusive(&mut self, range: RangeInclusive) -> Result<(), E> + where + Self: Sized, + { + let Some((key, _)) = TableTraverse::::lower_bound(self, range.start())? else { + return Ok(()); + }; + if !range.contains(&key) { + return Ok(()); + } + KvTraverseMut::delete_current(self)?; + + while let Some((key, _)) = TableTraverse::::read_next(self)? { + if !range.contains(&key) { + break; + } + KvTraverseMut::delete_current(self)?; + } + Ok(()) + } + + /// Delete a range of key-value pairs and return the removed entries. + fn take_range(&mut self, range: RangeInclusive) -> Result>, E> + where + Self: Sized, + { + let mut result = Vec::new(); + + let Some((key, value)) = TableTraverse::::lower_bound(self, range.start())? else { + return Ok(result); + }; + if !range.contains(&key) { + return Ok(result); + } + result.push((key, value)); + KvTraverseMut::delete_current(self)?; + + while let Some((key, value)) = TableTraverse::::read_next(self)? { + if !range.contains(&key) { + break; + } + result.push((key, value)); + KvTraverseMut::delete_current(self)?; + } + + Ok(result) + } +} + +/// Blanket implementation of [`TableTraverseMut`] for any cursor that implements [`KvTraverseMut`]. +impl TableTraverseMut for C +where + C: KvTraverseMut, + T: SingleKey, + E: HotKvReadError, +{ +} diff --git a/crates/hot/src/model/traverse/types.rs b/crates/hot/src/model/traverse/types.rs new file mode 100644 index 0000000..ea05f7c --- /dev/null +++ b/crates/hot/src/model/traverse/types.rs @@ -0,0 +1,79 @@ +//! Type definitions for dual-key traversal. + +use crate::tables::DualKey; +use std::borrow::Cow; + +/// Raw k2-value pair: (key2, value). +/// +/// This is returned by `iter_k2()` on dual-keyed tables. The caller already +/// knows k1 (they passed it in), so we don't return it redundantly. +pub type RawK2Value<'a> = (Cow<'a, [u8]>, super::RawValue<'a>); + +/// Typed k2-value pair for a dual-keyed table. +pub type K2Value = (::Key2, ::Value); + +/// An item from a dual-key iterator. +/// +/// This enum avoids cloning k1 for every value when iterating +/// over dual-keyed tables. K1 is only provided when it changes. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DualKeyItem { + /// First entry for a new k1. + NewK1(K1, K2, V), + /// Additional k2/value for the current k1. + SameK1(K2, V), +} + +impl DualKeyItem { + /// Returns the value, consuming self. + pub fn into_value(self) -> V { + match self { + Self::NewK1(_, _, v) | Self::SameK1(_, v) => v, + } + } + + /// Returns a reference to the value. + pub const fn value(&self) -> &V { + match self { + Self::NewK1(_, _, v) | Self::SameK1(_, v) => v, + } + } + + /// Returns the k2, consuming self. + pub fn into_k2(self) -> K2 { + match self { + Self::NewK1(_, k2, _) | Self::SameK1(k2, _) => k2, + } + } + + /// Returns a reference to k2. + pub const fn k2(&self) -> &K2 { + match self { + Self::NewK1(_, k2, _) | Self::SameK1(k2, _) => k2, + } + } + + /// Returns k1 if this is a NewK1 entry. + pub const fn k1(&self) -> Option<&K1> { + match self { + Self::NewK1(k1, _, _) => Some(k1), + Self::SameK1(_, _) => None, + } + } + + /// Returns true if this item represents a new k1. + pub const fn is_new_k1(&self) -> bool { + matches!(self, Self::NewK1(..)) + } + + /// Convert to a tuple, requiring k1 to be provided for SameK1 variants. + pub fn into_tuple(self, current_k1: K1) -> (K1, K2, V) { + match self { + Self::NewK1(k1, k2, v) => (k1, k2, v), + Self::SameK1(k2, v) => (current_k1, k2, v), + } + } +} + +/// Raw dual-key item: (k1?, k2, value) where k1 is only present on NewK1. +pub type RawDualKeyItem<'a> = DualKeyItem, Cow<'a, [u8]>, Cow<'a, [u8]>>; From eeafc53c037ee3b793c9fe0cf0c99afb52a14fee Mon Sep 17 00:00:00 2001 From: James Date: Fri, 30 Jan 2026 09:16:21 -0500 Subject: [PATCH 6/6] fix: remove 'migration' path --- crates/hot-mdbx/src/tx.rs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/crates/hot-mdbx/src/tx.rs b/crates/hot-mdbx/src/tx.rs index 67c0e55..ac508a6 100644 --- a/crates/hot-mdbx/src/tx.rs +++ b/crates/hot-mdbx/src/tx.rs @@ -51,17 +51,7 @@ impl Tx { .map_err(MdbxError::from)? .ok_or(MdbxError::UnknownTable(name))?; - // Migration: handle both old (16 byte) and new (8 byte) formats - let fsi = if data.len() == 16 { - // Old format: skip dbi (4) + flags (4), read FixedSizeInfo from offset 8 - FixedSizeInfo::decode_value(&data[8..16]) - } else { - // New format: 8 bytes of FixedSizeInfo - FixedSizeInfo::decode_value(&data) - } - .map_err(MdbxError::Deser)?; - - Ok(fsi) + FixedSizeInfo::decode_value(&data).map_err(MdbxError::Deser) } /// Gets cached FixedSizeInfo for a table.