mirror of
https://github.com/paradigmxyz/reth.git
synced 2026-04-30 03:01:58 -04:00
Compare commits
4 Commits
push
...
yk/cursor-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcb90854c2 | ||
|
|
6a100d6175 | ||
|
|
80082c330d | ||
|
|
4a2b60aeca |
@@ -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<C> {
|
||||
/// 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<B256>,
|
||||
}
|
||||
|
||||
impl<C> DatabaseHashedStorageCursor<C> {
|
||||
/// 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,52 @@ where
|
||||
{
|
||||
type Value = U256;
|
||||
|
||||
/// Seeks the given key in the hashed storage.
|
||||
///
|
||||
/// This method implements a locality optimization: if the cursor is already positioned
|
||||
/// and the target key is >= the current position, we use `next_dup_val` to walk forward
|
||||
/// instead of performing an expensive `seek_by_key_subkey` operation.
|
||||
#[allow(clippy::collapsible_if)]
|
||||
fn seek(&mut self, subkey: B256) -> Result<Option<(B256, Self::Value)>, DatabaseError> {
|
||||
Ok(self.cursor.seek_by_key_subkey(self.hashed_address, subkey)?.map(|e| (e.key, e.value)))
|
||||
// Locality optimization: if cursor is positioned and target is ahead,
|
||||
// walk forward using next_dup_val instead of seeking
|
||||
if let Some(last) = self.last_key {
|
||||
if subkey > last {
|
||||
// Walk forward using next_dup_val until we find a key >= target
|
||||
while let Some(entry) = self.cursor.next_dup_val()? {
|
||||
if entry.key >= subkey {
|
||||
self.last_key = Some(entry.key);
|
||||
return Ok(Some((entry.key, entry.value)));
|
||||
}
|
||||
}
|
||||
// Exhausted the duplicates, no match found
|
||||
self.last_key = None;
|
||||
return Ok(None);
|
||||
} 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 or when cursor is not positioned
|
||||
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<Option<(B256, Self::Value)>, 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 +165,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<B256> {
|
||||
(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::<tables::HashedStorages>().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::<tables::HashedStorages>().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::<tables::HashedStorages>().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::<tables::HashedStorages>().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::<tables::HashedStorages>().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::<tables::HashedStorages>().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::<tables::HashedStorages>().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::<tables::HashedStorages>().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::<tables::HashedStorages>().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::<tables::HashedStorages>().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::<tables::HashedStorages>().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::<tables::HashedStorages>().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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<C> {
|
||||
/// 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<Nibbles>,
|
||||
}
|
||||
|
||||
impl<C> DatabaseStorageTrieCursor<C> {
|
||||
/// 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<usize, DatabaseError> {
|
||||
// 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,65 @@ where
|
||||
&mut self,
|
||||
key: Nibbles,
|
||||
) -> Result<Option<(Nibbles, BranchNodeCompact)>, 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 locality optimization: if the cursor is already positioned
|
||||
/// and the target key is >= the current position, we use `next_dup` to walk forward
|
||||
/// instead of performing an expensive `seek_by_key_subkey` operation.
|
||||
#[allow(clippy::collapsible_if)]
|
||||
fn seek(
|
||||
&mut self,
|
||||
key: Nibbles,
|
||||
) -> Result<Option<(Nibbles, BranchNodeCompact)>, DatabaseError> {
|
||||
Ok(self
|
||||
// Locality optimization: if cursor is positioned and target is ahead,
|
||||
// walk forward using next_dup instead of seeking
|
||||
if let Some(last) = self.last_key {
|
||||
if key > last {
|
||||
// Walk forward using next_dup until we find a key >= target
|
||||
while let Some((_, entry)) = self.cursor.next_dup()? {
|
||||
if entry.nibbles.0 >= key {
|
||||
self.last_key = Some(entry.nibbles.0);
|
||||
return Ok(Some((entry.nibbles.0, entry.node)));
|
||||
}
|
||||
}
|
||||
// Exhausted the duplicates, no match found
|
||||
self.last_key = None;
|
||||
return Ok(None);
|
||||
} 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 or when cursor is not positioned
|
||||
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<Option<(Nibbles, BranchNodeCompact)>, 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 +244,7 @@ where
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
// No-op for database cursors
|
||||
self.last_key = None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,6 +254,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 +265,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<Nibbles> {
|
||||
(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 +321,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 +338,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::<tables::StoragesTrie>().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::<tables::StoragesTrie>().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::<tables::StoragesTrie>().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::<tables::StoragesTrie>().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::<tables::StoragesTrie>().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::<tables::StoragesTrie>().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::<tables::StoragesTrie>().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::<tables::StoragesTrie>().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::<tables::StoragesTrie>().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::<tables::StoragesTrie>().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::<tables::StoragesTrie>().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::<tables::StoragesTrie>().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::<tables::StoragesTrie>().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::<tables::StoragesTrie>().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]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user