1use crate::{common::SnapRecord, SnapSyncError};
9use alloy_eip7928::AccountChanges;
10use alloy_primitives::{keccak256, B256, U256};
11use reth_storage_api::{MetadataWriter, SnapAttemptId};
12use serde::{Deserialize, Serialize};
13use std::{
14 collections::{btree_map::Entry, BTreeMap, BTreeSet},
15 mem,
16};
17
18#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
20pub struct StateRepairs {
21 accounts: BTreeMap<B256, StaleAccount>,
23}
24
25impl StateRepairs {
26 pub fn insert_account(&mut self, hashed_address: B256) {
28 self.accounts.entry(hashed_address).or_default().mark_fields_stale();
29 }
30
31 pub fn insert_slot(&mut self, hashed_address: B256, hashed_slot: B256) {
33 self.accounts.entry(hashed_address).or_default().slots.insert(hashed_slot);
34 }
35
36 pub fn insert_changes(&mut self, hashed_address: B256, changes: &AccountChanges) {
38 let stale = self.accounts.entry(hashed_address).or_default();
39 stale.insert_changes(changes);
40 if stale.is_resolved() {
41 self.accounts.remove(&hashed_address);
42 }
43 }
44
45 pub fn is_empty(&self) -> bool {
47 self.accounts.is_empty()
48 }
49
50 pub fn len(&self) -> usize {
52 self.accounts.len()
53 }
54
55 pub fn first(&self) -> Option<B256> {
57 self.accounts.keys().next().copied()
58 }
59
60 pub fn slots(&self, hashed_address: B256) -> impl Iterator<Item = B256> + '_ {
62 self.accounts
63 .get(&hashed_address)
64 .into_iter()
65 .flat_map(|account| account.slots.iter())
66 .copied()
67 }
68
69 pub(crate) fn extend(&mut self, other: Self) {
71 for (hashed_address, stale) in other.accounts {
72 self.accounts.entry(hashed_address).or_default().extend(stale);
73 }
74 }
75
76 pub(crate) fn resolve_changes(
79 &mut self,
80 hashed_address: B256,
81 changes: &AccountChanges,
82 ) -> bool {
83 let Entry::Occupied(mut entry) = self.accounts.entry(hashed_address) else { return false };
84 let resolved = entry.get_mut().resolve_changes(changes);
85 if entry.get().is_resolved() {
86 entry.remove();
87 }
88 resolved
89 }
90
91 pub(crate) fn resolve(&mut self, hashed_address: B256, slots: Option<&[(B256, U256)]>) {
95 let Entry::Occupied(mut entry) = self.accounts.entry(hashed_address) else { return };
96 let account = entry.get_mut();
97 account.clear_fields();
98 match slots {
99 Some(slots) => {
100 for (slot, _) in slots {
101 account.slots.remove(slot);
102 }
103 }
104 None => account.slots.clear(),
105 }
106 if account.is_resolved() {
107 entry.remove();
108 }
109 }
110}
111
112#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
114struct StaleAccount {
115 balance: bool,
117 nonce: bool,
119 code: bool,
121 slots: BTreeSet<B256>,
123}
124
125impl StaleAccount {
126 fn is_resolved(&self) -> bool {
128 !self.balance && !self.nonce && !self.code && self.slots.is_empty()
129 }
130
131 fn insert_changes(&mut self, changes: &AccountChanges) {
133 let info = changes.account_info();
134 self.balance |= info.balance.is_some();
135 self.nonce |= info.nonce.is_some();
136 self.code |= info.code_hash.is_some();
137 self.slots.extend(Self::slots_of(changes));
138 }
139
140 fn extend(&mut self, other: Self) {
142 self.balance |= other.balance;
143 self.nonce |= other.nonce;
144 self.code |= other.code;
145 self.slots.extend(other.slots);
146 }
147
148 fn resolve_changes(&mut self, changes: &AccountChanges) -> bool {
150 let info = changes.account_info();
151 let mut resolved = false;
152 resolved |= info.balance.is_some() && mem::take(&mut self.balance);
153 resolved |= info.nonce.is_some() && mem::take(&mut self.nonce);
154 resolved |= info.code_hash.is_some() && mem::take(&mut self.code);
155 for slot in Self::slots_of(changes) {
156 resolved |= self.slots.remove(&slot);
157 }
158 resolved
159 }
160
161 fn slots_of(changes: &AccountChanges) -> impl Iterator<Item = B256> + '_ {
163 changes.storage_post_states().map(|(slot, _)| keccak256(B256::from(slot)))
164 }
165
166 const fn mark_fields_stale(&mut self) {
168 self.balance = true;
169 self.nonce = true;
170 self.code = true;
171 }
172
173 const fn clear_fields(&mut self) {
175 self.balance = false;
176 self.nonce = false;
177 self.code = false;
178 }
179}
180
181#[derive(Serialize, Deserialize)]
183pub(crate) struct StoredRepairs {
184 version: u32,
186 pub(crate) attempt: SnapAttemptId,
188 pub(crate) repairs: StateRepairs,
190}
191
192impl SnapRecord for StoredRepairs {
193 const KEY: &'static str = "snap_state_repairs";
194 const VERSION: u32 = 1;
195}
196
197impl StoredRepairs {
198 pub(crate) fn store(
200 provider: &impl MetadataWriter,
201 attempt: SnapAttemptId,
202 repairs: StateRepairs,
203 ) -> Result<(), SnapSyncError> {
204 if repairs.is_empty() {
205 return Self::clear(provider)
206 }
207 Self { version: Self::VERSION, attempt, repairs }.write(provider)
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214 use alloy_eip7928::{BalanceChange, BlockAccessIndex, NonceChange, SlotChanges, StorageChange};
215 use alloy_primitives::Address;
216
217 const ACCOUNT: Address = Address::repeat_byte(0xaa);
218
219 fn hashed() -> B256 {
220 keccak256(ACCOUNT)
221 }
222
223 fn balance(value: u64) -> AccountChanges {
224 AccountChanges::new(ACCOUNT)
225 .with_balance_change(BalanceChange::new(BlockAccessIndex::new(1), U256::from(value)))
226 }
227
228 fn nonce(value: u64) -> AccountChanges {
229 AccountChanges::new(ACCOUNT)
230 .with_nonce_change(NonceChange::new(BlockAccessIndex::new(1), value))
231 }
232
233 fn slots(slots: &[u64]) -> AccountChanges {
234 slots.iter().fold(AccountChanges::new(ACCOUNT), |changes, slot| {
235 changes.with_storage_change(SlotChanges::new(
236 U256::from(*slot),
237 vec![StorageChange::new(BlockAccessIndex::new(1), U256::ONE)],
238 ))
239 })
240 }
241
242 fn scheduled(changes: &AccountChanges) -> StateRepairs {
243 let mut repairs = StateRepairs::default();
244 repairs.insert_changes(hashed(), changes);
245 repairs
246 }
247
248 #[test]
249 fn a_field_both_branches_change_needs_no_repair() {
250 let mut repairs = scheduled(&balance(1));
251
252 repairs.resolve_changes(hashed(), &balance(2));
253
254 assert!(repairs.is_empty());
255 }
256
257 #[test]
258 fn a_field_only_the_old_branch_changes_stays_scheduled() {
259 let mut repairs = scheduled(&balance(1));
260
261 repairs.resolve_changes(hashed(), &nonce(2));
262
263 assert_eq!(repairs, scheduled(&balance(1)));
264 }
265
266 #[test]
267 fn slots_are_resolved_one_by_one() {
268 let mut repairs = scheduled(&slots(&[1, 2]));
269
270 repairs.resolve_changes(hashed(), &slots(&[1]));
271
272 let remaining: Vec<_> = repairs.slots(hashed()).collect();
273 assert_eq!(remaining, [keccak256(B256::with_last_byte(2))]);
274 }
275
276 #[test]
277 fn a_fetched_account_resolves_every_field() {
278 let mut repairs =
279 scheduled(&balance(1).with_nonce_change(NonceChange::new(BlockAccessIndex::new(1), 1)));
280
281 repairs.resolve(hashed(), None);
282
283 assert!(repairs.is_empty());
284 }
285}