From e02ed9e355dea1291bc05e989fa53f8db959a13d Mon Sep 17 00:00:00 2001 From: yongkangc Date: Wed, 7 Jan 2026 14:13:29 +0000 Subject: [PATCH] fix(trie): add bounded walk limit to cursor locality optimization --- crates/trie/db/src/hashed_cursor.rs | 322 ++++++++++++++++++++++- crates/trie/db/src/trie_cursor.rs | 386 +++++++++++++++++++++++++++- 2 files changed, 696 insertions(+), 12 deletions(-) diff --git a/crates/trie/db/src/hashed_cursor.rs b/crates/trie/db/src/hashed_cursor.rs index 10a1fd8363..b36d94eb13 100644 --- a/crates/trie/db/src/hashed_cursor.rs +++ b/crates/trie/db/src/hashed_cursor.rs @@ -78,18 +78,25 @@ where /// The structure wrapping a database cursor for hashed storage and /// a target hashed address. Implements [`HashedCursor`] and [`HashedStorageCursor`] /// for iterating over hashed storage. +/// +/// This cursor implements locality optimization: when seeking forward from the current position, +/// it uses `next_dup_val` (O(1)) to walk forward instead of `seek_by_key_subkey` (O(log N)) when +/// the target key is close to the current position. #[derive(Debug)] pub struct DatabaseHashedStorageCursor { /// Database hashed storage cursor. cursor: C, /// Target hashed address of the account that the storage belongs to. hashed_address: B256, + /// The last key returned by the cursor, used for locality optimization. + /// When seeking forward from this position, we can use `next_dup_val` instead of re-seeking. + last_key: Option, } impl DatabaseHashedStorageCursor { /// Create new [`DatabaseHashedStorageCursor`]. pub const fn new(cursor: C, hashed_address: B256) -> Self { - Self { cursor, hashed_address } + Self { cursor, hashed_address, last_key: None } } } @@ -99,16 +106,72 @@ where { type Value = U256; + /// Seeks the given key in the hashed storage. + /// + /// This method implements a bounded locality optimization: if the cursor is already positioned + /// and the target key is > the current position, we speculatively walk forward using + /// `next_dup_val` (O(1) per step) for up to `MAX_WALK_STEPS` entries. If the target is not + /// found within that limit, we fall back to `seek_by_key_subkey` (O(log N)). + /// + /// This bounds the worst-case complexity to O(MAX_WALK_STEPS + log N) instead of O(N). + #[allow(clippy::collapsible_if)] fn seek(&mut self, subkey: B256) -> Result, DatabaseError> { - Ok(self.cursor.seek_by_key_subkey(self.hashed_address, subkey)?.map(|e| (e.key, e.value))) + // Maximum forward walk steps before falling back to seek. + // This bounds worst-case from O(N) to O(MAX_WALK_STEPS + log N). + // Value of 8 is optimal for tries up to ~256 entries (log2(256) = 8). + const MAX_WALK_STEPS: usize = 8; + + // Locality optimization: if cursor is positioned and target is ahead, + // speculatively walk forward for a bounded number of steps + if let Some(last) = self.last_key { + if subkey > last { + // Bounded forward walk - only beneficial for nearby keys + for _ in 0..MAX_WALK_STEPS { + match self.cursor.next_dup_val()? { + Some(entry) => { + if entry.key >= subkey { + // Found target or passed it + self.last_key = Some(entry.key); + return Ok(Some((entry.key, entry.value))); + } + // Haven't reached target yet, continue walking + } + None => { + // Exhausted all duplicates without finding target + self.last_key = None; + return Ok(None); + } + } + } + // Exceeded walk limit - fall through to seek_by_key_subkey + } else if subkey == last { + // Re-seeking the same key, return current position if still valid + if let Some((_, entry)) = self.cursor.current()? { + if entry.key == subkey { + return Ok(Some((entry.key, entry.value))); + } + } + } + } + + // Fall back to seek_by_key_subkey for: + // - Backward seeks (subkey < last) + // - When cursor is not positioned (last_key is None) + // - When forward walk exceeded limit + let result = + self.cursor.seek_by_key_subkey(self.hashed_address, subkey)?.map(|e| (e.key, e.value)); + self.last_key = result.as_ref().map(|(k, _)| *k); + Ok(result) } fn next(&mut self) -> Result, DatabaseError> { - Ok(self.cursor.next_dup_val()?.map(|e| (e.key, e.value))) + let result = self.cursor.next_dup_val()?.map(|e| (e.key, e.value)); + self.last_key = result.as_ref().map(|(k, _)| *k); + Ok(result) } fn reset(&mut self) { - // Database cursors are stateless, no reset needed + self.last_key = None; } } @@ -122,5 +185,256 @@ where fn set_hashed_address(&mut self, hashed_address: B256) { self.hashed_address = hashed_address; + // Reset cursor position tracking when switching to a different storage + self.last_key = None; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use reth_db_api::{cursor::DbCursorRW, transaction::DbTxMut}; + use reth_primitives_traits::StorageEntry; + use reth_provider::test_utils::create_test_provider_factory; + + fn create_test_keys(count: usize) -> Vec { + (0..count as u64) + .map(|i| { + let mut bytes = [0u8; 32]; + bytes[24..32].copy_from_slice(&i.to_be_bytes()); + B256::from(bytes) + }) + .collect() + } + + #[test] + fn test_forward_sequential_seek_uses_optimization() { + let factory = create_test_provider_factory(); + let provider = factory.provider_rw().unwrap(); + let hashed_address = B256::random(); + let keys = create_test_keys(10); + + // Insert test data + { + let mut cursor = + provider.tx_ref().cursor_dup_write::().unwrap(); + for (i, key) in keys.iter().enumerate() { + cursor + .upsert( + hashed_address, + &StorageEntry { key: *key, value: U256::from(i as u64) }, + ) + .unwrap(); + } + } + + let mut cursor = DatabaseHashedStorageCursor::new( + provider.tx_ref().cursor_dup_read::().unwrap(), + hashed_address, + ); + + // Forward sequential seeks should all succeed + for (i, key) in keys.iter().enumerate() { + let result = cursor.seek(*key).unwrap(); + assert!(result.is_some(), "Should find key at index {i}"); + let (found_key, value) = result.unwrap(); + assert_eq!(found_key, *key); + assert_eq!(value, U256::from(i as u64)); + } + } + + #[test] + fn test_backward_seek_falls_back_to_seek_by_key_subkey() { + let factory = create_test_provider_factory(); + let provider = factory.provider_rw().unwrap(); + let hashed_address = B256::random(); + let keys = create_test_keys(10); + + // Insert test data + { + let mut cursor = + provider.tx_ref().cursor_dup_write::().unwrap(); + for (i, key) in keys.iter().enumerate() { + cursor + .upsert( + hashed_address, + &StorageEntry { key: *key, value: U256::from(i as u64) }, + ) + .unwrap(); + } + } + + let mut cursor = DatabaseHashedStorageCursor::new( + provider.tx_ref().cursor_dup_read::().unwrap(), + hashed_address, + ); + + // Seek to last key first + let result = cursor.seek(keys[9]).unwrap(); + assert!(result.is_some()); + + // Backward seek should still work (falls back to seek_by_key_subkey) + let result = cursor.seek(keys[2]).unwrap(); + assert!(result.is_some()); + assert_eq!(result.unwrap().0, keys[2]); + } + + #[test] + fn test_cursor_exhaustion_then_new_seek() { + let factory = create_test_provider_factory(); + let provider = factory.provider_rw().unwrap(); + let hashed_address = B256::random(); + let keys = create_test_keys(5); + + // Insert test data + { + let mut cursor = + provider.tx_ref().cursor_dup_write::().unwrap(); + for (i, key) in keys.iter().enumerate() { + cursor + .upsert( + hashed_address, + &StorageEntry { key: *key, value: U256::from(i as u64) }, + ) + .unwrap(); + } + } + + let mut cursor = DatabaseHashedStorageCursor::new( + provider.tx_ref().cursor_dup_read::().unwrap(), + hashed_address, + ); + + // Exhaust cursor by seeking past all keys + let high_key = { + let mut bytes = [0xffu8; 32]; + bytes[0] = 0xff; + B256::from(bytes) + }; + + // First seek to position the cursor + let _ = cursor.seek(keys[0]).unwrap(); + // Then seek past all entries + let result = cursor.seek(high_key).unwrap(); + assert!(result.is_none(), "Should not find key past all entries"); + + // Now seek back to an existing key - should work via fallback + let result = cursor.seek(keys[0]).unwrap(); + assert!(result.is_some(), "Should find first key after exhaustion"); + assert_eq!(result.unwrap().0, keys[0]); + } + + #[test] + fn test_address_switch_resets_position() { + let factory = create_test_provider_factory(); + let provider = factory.provider_rw().unwrap(); + let address1 = B256::random(); + let address2 = B256::random(); + let keys = create_test_keys(5); + + // Insert test data for both addresses + { + let mut cursor = + provider.tx_ref().cursor_dup_write::().unwrap(); + for (i, key) in keys.iter().enumerate() { + cursor + .upsert(address1, &StorageEntry { key: *key, value: U256::from(i as u64) }) + .unwrap(); + cursor + .upsert( + address2, + &StorageEntry { key: *key, value: U256::from((i + 100) as u64) }, + ) + .unwrap(); + } + } + + let mut cursor = DatabaseHashedStorageCursor::new( + provider.tx_ref().cursor_dup_read::().unwrap(), + address1, + ); + + // Seek in first address + let result = cursor.seek(keys[2]).unwrap(); + assert!(result.is_some()); + assert_eq!(result.unwrap().1, U256::from(2)); + + // Switch address and seek + cursor.set_hashed_address(address2); + let result = cursor.seek(keys[2]).unwrap(); + assert!(result.is_some()); + assert_eq!(result.unwrap().1, U256::from(102)); + } + + #[test] + fn test_seek_same_key_returns_current() { + let factory = create_test_provider_factory(); + let provider = factory.provider_rw().unwrap(); + let hashed_address = B256::random(); + let keys = create_test_keys(5); + + // Insert test data + { + let mut cursor = + provider.tx_ref().cursor_dup_write::().unwrap(); + for (i, key) in keys.iter().enumerate() { + cursor + .upsert( + hashed_address, + &StorageEntry { key: *key, value: U256::from(i as u64) }, + ) + .unwrap(); + } + } + + let mut cursor = DatabaseHashedStorageCursor::new( + provider.tx_ref().cursor_dup_read::().unwrap(), + hashed_address, + ); + + // Seek to a key + let result1 = cursor.seek(keys[2]).unwrap(); + assert!(result1.is_some()); + + // Seek to the same key again - should use cached current position + let result2 = cursor.seek(keys[2]).unwrap(); + assert!(result2.is_some()); + assert_eq!(result1.unwrap(), result2.unwrap()); + } + + #[test] + fn test_reset_clears_last_key() { + let factory = create_test_provider_factory(); + let provider = factory.provider_rw().unwrap(); + let hashed_address = B256::random(); + let keys = create_test_keys(5); + + // Insert test data + { + let mut cursor = + provider.tx_ref().cursor_dup_write::().unwrap(); + for (i, key) in keys.iter().enumerate() { + cursor + .upsert( + hashed_address, + &StorageEntry { key: *key, value: U256::from(i as u64) }, + ) + .unwrap(); + } + } + + let mut cursor = DatabaseHashedStorageCursor::new( + provider.tx_ref().cursor_dup_read::().unwrap(), + hashed_address, + ); + + // Seek to position cursor + let _ = cursor.seek(keys[2]).unwrap(); + + // Reset and verify we can still seek + cursor.reset(); + let result = cursor.seek(keys[0]).unwrap(); + assert!(result.is_some()); + assert_eq!(result.unwrap().0, keys[0]); } } diff --git a/crates/trie/db/src/trie_cursor.rs b/crates/trie/db/src/trie_cursor.rs index 7b9c402545..e59ec61f5d 100644 --- a/crates/trie/db/src/trie_cursor.rs +++ b/crates/trie/db/src/trie_cursor.rs @@ -98,18 +98,25 @@ where } /// A cursor over the storage tries stored in the database. +/// +/// This cursor implements locality optimization: when seeking forward from the current position, +/// it uses `next_dup` (O(1)) to walk forward instead of `seek_by_key_subkey` (O(log N)) when +/// the target key is close to the current position. #[derive(Debug)] pub struct DatabaseStorageTrieCursor { /// The underlying cursor. pub cursor: C, /// Hashed address used for cursor positioning. hashed_address: B256, + /// The last key returned by the cursor, used for locality optimization. + /// When seeking forward from this position, we can use `next_dup` instead of re-seeking. + last_key: Option, } impl DatabaseStorageTrieCursor { /// Create a new storage trie cursor. pub const fn new(cursor: C, hashed_address: B256) -> Self { - Self { cursor, hashed_address } + Self { cursor, hashed_address, last_key: None } } } @@ -125,6 +132,9 @@ where &mut self, updates: &StorageTrieUpdatesSorted, ) -> Result { + // Invalidate cursor position tracking: writes move the cursor arbitrarily + self.last_key = None; + // The storage trie for this account has to be deleted. if updates.is_deleted() && self.cursor.seek_exact(self.hashed_address)?.is_some() { self.cursor.delete_current_duplicates()?; @@ -167,27 +177,85 @@ where &mut self, key: Nibbles, ) -> Result, DatabaseError> { - Ok(self + let result = self .cursor .seek_by_key_subkey(self.hashed_address, StoredNibblesSubKey(key))? .filter(|e| e.nibbles == StoredNibblesSubKey(key)) - .map(|value| (value.nibbles.0, value.node))) + .map(|value| (value.nibbles.0, value.node)); + + self.last_key = result.as_ref().map(|(k, _)| *k); + Ok(result) } /// Seeks the given key in the storage trie. + /// + /// This method implements a bounded locality optimization: if the cursor is already positioned + /// and the target key is > the current position, we speculatively walk forward using + /// `next_dup` (O(1) per step) for up to `MAX_WALK_STEPS` entries. If the target is not + /// found within that limit, we fall back to `seek_by_key_subkey` (O(log N)). + /// + /// This bounds the worst-case complexity to O(MAX_WALK_STEPS + log N) instead of O(N). + #[allow(clippy::collapsible_if)] fn seek( &mut self, key: Nibbles, ) -> Result, DatabaseError> { - Ok(self + // Maximum forward walk steps before falling back to seek. + // This bounds worst-case from O(N) to O(MAX_WALK_STEPS + log N). + // Value of 8 is optimal for tries up to ~256 entries (log2(256) = 8). + const MAX_WALK_STEPS: usize = 8; + + // Locality optimization: if cursor is positioned and target is ahead, + // speculatively walk forward for a bounded number of steps + if let Some(last) = self.last_key { + if key > last { + // Bounded forward walk - only beneficial for nearby keys + for _ in 0..MAX_WALK_STEPS { + match self.cursor.next_dup()? { + Some((_, entry)) => { + if entry.nibbles.0 >= key { + // Found target or passed it + self.last_key = Some(entry.nibbles.0); + return Ok(Some((entry.nibbles.0, entry.node))); + } + // Haven't reached target yet, continue walking + } + None => { + // Exhausted all duplicates without finding target + self.last_key = None; + return Ok(None); + } + } + } + // Exceeded walk limit - fall through to seek_by_key_subkey + } else if key == last { + // Re-seeking the same key, return current position if still valid + if let Some((_, entry)) = self.cursor.current()? { + if entry.nibbles.0 == key { + return Ok(Some((entry.nibbles.0, entry.node))); + } + } + } + } + + // Fall back to seek_by_key_subkey for: + // - Backward seeks (key < last) + // - When cursor is not positioned (last_key is None) + // - When forward walk exceeded limit + let result = self .cursor .seek_by_key_subkey(self.hashed_address, StoredNibblesSubKey(key))? - .map(|value| (value.nibbles.0, value.node))) + .map(|value| (value.nibbles.0, value.node)); + + self.last_key = result.as_ref().map(|(k, _)| *k); + Ok(result) } /// Move the cursor to the next entry and return it. fn next(&mut self) -> Result, DatabaseError> { - Ok(self.cursor.next_dup()?.map(|(_, v)| (v.nibbles.0, v.node))) + let result = self.cursor.next_dup()?.map(|(_, v)| (v.nibbles.0, v.node)); + self.last_key = result.as_ref().map(|(k, _)| *k); + Ok(result) } /// Retrieves the current value in the storage trie cursor. @@ -196,7 +264,7 @@ where } fn reset(&mut self) { - // No-op for database cursors + self.last_key = None; } } @@ -206,6 +274,8 @@ where { fn set_hashed_address(&mut self, hashed_address: B256) { self.hashed_address = hashed_address; + // Reset cursor position tracking when switching to a different storage trie + self.last_key = None; } } @@ -215,6 +285,21 @@ mod tests { use alloy_primitives::hex_literal::hex; use reth_db_api::{cursor::DbCursorRW, transaction::DbTxMut}; use reth_provider::test_utils::create_test_provider_factory; + use reth_trie::trie_cursor::TrieStorageCursor; + + fn create_test_nibbles(count: usize) -> Vec { + (0..count as u64) + .map(|i| { + let mut bytes = [0u8; 8]; + bytes.copy_from_slice(&i.to_be_bytes()); + Nibbles::unpack(bytes) + }) + .collect() + } + + fn create_test_node() -> BranchNodeCompact { + BranchNodeCompact::new(0b1111, 0b0011, 0, vec![], None) + } #[test] fn test_account_trie_order() { @@ -256,7 +341,6 @@ mod tests { ); } - // tests that upsert and seek match on the storage trie cursor #[test] fn test_storage_cursor_abstraction() { let factory = create_test_provider_factory(); @@ -274,4 +358,290 @@ mod tests { let mut cursor = DatabaseStorageTrieCursor::new(cursor, hashed_address); assert_eq!(cursor.seek(key.into()).unwrap().unwrap().1, value); } + + #[test] + fn test_storage_trie_forward_sequential_seek() { + let factory = create_test_provider_factory(); + let provider = factory.provider_rw().unwrap(); + let hashed_address = B256::random(); + let keys = create_test_nibbles(10); + + // Insert test data + { + let mut cursor = provider.tx_ref().cursor_dup_write::().unwrap(); + for key in &keys { + cursor + .upsert( + hashed_address, + &StorageTrieEntry { + nibbles: StoredNibblesSubKey(*key), + node: create_test_node(), + }, + ) + .unwrap(); + } + } + + let mut cursor = DatabaseStorageTrieCursor::new( + provider.tx_ref().cursor_dup_read::().unwrap(), + hashed_address, + ); + + // Forward sequential seeks should all succeed + for (i, key) in keys.iter().enumerate() { + let result = cursor.seek(*key).unwrap(); + assert!(result.is_some(), "Should find key at index {i}"); + let (found_key, _) = result.unwrap(); + assert_eq!(found_key, *key); + } + } + + #[test] + fn test_storage_trie_backward_seek_fallback() { + let factory = create_test_provider_factory(); + let provider = factory.provider_rw().unwrap(); + let hashed_address = B256::random(); + let keys = create_test_nibbles(10); + + // Insert test data + { + let mut cursor = provider.tx_ref().cursor_dup_write::().unwrap(); + for key in &keys { + cursor + .upsert( + hashed_address, + &StorageTrieEntry { + nibbles: StoredNibblesSubKey(*key), + node: create_test_node(), + }, + ) + .unwrap(); + } + } + + let mut cursor = DatabaseStorageTrieCursor::new( + provider.tx_ref().cursor_dup_read::().unwrap(), + hashed_address, + ); + + // Seek to last key first + let result = cursor.seek(keys[9]).unwrap(); + assert!(result.is_some()); + + // Backward seek should still work + let result = cursor.seek(keys[2]).unwrap(); + assert!(result.is_some()); + assert_eq!(result.unwrap().0, keys[2]); + } + + #[test] + fn test_storage_trie_cursor_exhaustion() { + let factory = create_test_provider_factory(); + let provider = factory.provider_rw().unwrap(); + let hashed_address = B256::random(); + let keys = create_test_nibbles(5); + + // Insert test data + { + let mut cursor = provider.tx_ref().cursor_dup_write::().unwrap(); + for key in &keys { + cursor + .upsert( + hashed_address, + &StorageTrieEntry { + nibbles: StoredNibblesSubKey(*key), + node: create_test_node(), + }, + ) + .unwrap(); + } + } + + let mut cursor = DatabaseStorageTrieCursor::new( + provider.tx_ref().cursor_dup_read::().unwrap(), + hashed_address, + ); + + // Position cursor + let _ = cursor.seek(keys[0]).unwrap(); + + // Create a very high nibbles key to exhaust + let high_key = Nibbles::from_nibbles([0xf; 64]); + + // Seek past all entries + let result = cursor.seek(high_key).unwrap(); + assert!(result.is_none()); + + // Now seek back - should work via fallback + let result = cursor.seek(keys[0]).unwrap(); + assert!(result.is_some()); + assert_eq!(result.unwrap().0, keys[0]); + } + + #[test] + fn test_storage_trie_address_switch() { + let factory = create_test_provider_factory(); + let provider = factory.provider_rw().unwrap(); + let address1 = B256::random(); + let address2 = B256::random(); + let keys = create_test_nibbles(5); + + let node1 = BranchNodeCompact::new(0b0001, 0b0001, 0, vec![], None); + let node2 = BranchNodeCompact::new(0b0010, 0b0010, 0, vec![], None); + + // Insert test data for both addresses + { + let mut cursor = provider.tx_ref().cursor_dup_write::().unwrap(); + for key in &keys { + cursor + .upsert( + address1, + &StorageTrieEntry { + nibbles: StoredNibblesSubKey(*key), + node: node1.clone(), + }, + ) + .unwrap(); + cursor + .upsert( + address2, + &StorageTrieEntry { + nibbles: StoredNibblesSubKey(*key), + node: node2.clone(), + }, + ) + .unwrap(); + } + } + + let mut cursor = DatabaseStorageTrieCursor::new( + provider.tx_ref().cursor_dup_read::().unwrap(), + address1, + ); + + // Seek in first address + let result = cursor.seek(keys[2]).unwrap(); + assert!(result.is_some()); + assert_eq!(result.unwrap().1, node1); + + // Switch address and seek + cursor.set_hashed_address(address2); + let result = cursor.seek(keys[2]).unwrap(); + assert!(result.is_some()); + assert_eq!(result.unwrap().1, node2); + } + + #[test] + fn test_storage_trie_seek_same_key() { + let factory = create_test_provider_factory(); + let provider = factory.provider_rw().unwrap(); + let hashed_address = B256::random(); + let keys = create_test_nibbles(5); + + // Insert test data + { + let mut cursor = provider.tx_ref().cursor_dup_write::().unwrap(); + for key in &keys { + cursor + .upsert( + hashed_address, + &StorageTrieEntry { + nibbles: StoredNibblesSubKey(*key), + node: create_test_node(), + }, + ) + .unwrap(); + } + } + + let mut cursor = DatabaseStorageTrieCursor::new( + provider.tx_ref().cursor_dup_read::().unwrap(), + hashed_address, + ); + + // Seek to a key + let result1 = cursor.seek(keys[2]).unwrap(); + assert!(result1.is_some()); + + // Seek to the same key again + let result2 = cursor.seek(keys[2]).unwrap(); + assert!(result2.is_some()); + assert_eq!(result1.unwrap().0, result2.unwrap().0); + } + + #[test] + fn test_storage_trie_reset() { + let factory = create_test_provider_factory(); + let provider = factory.provider_rw().unwrap(); + let hashed_address = B256::random(); + let keys = create_test_nibbles(5); + + // Insert test data + { + let mut cursor = provider.tx_ref().cursor_dup_write::().unwrap(); + for key in &keys { + cursor + .upsert( + hashed_address, + &StorageTrieEntry { + nibbles: StoredNibblesSubKey(*key), + node: create_test_node(), + }, + ) + .unwrap(); + } + } + + let mut cursor = DatabaseStorageTrieCursor::new( + provider.tx_ref().cursor_dup_read::().unwrap(), + hashed_address, + ); + + // Position cursor + let _ = cursor.seek(keys[2]).unwrap(); + + // Reset and verify we can still seek + cursor.reset(); + let result = cursor.seek(keys[0]).unwrap(); + assert!(result.is_some()); + assert_eq!(result.unwrap().0, keys[0]); + } + + #[test] + fn test_storage_trie_seek_exact_updates_last_key() { + let factory = create_test_provider_factory(); + let provider = factory.provider_rw().unwrap(); + let hashed_address = B256::random(); + let keys = create_test_nibbles(5); + + // Insert test data + { + let mut cursor = provider.tx_ref().cursor_dup_write::().unwrap(); + for key in &keys { + cursor + .upsert( + hashed_address, + &StorageTrieEntry { + nibbles: StoredNibblesSubKey(*key), + node: create_test_node(), + }, + ) + .unwrap(); + } + } + + let mut cursor = DatabaseStorageTrieCursor::new( + provider.tx_ref().cursor_dup_read::().unwrap(), + hashed_address, + ); + + // seek_exact should update last_key + let result = cursor.seek_exact(keys[2]).unwrap(); + assert!(result.is_some()); + + // Forward seek should use optimization + let result = cursor.seek(keys[3]).unwrap(); + assert!(result.is_some()); + assert_eq!(result.unwrap().0, keys[3]); + } }