reth_trie_common/
added_removed_keys.rs

1//! Tracking of keys having been added and removed from the tries.
2
3use crate::HashedPostState;
4use alloy_primitives::{map::B256Map, B256};
5use alloy_trie::proof::AddedRemovedKeys;
6
7/// Tracks added and removed keys across account and storage tries.
8#[derive(Debug, Clone)]
9pub struct MultiAddedRemovedKeys {
10    account: AddedRemovedKeys,
11    storages: B256Map<AddedRemovedKeys>,
12}
13
14/// Returns [`AddedRemovedKeys`] with default parameters. This is necessary while we are not yet
15/// tracking added keys.
16fn default_added_removed_keys() -> AddedRemovedKeys {
17    AddedRemovedKeys::default().with_assume_added(true)
18}
19
20impl Default for MultiAddedRemovedKeys {
21    fn default() -> Self {
22        Self::new()
23    }
24}
25
26impl MultiAddedRemovedKeys {
27    /// Returns a new instance.
28    pub fn new() -> Self {
29        Self { account: default_added_removed_keys(), storages: Default::default() }
30    }
31
32    /// Updates the set of removed keys based on a [`HashedPostState`].
33    ///
34    /// Storage keys set to [`alloy_primitives::U256::ZERO`] are added to the set for their
35    /// respective account. Keys set to any other value are removed from their respective
36    /// account.
37    pub fn update_with_state(&mut self, update: &HashedPostState) {
38        for (hashed_address, storage) in &update.storages {
39            let account = update
40                .accounts
41                .get(hashed_address)
42                .map(|entry| entry.unwrap_or_default())
43                .unwrap_or_default();
44
45            if storage.wiped {
46                self.storages.remove(hashed_address);
47                if account.is_empty() {
48                    self.account.insert_removed(*hashed_address);
49                }
50                continue
51            }
52
53            let storage_removed_keys =
54                self.storages.entry(*hashed_address).or_insert_with(default_added_removed_keys);
55
56            for (key, val) in &storage.storage {
57                if val.is_zero() {
58                    storage_removed_keys.insert_removed(*key);
59                } else {
60                    storage_removed_keys.remove_removed(key);
61                }
62            }
63
64            if !account.is_empty() {
65                self.account.remove_removed(hashed_address);
66            }
67        }
68    }
69
70    /// Returns a [`AddedRemovedKeys`] for the storage trie of a particular account, if any.
71    pub fn get_storage(&self, hashed_address: &B256) -> Option<&AddedRemovedKeys> {
72        self.storages.get(hashed_address)
73    }
74
75    /// Returns an [`AddedRemovedKeys`] for tracking account-level changes.
76    pub const fn get_accounts(&self) -> &AddedRemovedKeys {
77        &self.account
78    }
79
80    /// Marks an account as existing, and therefore having storage.
81    pub fn touch_accounts(&mut self, addresses: impl Iterator<Item = B256>) {
82        for address in addresses {
83            self.storages.entry(address).or_insert_with(default_added_removed_keys);
84        }
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use crate::HashedStorage;
92    use alloy_primitives::U256;
93    use reth_primitives_traits::Account;
94
95    #[test]
96    fn test_update_with_state_storage_keys_non_zero() {
97        let mut multi_keys = MultiAddedRemovedKeys::new();
98        let mut update = HashedPostState::default();
99
100        let addr = B256::random();
101        let slot1 = B256::random();
102        let slot2 = B256::random();
103
104        // First mark slots as removed
105        let mut storage = HashedStorage::default();
106        storage.storage.insert(slot1, U256::ZERO);
107        storage.storage.insert(slot2, U256::ZERO);
108        update.storages.insert(addr, storage);
109        multi_keys.update_with_state(&update);
110
111        // Verify they are removed
112        assert!(multi_keys.get_storage(&addr).unwrap().is_removed(&slot1));
113        assert!(multi_keys.get_storage(&addr).unwrap().is_removed(&slot2));
114
115        // Now update with non-zero values
116        let mut update2 = HashedPostState::default();
117        let mut storage2 = HashedStorage::default();
118        storage2.storage.insert(slot1, U256::from(100));
119        storage2.storage.insert(slot2, U256::from(200));
120        update2.storages.insert(addr, storage2);
121        multi_keys.update_with_state(&update2);
122
123        // Slots should no longer be marked as removed
124        let storage_keys = multi_keys.get_storage(&addr).unwrap();
125        assert!(!storage_keys.is_removed(&slot1));
126        assert!(!storage_keys.is_removed(&slot2));
127    }
128
129    #[test]
130    fn test_update_with_state_wiped_storage() {
131        let mut multi_keys = MultiAddedRemovedKeys::new();
132        let mut update = HashedPostState::default();
133
134        let addr = B256::random();
135        let slot1 = B256::random();
136
137        // First add some removed keys
138        let mut storage = HashedStorage::default();
139        storage.storage.insert(slot1, U256::ZERO);
140        update.storages.insert(addr, storage);
141        multi_keys.update_with_state(&update);
142        assert!(multi_keys.get_storage(&addr).is_some());
143
144        // Now wipe the storage
145        let mut update2 = HashedPostState::default();
146        let wiped_storage = HashedStorage::new(true);
147        update2.storages.insert(addr, wiped_storage);
148        multi_keys.update_with_state(&update2);
149
150        // Storage and account should be removed
151        assert!(multi_keys.get_storage(&addr).is_none());
152        assert!(multi_keys.get_accounts().is_removed(&addr));
153    }
154
155    #[test]
156    fn test_update_with_state_account_tracking() {
157        let mut multi_keys = MultiAddedRemovedKeys::new();
158        let mut update = HashedPostState::default();
159
160        let addr = B256::random();
161        let slot = B256::random();
162
163        // Add storage with zero value and empty account
164        let mut storage = HashedStorage::default();
165        storage.storage.insert(slot, U256::ZERO);
166        update.storages.insert(addr, storage);
167        // Account is implicitly empty (not in accounts map)
168
169        multi_keys.update_with_state(&update);
170
171        // Storage should have removed keys but account should not be removed
172        assert!(multi_keys.get_storage(&addr).unwrap().is_removed(&slot));
173        assert!(!multi_keys.get_accounts().is_removed(&addr));
174
175        // Now clear all removed storage keys and keep account empty
176        let mut update2 = HashedPostState::default();
177        let mut storage2 = HashedStorage::default();
178        storage2.storage.insert(slot, U256::from(100)); // Non-zero removes from removed set
179        update2.storages.insert(addr, storage2);
180
181        multi_keys.update_with_state(&update2);
182
183        // Account should not be marked as removed still
184        assert!(!multi_keys.get_accounts().is_removed(&addr));
185    }
186
187    #[test]
188    fn test_update_with_state_account_with_balance() {
189        let mut multi_keys = MultiAddedRemovedKeys::new();
190        let mut update = HashedPostState::default();
191
192        let addr = B256::random();
193
194        // Add account with non-empty state (has balance)
195        let account = Account { balance: U256::from(1000), nonce: 0, bytecode_hash: None };
196        update.accounts.insert(addr, Some(account));
197
198        // Add empty storage
199        let storage = HashedStorage::default();
200        update.storages.insert(addr, storage);
201
202        multi_keys.update_with_state(&update);
203
204        // Account should not be marked as removed because it has balance
205        assert!(!multi_keys.get_accounts().is_removed(&addr));
206
207        // Now wipe the storage
208        let mut update2 = HashedPostState::default();
209        let wiped_storage = HashedStorage::new(true);
210        update2.storages.insert(addr, wiped_storage);
211        update2.accounts.insert(addr, Some(account));
212        multi_keys.update_with_state(&update2);
213
214        // Storage should be None, but account should not be removed.
215        assert!(multi_keys.get_storage(&addr).is_none());
216        assert!(!multi_keys.get_accounts().is_removed(&addr));
217    }
218}