1use crate::{common::SnapRecord, SnapSyncError};
9use alloy_eips::{eip1898::BlockWithParent, BlockNumHash};
10use alloy_primitives::{Sealable, B256};
11use reth_primitives_traits::{AlloyBlockHeader, SealedHeader};
12use reth_storage_api::{
13 BlockHashReader, HeaderProvider, MetadataProvider, MetadataWriter, SnapAttempt, SnapAttemptId,
14};
15use serde::{Deserialize, Serialize};
16
17const KEPT_BLOCKS: u64 = 64;
20
21#[derive(Clone, Debug)]
24pub struct SnapReorg {
25 ancestor: BlockNumHash,
27 orphaned: Vec<BlockWithAccessListHash>,
29}
30
31impl SnapReorg {
32 pub const fn ancestor(&self) -> BlockNumHash {
34 self.ancestor
35 }
36
37 pub fn orphaned(&self) -> &[BlockWithAccessListHash] {
39 &self.orphaned
40 }
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
45pub struct BlockWithAccessListHash {
46 block: BlockWithParent,
48 block_access_list_hash: Option<B256>,
50}
51
52impl BlockWithAccessListHash {
53 pub const fn block(&self) -> BlockWithParent {
55 self.block
56 }
57
58 pub const fn block_access_list_hash(&self) -> Option<B256> {
60 self.block_access_list_hash
61 }
62}
63
64impl<H: AlloyBlockHeader + Sealable> From<&SealedHeader<H>> for BlockWithAccessListHash {
65 fn from(header: &SealedHeader<H>) -> Self {
66 Self {
67 block: header.block_with_parent(),
68 block_access_list_hash: header.block_access_list_hash(),
69 }
70 }
71}
72
73#[derive(Serialize, Deserialize)]
75pub(crate) struct StoredAncestry {
76 version: u32,
78 attempt: SnapAttemptId,
80 blocks: Vec<BlockWithAccessListHash>,
82}
83
84impl SnapRecord for StoredAncestry {
85 const KEY: &'static str = "snap_ancestry";
86 const VERSION: u32 = 2;
87}
88
89impl StoredAncestry {
90 pub(crate) fn record<P: HeaderProvider + MetadataWriter>(
93 provider: &P,
94 attempt: SnapAttemptId,
95 pivot: BlockNumHash,
96 ) -> Result<(), SnapSyncError> {
97 let from = pivot.number.saturating_sub(KEPT_BLOCKS) + 1;
98 let headers = provider.sealed_headers_range(from..=pivot.number)?;
99 let contiguous = headers.windows(2).all(|pair| pair[1].parent_hash() == pair[0].hash());
100 if !contiguous || headers.last().map(SealedHeader::num_hash) != Some(pivot) {
101 return Self::clear(provider)
102 }
103 let blocks = headers.iter().map(BlockWithAccessListHash::from).collect();
104 Self { version: Self::VERSION, attempt, blocks }.write(provider)
105 }
106
107 pub(crate) fn reorg<P: MetadataProvider + BlockHashReader>(
110 provider: &P,
111 attempt: &SnapAttempt,
112 ) -> Result<Option<SnapReorg>, SnapSyncError> {
113 let Some(mut blocks) = Self::read(provider)?
114 .filter(|stored| stored.attempt == attempt.id())
115 .map(|stored| stored.blocks)
116 else {
117 return Ok(None)
118 };
119 if blocks.last().map(|kept| kept.block.block) != Some(attempt.pivot()) {
121 return Ok(None)
122 }
123
124 let mut split = None;
126 for (index, kept) in blocks.iter().enumerate().rev() {
127 let block = kept.block.block;
128 if provider.block_hash(block.number)? == Some(block.hash) {
129 split = Some((block, index + 1));
130 break
131 }
132 }
133 let (ancestor, from) = match split {
134 Some(split) => split,
135 None => {
136 let lowest = blocks[0].block;
138 let Some(parent) = lowest.block.number.checked_sub(1) else { return Ok(None) };
139 if provider.block_hash(parent)? != Some(lowest.parent) {
140 return Ok(None)
141 }
142 (BlockNumHash::new(parent, lowest.parent), 0)
143 }
144 };
145 Ok(Some(SnapReorg { ancestor, orphaned: blocks.split_off(from) }))
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use crate::{
153 test_utils::{account, hashed_factory, state_root, verified_range, BalChain},
154 SnapAccountStore, SnapAttemptStore, SnapCatchUpStore, SnapGeneration, SnapWrite,
155 StateRepairs,
156 };
157 use alloy_eip7928::{
158 AccountChanges, BalanceChange, BlockAccessIndex, NonceChange, SlotChanges, StorageChange,
159 };
160 use alloy_eips::eip7928::bal::{Bal, DecodedBal};
161 use alloy_primitives::{keccak256, Address, B256, U256};
162 use reth_provider::{
163 test_utils::{insert_headers, MockNodeTypesWithDB},
164 DatabaseProviderFactory, ProviderFactory,
165 };
166 use reth_storage_api::DBProvider;
167 use reth_trie_common::TrieAccount;
168
169 type Factory = ProviderFactory<MockNodeTypesWithDB>;
170
171 const ANCESTOR: u64 = 2;
173
174 fn credit(address: Address, balance: u64) -> Vec<AccountChanges> {
175 vec![AccountChanges::new(address)
176 .with_balance_change(BalanceChange::new(BlockAccessIndex::new(1), U256::from(balance)))]
177 }
178
179 fn branches() -> (BalChain, BalChain) {
181 let address = Address::repeat_byte(0x11);
182 (
183 BalChain::new(ANCESTOR, [credit(address, 1), credit(address, 2)])
184 .with_state_root(root()),
185 BalChain::new(
186 ANCESTOR,
187 [credit(address, 10), credit(address, 20), credit(address, 30)],
188 )
189 .with_state_root(root()),
190 )
191 }
192
193 fn root() -> B256 {
195 state_root(&accounts().1)
196 }
197
198 fn accounts() -> (Vec<Address>, Vec<(B256, TrieAccount)>) {
200 let mut addresses: Vec<_> = (1..=3).map(Address::repeat_byte).collect();
201 addresses.sort_by_key(|address| keccak256(address));
202 let accounts = addresses
203 .iter()
204 .enumerate()
205 .map(|(nonce, address)| (keccak256(address), account(nonce as u64)))
206 .collect();
207 (addresses, accounts)
208 }
209
210 fn started(chain: &BalChain, number: u64) -> (Factory, SnapWrite) {
212 let (_, accounts) = accounts();
213 let factory = hashed_factory();
214 insert_headers(&factory, &chain.headers);
215 let provider = factory.database_provider_rw().unwrap();
216 let pivot = chain.headers[number as usize].num_hash();
217 let write =
218 provider.start_snap_attempt(SnapGeneration::new(pivot, state_root(&accounts))).unwrap();
219 provider.start_account_coverage(write).unwrap();
220 let range = verified_range(&accounts, 0..2, B256::ZERO, &[B256::ZERO, accounts[1].0]);
221 provider.commit_account_range(write, &range, Default::default(), Vec::new()).unwrap();
222 provider.commit().unwrap();
223 (factory, write)
224 }
225
226 fn reorg(factory: &Factory, write: SnapWrite) -> Option<SnapReorg> {
227 factory.database_provider_ro().unwrap().snap_reorg(write).unwrap()
228 }
229
230 fn kept(headers: &[SealedHeader]) -> Vec<BlockWithAccessListHash> {
231 headers.iter().map(BlockWithAccessListHash::from).collect()
232 }
233
234 fn list(changes: Vec<AccountChanges>) -> DecodedBal {
236 DecodedBal::from_rlp_bytes(alloy_rlp::encode(Bal::from(changes)).into()).unwrap()
237 }
238
239 #[test]
240 fn a_canonical_pivot_orphans_nothing() {
241 let (old, _) = branches();
242 let (factory, write) = started(&old, 4);
243
244 let reorg = reorg(&factory, write).unwrap();
245
246 assert_eq!(reorg.ancestor(), old.tip());
247 assert!(reorg.orphaned().is_empty());
248 }
249
250 #[test]
251 fn the_ancestor_is_the_last_block_both_branches_share() {
252 let (old, new) = branches();
253 let (factory, write) = started(&old, 4);
254 new.replace_after(&factory, ANCESTOR);
255
256 let reorg = reorg(&factory, write).unwrap();
257
258 assert_eq!(reorg.ancestor(), old.block(0));
259 assert_eq!(reorg.orphaned(), kept(&old.headers[3..]));
260 }
261
262 #[test]
263 fn the_ancestor_can_sit_just_below_the_kept_blocks() {
264 let address = Address::repeat_byte(0x11);
265 let old =
266 BalChain::new(0, [credit(address, 1), credit(address, 2)]).with_state_root(root());
267 let new = BalChain::new(0, [credit(address, 10)]).with_state_root(root());
268 let (factory, write) = started(&old, 2);
269 new.replace_after(&factory, 0);
270
271 let reorg = reorg(&factory, write).unwrap();
272
273 assert_eq!(reorg.ancestor(), old.block(0));
274 assert_eq!(reorg.orphaned(), kept(&old.headers[1..]));
275 }
276
277 #[test]
278 fn a_reorg_below_the_kept_blocks_is_unrecoverable() {
279 let address = Address::repeat_byte(0x11);
280 let depth = KEPT_BLOCKS + 2;
281 let old = BalChain::new(1, (0..depth).map(|n| credit(address, n))).with_state_root(root());
282 let new =
283 BalChain::new(1, (0..depth).map(|n| credit(address, n + 100))).with_state_root(root());
284 let (factory, write) = started(&old, 1 + depth);
285 new.replace_after(&factory, 1);
286
287 assert!(reorg(&factory, write).is_none());
288 }
289
290 #[test]
291 fn recovery_rewinds_catch_up_to_the_ancestor_and_refuses_old_writes() {
292 let (old, new) = branches();
293 let (factory, write) = started(&old, 4);
294 new.replace_after(&factory, ANCESTOR);
295 let (_, accounts) = accounts();
296 let generation = SnapGeneration::new(new.tip(), state_root(&accounts));
297
298 let provider = factory.database_provider_rw().unwrap();
299 let recovered =
300 provider.commit_reorg_recovery(write, old.block(0), &[], generation).unwrap();
301 provider.commit().unwrap();
302
303 let provider = factory.database_provider_ro().unwrap();
304 assert_eq!(provider.catch_up_progress(recovered).unwrap().unwrap().applied(), old.block(0));
305 assert!(matches!(
306 provider.authorize_snap_write(write),
307 Err(SnapSyncError::StaleWrite { .. })
308 ));
309 drop(provider);
310 let reorg = reorg(&factory, recovered).unwrap();
312 assert_eq!(reorg.ancestor(), new.tip());
313 }
314
315 #[test]
316 fn a_new_pivot_reorged_meanwhile_is_refused_without_scheduling() {
317 let (old, new) = branches();
318 let (factory, write) = started(&old, 4);
319 new.replace_after(&factory, ANCESTOR);
320 let (addresses, accounts) = accounts();
321 let generation = SnapGeneration::new(old.tip(), state_root(&accounts));
323 let lists = [list(credit(addresses[0], 5))];
324
325 let provider = factory.database_provider_rw().unwrap();
326 let refused = provider.commit_reorg_recovery(write, old.block(0), &lists, generation);
327 assert!(matches!(refused, Err(SnapSyncError::NonCanonicalBlock { block: 4, .. })));
328 drop(provider);
329
330 let provider = factory.database_provider_ro().unwrap();
332 assert!(provider.snap_repairs(write).unwrap().is_empty());
333 assert_eq!(reorg(&factory, write).unwrap().ancestor(), old.block(0));
334 }
335
336 #[test]
337 fn catch_up_below_the_ancestor_stays_where_it_is() {
338 let (old, new) = branches();
339 let (factory, write) = started(&old, 1);
340 let (_, accounts) = accounts();
341 let provider = factory.database_provider_rw().unwrap();
342 let write = provider
343 .advance_snap_pivot(write, SnapGeneration::new(old.tip(), state_root(&accounts)))
344 .unwrap();
345 provider.commit().unwrap();
346 new.replace_after(&factory, ANCESTOR);
347
348 let provider = factory.database_provider_rw().unwrap();
349 let generation = SnapGeneration::new(new.tip(), state_root(&accounts));
350 let recovered =
351 provider.commit_reorg_recovery(write, old.block(0), &[], generation).unwrap();
352
353 assert_eq!(
354 provider.catch_up_progress(recovered).unwrap().unwrap().applied(),
355 old.headers[1].num_hash()
356 );
357 }
358
359 #[test]
360 fn orphaned_changes_to_downloaded_accounts_are_scheduled() {
361 let (old, new) = branches();
362 let (factory, write) = started(&old, 4);
363 new.replace_after(&factory, ANCESTOR);
364 let (addresses, accounts) = accounts();
365 let slot = SlotChanges::new(
366 U256::from(7),
367 vec![StorageChange::new(BlockAccessIndex::new(1), U256::ONE)],
368 );
369 let downloaded = credit(addresses[0], 5).remove(0).with_storage_change(slot);
370 let lists = [
371 list(vec![downloaded.clone()]),
372 list(vec![
373 AccountChanges::new(addresses[1]).with_storage_read(U256::ONE),
375 AccountChanges::new(addresses[2])
377 .with_nonce_change(NonceChange::new(BlockAccessIndex::new(1), 1)),
378 ]),
379 ];
380 let generation = SnapGeneration::new(new.tip(), state_root(&accounts));
381
382 let provider = factory.database_provider_rw().unwrap();
383 let recovered =
384 provider.commit_reorg_recovery(write, old.block(0), &lists, generation).unwrap();
385
386 let mut expected = StateRepairs::default();
387 expected.insert_changes(keccak256(addresses[0]), &downloaded);
388 assert_eq!(provider.snap_repairs(recovered).unwrap(), expected);
389 }
390}