Skip to main content

reth_snap_sync/
repair.rs

1//! Entries the downloaded state holds values for that no list will overwrite.
2//!
3//! Lists only overwrite the fields their blocks change, so a value the state holds for another
4//! reason, such as a block a reorg orphaned, survives catch-up. Such entries are scheduled here and
5//! fetched again on their own once catch-up reaches the pivot, where the pivot's values leave the
6//! whole state at one block.
7
8use 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/// Accounts and storage slots to fetch again at the pivot, in key order.
19#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
20pub struct StateRepairs {
21    // Hashed addresses, each with the fields and hashed slots its state holds stale values for.
22    accounts: BTreeMap<B256, StaleAccount>,
23}
24
25impl StateRepairs {
26    /// Schedules every field of the account at `hashed_address`.
27    pub fn insert_account(&mut self, hashed_address: B256) {
28        self.accounts.entry(hashed_address).or_default().mark_fields_stale();
29    }
30
31    /// Schedules `hashed_slot` of the storage at `hashed_address`, along with its account.
32    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    /// Schedules the fields and slots `changes` writes for the account at `hashed_address`.
37    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    /// Returns whether nothing is scheduled.
42    pub fn is_empty(&self) -> bool {
43        self.accounts.is_empty()
44    }
45
46    /// Number of accounts scheduled.
47    pub fn len(&self) -> usize {
48        self.accounts.len()
49    }
50
51    /// First account scheduled, in key order.
52    pub fn first(&self) -> Option<B256> {
53        self.accounts.keys().next().copied()
54    }
55
56    /// Slots scheduled for the storage at `hashed_address`, in key order.
57    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    // Adds what `other` schedules.
66    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    // Drops the fields and slots a canonical list overwrites, as `changes` records them. Returns
73    // whether anything was dropped.
74    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    // Drops the account at `hashed_address`, fetched whole, with the `slots` fetched along with it,
88    // or every slot when it has no storage at the pivot. Slots scheduled after the fetch stay, and
89    // keep their account scheduled.
90    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// What the state holds stale values for in one account.
109#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
110struct StaleAccount {
111    // Whether the balance is stale.
112    balance: bool,
113    // Whether the nonce is stale.
114    nonce: bool,
115    // Whether the code hash is stale.
116    code: bool,
117    // Hashed slots of its storage.
118    slots: BTreeSet<B256>,
119}
120
121impl StaleAccount {
122    // Whether nothing about the account is stale any more.
123    fn is_resolved(&self) -> bool {
124        !self.balance && !self.nonce && !self.code && self.slots.is_empty()
125    }
126
127    // Marks the fields and slots `changes` writes as stale.
128    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    // Marks what `other` holds stale as stale.
137    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    // Clears the fields and slots `changes` overwrites, returning whether any was stale.
145    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    // Hashed keys of the slots `changes` writes.
158    fn slots_of(changes: &AccountChanges) -> impl Iterator<Item = B256> + '_ {
159        changes.storage_post_states().map(|(slot, _)| keccak256(B256::from(slot)))
160    }
161
162    // Marks every account-level field stale.
163    const fn mark_fields_stale(&mut self) {
164        self.balance = true;
165        self.nonce = true;
166        self.code = true;
167    }
168
169    // Marks every account-level field fetched.
170    const fn clear_fields(&mut self) {
171        self.balance = false;
172        self.nonce = false;
173        self.code = false;
174    }
175}
176
177// The schedule as persisted, tied to the attempt that recorded it.
178#[derive(Serialize, Deserialize)]
179pub(crate) struct StoredRepairs {
180    // Encoding version, checked before the rest is decoded.
181    version: u32,
182    // Attempt the repairs belong to.
183    pub(crate) attempt: SnapAttemptId,
184    // What that attempt still has to fetch again.
185    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    // Persists `repairs` for `attempt`, removing the record once nothing is left.
195    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}