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        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    /// Returns whether nothing is scheduled.
46    pub fn is_empty(&self) -> bool {
47        self.accounts.is_empty()
48    }
49
50    /// Number of accounts scheduled.
51    pub fn len(&self) -> usize {
52        self.accounts.len()
53    }
54
55    /// First account scheduled, in key order.
56    pub fn first(&self) -> Option<B256> {
57        self.accounts.keys().next().copied()
58    }
59
60    /// Slots scheduled for the storage at `hashed_address`, in key order.
61    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    // Adds what `other` schedules.
70    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    // Drops the fields and slots a canonical list overwrites, as `changes` records them. Returns
77    // whether anything was dropped.
78    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    // Drops the account at `hashed_address`, fetched whole, with the `slots` fetched along with it,
92    // or every slot when it has no storage at the pivot. Slots scheduled after the fetch stay, and
93    // keep their account scheduled.
94    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// What the state holds stale values for in one account.
113#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
114struct StaleAccount {
115    // Whether the balance is stale.
116    balance: bool,
117    // Whether the nonce is stale.
118    nonce: bool,
119    // Whether the code hash is stale.
120    code: bool,
121    // Hashed slots of its storage.
122    slots: BTreeSet<B256>,
123}
124
125impl StaleAccount {
126    // Whether nothing about the account is stale any more.
127    fn is_resolved(&self) -> bool {
128        !self.balance && !self.nonce && !self.code && self.slots.is_empty()
129    }
130
131    // Marks the fields and slots `changes` writes as stale.
132    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    // Marks what `other` holds stale as stale.
141    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    // Clears the fields and slots `changes` overwrites, returning whether any was stale.
149    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    // Hashed keys of the slots `changes` writes.
162    fn slots_of(changes: &AccountChanges) -> impl Iterator<Item = B256> + '_ {
163        changes.storage_post_states().map(|(slot, _)| keccak256(B256::from(slot)))
164    }
165
166    // Marks every account-level field stale.
167    const fn mark_fields_stale(&mut self) {
168        self.balance = true;
169        self.nonce = true;
170        self.code = true;
171    }
172
173    // Marks every account-level field fetched.
174    const fn clear_fields(&mut self) {
175        self.balance = false;
176        self.nonce = false;
177        self.code = false;
178    }
179}
180
181// The schedule as persisted, tied to the attempt that recorded it.
182#[derive(Serialize, Deserialize)]
183pub(crate) struct StoredRepairs {
184    // Encoding version, checked before the rest is decoded.
185    version: u32,
186    // Attempt the repairs belong to.
187    pub(crate) attempt: SnapAttemptId,
188    // What that attempt still has to fetch again.
189    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    // Persists `repairs` for `attempt`, removing the record once nothing is left.
199    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}