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