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 self.accounts.entry(hashed_address).or_default().insert_changes(changes);
39 }
40
41 pub fn is_empty(&self) -> bool {
43 self.accounts.is_empty()
44 }
45
46 pub fn len(&self) -> usize {
48 self.accounts.len()
49 }
50
51 pub fn first(&self) -> Option<B256> {
53 self.accounts.keys().next().copied()
54 }
55
56 pub fn slots(&self, hashed_address: B256) -> impl Iterator<Item = B256> + '_ {
58 self.accounts
59 .get(&hashed_address)
60 .into_iter()
61 .flat_map(|account| account.slots.iter())
62 .copied()
63 }
64
65 pub(crate) fn extend(&mut self, other: Self) {
67 for (hashed_address, stale) in other.accounts {
68 self.accounts.entry(hashed_address).or_default().extend(stale);
69 }
70 }
71
72 pub(crate) fn resolve_changes(
75 &mut self,
76 hashed_address: B256,
77 changes: &AccountChanges,
78 ) -> bool {
79 let Entry::Occupied(mut entry) = self.accounts.entry(hashed_address) else { return false };
80 let resolved = entry.get_mut().resolve_changes(changes);
81 if entry.get().is_resolved() {
82 entry.remove();
83 }
84 resolved
85 }
86
87 pub(crate) fn resolve(&mut self, hashed_address: B256, slots: Option<&[(B256, U256)]>) {
91 let Entry::Occupied(mut entry) = self.accounts.entry(hashed_address) else { return };
92 let account = entry.get_mut();
93 account.clear_fields();
94 match slots {
95 Some(slots) => {
96 for (slot, _) in slots {
97 account.slots.remove(slot);
98 }
99 }
100 None => account.slots.clear(),
101 }
102 if account.is_resolved() {
103 entry.remove();
104 }
105 }
106}
107
108#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
110struct StaleAccount {
111 balance: bool,
113 nonce: bool,
115 code: bool,
117 slots: BTreeSet<B256>,
119}
120
121impl StaleAccount {
122 fn is_resolved(&self) -> bool {
124 !self.balance && !self.nonce && !self.code && self.slots.is_empty()
125 }
126
127 fn insert_changes(&mut self, changes: &AccountChanges) {
129 let info = changes.account_info();
130 self.balance |= info.balance.is_some();
131 self.nonce |= info.nonce.is_some();
132 self.code |= info.code_hash.is_some();
133 self.slots.extend(Self::slots_of(changes));
134 }
135
136 fn extend(&mut self, other: Self) {
138 self.balance |= other.balance;
139 self.nonce |= other.nonce;
140 self.code |= other.code;
141 self.slots.extend(other.slots);
142 }
143
144 fn resolve_changes(&mut self, changes: &AccountChanges) -> bool {
146 let info = changes.account_info();
147 let mut resolved = false;
148 resolved |= info.balance.is_some() && mem::take(&mut self.balance);
149 resolved |= info.nonce.is_some() && mem::take(&mut self.nonce);
150 resolved |= info.code_hash.is_some() && mem::take(&mut self.code);
151 for slot in Self::slots_of(changes) {
152 resolved |= self.slots.remove(&slot);
153 }
154 resolved
155 }
156
157 fn slots_of(changes: &AccountChanges) -> impl Iterator<Item = B256> + '_ {
159 changes.storage_post_states().map(|(slot, _)| keccak256(B256::from(slot)))
160 }
161
162 const fn mark_fields_stale(&mut self) {
164 self.balance = true;
165 self.nonce = true;
166 self.code = true;
167 }
168
169 const fn clear_fields(&mut self) {
171 self.balance = false;
172 self.nonce = false;
173 self.code = false;
174 }
175}
176
177#[derive(Serialize, Deserialize)]
179pub(crate) struct StoredRepairs {
180 version: u32,
182 pub(crate) attempt: SnapAttemptId,
184 pub(crate) repairs: StateRepairs,
186}
187
188impl SnapRecord for StoredRepairs {
189 const KEY: &'static str = "snap_state_repairs";
190 const VERSION: u32 = 1;
191}
192
193impl StoredRepairs {
194 pub(crate) fn store(
196 provider: &impl MetadataWriter,
197 attempt: SnapAttemptId,
198 repairs: StateRepairs,
199 ) -> Result<(), SnapSyncError> {
200 if repairs.is_empty() {
201 return Self::clear(provider)
202 }
203 Self { version: Self::VERSION, attempt, repairs }.write(provider)
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210 use alloy_eip7928::{BalanceChange, BlockAccessIndex, NonceChange, SlotChanges, StorageChange};
211 use alloy_primitives::Address;
212
213 const ACCOUNT: Address = Address::repeat_byte(0xaa);
214
215 fn hashed() -> B256 {
216 keccak256(ACCOUNT)
217 }
218
219 fn balance(value: u64) -> AccountChanges {
220 AccountChanges::new(ACCOUNT)
221 .with_balance_change(BalanceChange::new(BlockAccessIndex::new(1), U256::from(value)))
222 }
223
224 fn nonce(value: u64) -> AccountChanges {
225 AccountChanges::new(ACCOUNT)
226 .with_nonce_change(NonceChange::new(BlockAccessIndex::new(1), value))
227 }
228
229 fn slots(slots: &[u64]) -> AccountChanges {
230 slots.iter().fold(AccountChanges::new(ACCOUNT), |changes, slot| {
231 changes.with_storage_change(SlotChanges::new(
232 U256::from(*slot),
233 vec![StorageChange::new(BlockAccessIndex::new(1), U256::from(1))],
234 ))
235 })
236 }
237
238 fn scheduled(changes: &AccountChanges) -> StateRepairs {
239 let mut repairs = StateRepairs::default();
240 repairs.insert_changes(hashed(), changes);
241 repairs
242 }
243
244 #[test]
245 fn a_field_both_branches_change_needs_no_repair() {
246 let mut repairs = scheduled(&balance(1));
247
248 repairs.resolve_changes(hashed(), &balance(2));
249
250 assert!(repairs.is_empty());
251 }
252
253 #[test]
254 fn a_field_only_the_old_branch_changes_stays_scheduled() {
255 let mut repairs = scheduled(&balance(1));
256
257 repairs.resolve_changes(hashed(), &nonce(2));
258
259 assert_eq!(repairs, scheduled(&balance(1)));
260 }
261
262 #[test]
263 fn slots_are_resolved_one_by_one() {
264 let mut repairs = scheduled(&slots(&[1, 2]));
265
266 repairs.resolve_changes(hashed(), &slots(&[1]));
267
268 let remaining: Vec<_> = repairs.slots(hashed()).collect();
269 assert_eq!(remaining, [keccak256(B256::from(U256::from(2)))]);
270 }
271
272 #[test]
273 fn a_fetched_account_resolves_every_field() {
274 let mut repairs =
275 scheduled(&balance(1).with_nonce_change(NonceChange::new(BlockAccessIndex::new(1), 1)));
276
277 repairs.resolve(hashed(), None);
278
279 assert!(repairs.is_empty());
280 }
281}