1use crate::metrics::PersistenceMetrics;
2use alloy_eips::BlockNumHash;
3use crossbeam_channel::Sender as CrossbeamSender;
4use reth_errors::ProviderError;
5use reth_ethereum_primitives::EthPrimitives;
6use reth_primitives_traits::{FastInstant as Instant, NodePrimitives};
7use reth_provider::{
8 providers::ProviderNodeTypes, BalProvider, BlockExecutionWriter, BlockHashReader,
9 ChainStateBlockWriter, DBProvider, DatabaseProviderFactory, ProviderFactory, SaveBlocksInput,
10};
11use reth_prune::{PrunerError, PrunerWithFactory};
12use reth_stages_api::{MetricEvent, MetricEventsSender};
13use reth_tasks::spawn_os_thread;
14use std::{
15 sync::{
16 mpsc::{Receiver, SendError, Sender},
17 Arc,
18 },
19 thread::JoinHandle,
20 time::Duration,
21};
22use thiserror::Error;
23use tracing::{debug, error, instrument, warn};
24
25#[derive(Debug)]
27pub struct PersistenceResult {
28 pub last_block: BlockNumHash,
30 pub last_state_trie_block: BlockNumHash,
32 pub commit_duration: Option<Duration>,
34}
35
36#[derive(Debug)]
44pub struct PersistenceService<N>
45where
46 N: ProviderNodeTypes,
47{
48 provider: ProviderFactory<N>,
50 incoming: Receiver<PersistenceAction<N::Primitives>>,
52 pruner: PrunerWithFactory<ProviderFactory<N>>,
54 metrics: PersistenceMetrics,
56 sync_metrics_tx: MetricEventsSender,
58 pending_finalized_block: Option<u64>,
61 pending_safe_block: Option<u64>,
64}
65
66impl<N> PersistenceService<N>
67where
68 N: ProviderNodeTypes,
69{
70 pub fn new(
72 provider: ProviderFactory<N>,
73 incoming: Receiver<PersistenceAction<N::Primitives>>,
74 pruner: PrunerWithFactory<ProviderFactory<N>>,
75 sync_metrics_tx: MetricEventsSender,
76 ) -> Self {
77 Self {
78 provider,
79 incoming,
80 pruner,
81 metrics: PersistenceMetrics::default(),
82 sync_metrics_tx,
83 pending_finalized_block: None,
84 pending_safe_block: None,
85 }
86 }
87}
88
89impl<N> PersistenceService<N>
90where
91 N: ProviderNodeTypes,
92{
93 pub fn run(mut self) -> Result<(), PersistenceError> {
96 while let Ok(action) = self.incoming.recv() {
98 match action {
99 PersistenceAction::RemoveBlocksAbove(new_tip_num, sender) => {
100 let result = self.on_remove_blocks_above(new_tip_num)?;
101 let _ =
103 self.sync_metrics_tx.send(MetricEvent::SyncHeight { height: new_tip_num });
104 let _ = sender.send(result);
105 }
106 PersistenceAction::SaveBlocks(input, sender) => {
107 let new_db_tip = input.new_db_tip();
108 let db_tip_advanced = input.prev_db_tip() < new_db_tip;
109 let result = self.on_save_blocks(input)?;
110
111 let _ = sender.send(result);
112
113 if db_tip_advanced {
114 let _ = self
116 .sync_metrics_tx
117 .send(MetricEvent::SyncHeight { height: new_db_tip });
118 self.maybe_run_pruner(new_db_tip)?;
119 }
120 }
121 PersistenceAction::SaveFinalizedBlock(finalized_block) => {
122 self.pending_finalized_block = Some(finalized_block);
123 }
124 PersistenceAction::SaveSafeBlock(safe_block) => {
125 self.pending_safe_block = Some(safe_block);
126 }
127 }
128 }
129 Ok(())
130 }
131
132 #[instrument(level = "debug", target = "engine::persistence", skip_all, fields(%new_tip_num))]
133 fn on_remove_blocks_above(
134 &self,
135 new_tip_num: u64,
136 ) -> Result<PersistenceResult, PersistenceError> {
137 debug!(target: "engine::persistence", ?new_tip_num, "Removing blocks");
138 let start_time = Instant::now();
139 let provider_rw = self.provider.database_provider_rw()?;
140
141 let new_tip_hash = provider_rw
142 .block_hash(new_tip_num)?
143 .ok_or_else(|| ProviderError::HeaderNotFound(new_tip_num.into()))?;
144 let frontiers = provider_rw.remove_block_and_execution_above(new_tip_num)?;
145 debug_assert_eq!(frontiers.db_tip, new_tip_num);
146 let last_block = BlockNumHash::new(new_tip_num, new_tip_hash);
147 let last_state_trie_block = if frontiers.partial_state_trie == new_tip_num {
148 last_block
149 } else {
150 let hash = provider_rw.block_hash(frontiers.partial_state_trie)?.ok_or_else(|| {
151 ProviderError::HeaderNotFound(frontiers.partial_state_trie.into())
152 })?;
153 BlockNumHash::new(frontiers.partial_state_trie, hash)
154 };
155 provider_rw.commit()?;
156
157 debug!(target: "engine::persistence", ?new_tip_num, ?new_tip_hash, "Removed blocks from disk");
158 self.metrics.remove_blocks_above_duration_seconds.record(start_time.elapsed());
159 Ok(PersistenceResult { last_block, last_state_trie_block, commit_duration: None })
160 }
161
162 #[instrument(level = "debug", target = "engine::persistence", skip_all, fields(block_count = input.persist_rest_blocks().len()))]
163 fn on_save_blocks(
164 &mut self,
165 input: SaveBlocksInput<N::Primitives>,
166 ) -> Result<PersistenceResult, PersistenceError> {
167 let first_block =
168 input.first_persist_rest_block().map(|block| block.recovered_block().num_hash());
169 let last_block = input.last_block();
170 let block_count = input.persist_rest_blocks().len();
171
172 let pending_finalized = self.pending_finalized_block.take();
173 let pending_safe = self.pending_safe_block.take();
174
175 debug!(target: "engine::persistence", ?block_count, first=?first_block, last=?last_block, "Saving range of blocks");
176
177 let start_time = Instant::now();
178
179 let canonical_blocks = input
180 .persist_rest_blocks()
181 .iter()
182 .map(|block| block.recovered_block().num_hash())
183 .collect::<Vec<_>>();
184 let provider_rw = self.provider.database_provider_rw()?;
185 let last_state_trie_block = if let Some(block) = input.state_trie_blocks().last() {
186 block.recovered_block().num_hash()
189 } else {
190 let number = input.new_partial_state_trie();
193 let hash = provider_rw
194 .block_hash(number)?
195 .ok_or_else(|| ProviderError::HeaderNotFound(number.into()))?;
196 BlockNumHash::new(number, hash)
197 };
198 provider_rw.save_blocks(&input)?;
199
200 if let Some(finalized) = pending_finalized {
201 provider_rw.save_finalized_block_number(finalized.min(last_block.number))?;
202 if finalized > last_block.number {
203 self.pending_finalized_block = Some(finalized);
204 }
205 }
206 if let Some(safe) = pending_safe {
207 provider_rw.save_safe_block_number(safe.min(last_block.number))?;
208 if safe > last_block.number {
209 self.pending_safe_block = Some(safe);
210 }
211 }
212
213 provider_rw.commit()?;
214 let _ = self.provider.bal_store().flush(&canonical_blocks).inspect_err(|err| {
216 warn!(target: "engine::persistence", last=?last_block, ?err, "Failed to flush BAL store");
217 });
218 debug!(target: "engine::persistence", first=?first_block, last=?last_block, "Saved range of blocks");
219
220 let elapsed = start_time.elapsed();
221 self.metrics.save_blocks_batch_size.record(block_count as f64);
222 self.metrics.save_blocks_duration_seconds.record(elapsed);
223
224 Ok(PersistenceResult { last_block, last_state_trie_block, commit_duration: Some(elapsed) })
225 }
226
227 fn maybe_run_pruner(&mut self, block_number: u64) -> Result<(), PersistenceError> {
228 if self.pruner.is_pruning_needed(block_number) {
231 debug!(target: "engine::persistence", block_num=?block_number, "Running pruner");
232 let prune_start = Instant::now();
233 let provider_rw = self.provider.database_provider_rw()?;
234 let _ = self.pruner.run_with_provider(&provider_rw, block_number)?;
235 provider_rw.commit()?;
236 let pruned_bals = self
237 .provider
238 .bal_store()
239 .prune(block_number)
240 .inspect_err(|err| {
241 warn!(target: "engine::persistence", tip=?block_number, ?err, "Failed to prune BAL store");
242 })
243 .unwrap_or_default();
244 debug!(target: "engine::persistence", tip=?block_number, pruned_bals, "Finished pruning after saving blocks");
245 self.metrics.prune_before_duration_seconds.record(prune_start.elapsed());
246 }
247
248 Ok(())
249 }
250}
251
252#[derive(Debug, Error)]
254pub enum PersistenceError {
255 #[error(transparent)]
257 PrunerError(#[from] PrunerError),
258
259 #[error(transparent)]
261 ProviderError(#[from] ProviderError),
262}
263
264#[derive(Debug)]
266pub enum PersistenceAction<N: NodePrimitives = EthPrimitives> {
267 SaveBlocks(SaveBlocksInput<N>, CrossbeamSender<PersistenceResult>),
269
270 RemoveBlocksAbove(u64, CrossbeamSender<PersistenceResult>),
275
276 SaveFinalizedBlock(u64),
278
279 SaveSafeBlock(u64),
281}
282
283#[derive(Debug, Clone)]
285pub struct PersistenceHandle<N: NodePrimitives = EthPrimitives> {
286 sender: Sender<PersistenceAction<N>>,
288 _service_guard: Arc<ServiceGuard>,
291}
292
293impl<T: NodePrimitives> PersistenceHandle<T> {
294 pub fn new(sender: Sender<PersistenceAction<T>>) -> Self {
299 Self { sender, _service_guard: Arc::new(ServiceGuard(None)) }
300 }
301
302 pub fn spawn_service<N>(
308 provider_factory: ProviderFactory<N>,
309 pruner: PrunerWithFactory<ProviderFactory<N>>,
310 sync_metrics_tx: MetricEventsSender,
311 ) -> PersistenceHandle<N::Primitives>
312 where
313 N: ProviderNodeTypes,
314 {
315 let (db_service_tx, db_service_rx) = std::sync::mpsc::channel();
317
318 let db_service =
320 PersistenceService::new(provider_factory, db_service_rx, pruner, sync_metrics_tx);
321 let join_handle = spawn_os_thread("persistence", || {
322 if let Err(err) = db_service.run() {
323 error!(target: "engine::persistence", ?err, "Persistence service failed");
324 }
325 });
326
327 PersistenceHandle {
328 sender: db_service_tx,
329 _service_guard: Arc::new(ServiceGuard(Some(join_handle))),
330 }
331 }
332
333 pub fn send_action(
336 &self,
337 action: PersistenceAction<T>,
338 ) -> Result<(), SendError<PersistenceAction<T>>> {
339 self.sender.send(action)
340 }
341
342 pub fn save_blocks(
348 &self,
349 input: SaveBlocksInput<T>,
350 tx: CrossbeamSender<PersistenceResult>,
351 ) -> Result<(), SendError<PersistenceAction<T>>> {
352 self.send_action(PersistenceAction::SaveBlocks(input, tx))
353 }
354
355 pub fn save_finalized_block_number(
360 &self,
361 finalized_block: u64,
362 ) -> Result<(), SendError<PersistenceAction<T>>> {
363 self.send_action(PersistenceAction::SaveFinalizedBlock(finalized_block))
364 }
365
366 pub fn save_safe_block_number(
371 &self,
372 safe_block: u64,
373 ) -> Result<(), SendError<PersistenceAction<T>>> {
374 self.send_action(PersistenceAction::SaveSafeBlock(safe_block))
375 }
376
377 pub fn remove_blocks_above(
383 &self,
384 block_num: u64,
385 tx: CrossbeamSender<PersistenceResult>,
386 ) -> Result<(), SendError<PersistenceAction<T>>> {
387 self.send_action(PersistenceAction::RemoveBlocksAbove(block_num, tx))
388 }
389}
390
391struct ServiceGuard(Option<JoinHandle<()>>);
397
398impl std::fmt::Debug for ServiceGuard {
399 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
400 f.debug_tuple("ServiceGuard").field(&self.0.as_ref().map(|_| "...")).finish()
401 }
402}
403
404impl Drop for ServiceGuard {
405 fn drop(&mut self) {
406 if let Some(join_handle) = self.0.take() {
407 let _ = join_handle.join();
408 }
409 }
410}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415 use alloy_eips::NumHash;
416 use alloy_primitives::{BlockHash, BlockNumber, Bytes, B256, U256};
417 use reth_chain_state::{test_utils::TestBlockBuilder, ExecutedBlock};
418 use reth_db_common::init::init_genesis;
419 use reth_exex_types::FinishedExExHeight;
420 use reth_provider::{
421 providers::{ProviderFactoryBuilder, ReadOnlyConfig},
422 test_utils::{create_test_provider_factory, MockNodeTypes},
423 AccountReader, BalConfig, BalNotificationStream, BalStore, BalStoreHandle,
424 ChainSpecProvider, HeaderProvider, InMemoryBalStore, ProviderError, ProviderResult, RawBal,
425 StorageSettingsCache, TryIntoHistoricalStateProvider,
426 };
427 use reth_prune::Pruner;
428 use reth_prune_types::PruneMode;
429 use tokio::sync::mpsc::unbounded_channel;
430
431 fn default_persistence_handle() -> PersistenceHandle<EthPrimitives> {
432 let provider = create_test_provider_factory();
433 init_genesis(&provider).unwrap();
434
435 let (_finished_exex_height_tx, finished_exex_height_rx) =
436 tokio::sync::watch::channel(FinishedExExHeight::NoExExs);
437
438 let pruner =
439 Pruner::new_with_factory(provider.clone(), vec![], 5, 0, None, finished_exex_height_rx);
440
441 let (sync_metrics_tx, _sync_metrics_rx) = unbounded_channel();
442 PersistenceHandle::<EthPrimitives>::spawn_service(provider, pruner, sync_metrics_tx)
443 }
444
445 fn full_save_input(
446 blocks: Vec<ExecutedBlock<EthPrimitives>>,
447 ) -> SaveBlocksInput<EthPrimitives> {
448 let prev_tip = blocks
449 .first()
450 .map(|block| block.recovered_block().number.saturating_sub(1))
451 .expect("save input must not be empty");
452 let new_tip =
453 blocks.last().map(|block| block.recovered_block().number).expect("checked non-empty");
454 SaveBlocksInput::new(blocks, prev_tip, prev_tip, new_tip, new_tip)
455 }
456
457 #[test]
458 fn test_pruner_prunes_bal_store() {
459 reth_tracing::init_test_tracing();
460
461 let old_hash = B256::random();
462 let retained_hash = B256::random();
463 let old_bal = Bytes::from_static(b"old");
464 let retained_bal = Bytes::from_static(b"retained");
465 let bal_store = BalStoreHandle::new(InMemoryBalStore::new(
466 BalConfig::with_in_memory_retention(PruneMode::Before(2)),
467 ));
468
469 bal_store.insert(NumHash::new(1, old_hash), RawBal::new(old_bal)).unwrap();
470 bal_store
471 .insert(NumHash::new(2, retained_hash), RawBal::new(retained_bal.clone()))
472 .unwrap();
473
474 let provider = create_test_provider_factory().with_bal_store(bal_store.clone());
475 let (_finished_exex_height_tx, finished_exex_height_rx) =
476 tokio::sync::watch::channel(FinishedExExHeight::NoExExs);
477 let pruner =
478 Pruner::new_with_factory(provider.clone(), vec![], 0, 0, None, finished_exex_height_rx);
479 let (_db_service_tx, db_service_rx) = std::sync::mpsc::channel();
480 let (sync_metrics_tx, _sync_metrics_rx) = unbounded_channel();
481 let mut service = PersistenceService::new(provider, db_service_rx, pruner, sync_metrics_tx);
482
483 service.maybe_run_pruner(2).unwrap();
484
485 assert_eq!(
486 bal_store.get_by_hashes(&[old_hash, retained_hash]).unwrap(),
487 vec![None, Some(retained_bal)]
488 );
489 }
490
491 #[test]
492 fn test_pruner_ignores_bal_store_prune_error() {
493 reth_tracing::init_test_tracing();
494
495 let provider = create_test_provider_factory()
496 .with_bal_store(BalStoreHandle::new(FailingPruneBalStore));
497 let (_finished_exex_height_tx, finished_exex_height_rx) =
498 tokio::sync::watch::channel(FinishedExExHeight::NoExExs);
499 let pruner =
500 Pruner::new_with_factory(provider.clone(), vec![], 0, 0, None, finished_exex_height_rx);
501 let (_db_service_tx, db_service_rx) = std::sync::mpsc::channel();
502 let (sync_metrics_tx, _sync_metrics_rx) = unbounded_channel();
503 let mut service = PersistenceService::new(provider, db_service_rx, pruner, sync_metrics_tx);
504
505 service.maybe_run_pruner(2).unwrap();
506 }
507
508 #[test]
509 fn test_remove_blocks_above_requires_tip_header() {
510 let provider = create_test_provider_factory();
511 init_genesis(&provider).unwrap();
512
513 let (_finished_exex_height_tx, finished_exex_height_rx) =
514 tokio::sync::watch::channel(FinishedExExHeight::NoExExs);
515 let pruner =
516 Pruner::new_with_factory(provider.clone(), vec![], 5, 0, None, finished_exex_height_rx);
517 let (_db_service_tx, db_service_rx) = std::sync::mpsc::channel();
518 let (sync_metrics_tx, _sync_metrics_rx) = unbounded_channel();
519 let service = PersistenceService::new(provider, db_service_rx, pruner, sync_metrics_tx);
520
521 assert!(matches!(
522 service.on_remove_blocks_above(1),
523 Err(PersistenceError::ProviderError(ProviderError::HeaderNotFound(_)))
524 ));
525 }
526
527 #[derive(Debug)]
528 struct FailingPruneBalStore;
529
530 impl BalStore for FailingPruneBalStore {
531 fn insert(&self, _num_hash: NumHash, _bal: RawBal) -> ProviderResult<()> {
532 Ok(())
533 }
534
535 fn prune(&self, _tip: BlockNumber) -> ProviderResult<usize> {
536 Err(ProviderError::other(std::io::Error::other("BAL store prune failed")))
537 }
538
539 fn get_by_hashes(&self, block_hashes: &[BlockHash]) -> ProviderResult<Vec<Option<Bytes>>> {
540 Ok(vec![None; block_hashes.len()])
541 }
542
543 fn bal_stream(&self) -> BalNotificationStream {
544 BalStoreHandle::noop().bal_stream()
545 }
546 }
547
548 #[test]
549 fn test_save_blocks_single_block() {
550 reth_tracing::init_test_tracing();
551 let handle = default_persistence_handle();
552 let block_number = 1;
553 let mut test_block_builder = TestBlockBuilder::eth();
554 let executed =
555 test_block_builder.get_executed_block_with_number(block_number, B256::random());
556 let block_hash = executed.recovered_block().hash();
557
558 let blocks = full_save_input(vec![executed]);
559 let (tx, rx) = crossbeam_channel::bounded(1);
560
561 handle.save_blocks(blocks, tx).unwrap();
562
563 let result = rx.recv_timeout(std::time::Duration::from_secs(10)).expect("test timed out");
564
565 assert_eq!(block_hash, result.last_block.hash);
566 assert_eq!(result.last_state_trie_block, result.last_block);
567 }
568
569 #[test]
570 fn test_save_blocks_flushes_bal_store() {
571 use reth_provider::{RocksDBBalStore, RocksDBProviderFactory};
572
573 reth_tracing::init_test_tracing();
574 let provider = create_test_provider_factory();
575 let bal_store = BalStoreHandle::new(RocksDBBalStore::new(provider.rocksdb_provider()));
576 let provider = provider.with_bal_store(bal_store);
577 init_genesis(&provider).unwrap();
578
579 let mut test_block_builder = TestBlockBuilder::eth();
580 let executed = test_block_builder.get_executed_block_with_number(1, B256::random());
581 let num_hash = executed.recovered_block().num_hash();
582 let raw_bal = Bytes::from_static(&[0xc0]);
583
584 provider.bal_store().insert(num_hash, RawBal::new(raw_bal.clone())).unwrap();
585
586 let (_finished_exex_height_tx, finished_exex_height_rx) =
587 tokio::sync::watch::channel(FinishedExExHeight::NoExExs);
588 let pruner =
589 Pruner::new_with_factory(provider.clone(), vec![], 5, 0, None, finished_exex_height_rx);
590 let (sync_metrics_tx, _sync_metrics_rx) = unbounded_channel();
591 let handle = PersistenceHandle::<EthPrimitives>::spawn_service(
592 provider.clone(),
593 pruner,
594 sync_metrics_tx,
595 );
596 let (tx, rx) = crossbeam_channel::bounded(1);
597
598 handle.save_blocks(full_save_input(vec![executed]), tx).unwrap();
599
600 let result = rx.recv_timeout(std::time::Duration::from_secs(10)).expect("test timed out");
601 assert_eq!(result.last_block, num_hash);
602 let persisted_store = RocksDBBalStore::new(provider.rocksdb_provider());
603 assert_eq!(persisted_store.get_by_hash(num_hash.hash).unwrap(), Some(raw_bal));
604 }
605
606 #[test]
607 fn test_save_blocks_multiple_blocks() {
608 reth_tracing::init_test_tracing();
609 let handle = default_persistence_handle();
610
611 let mut test_block_builder = TestBlockBuilder::eth();
612 let blocks = test_block_builder.get_executed_blocks(1..6).collect::<Vec<_>>();
613 let last_hash = blocks.last().unwrap().recovered_block().hash();
614 let (tx, rx) = crossbeam_channel::bounded(1);
615
616 handle.save_blocks(full_save_input(blocks), tx).unwrap();
617 let result = rx.recv().unwrap();
618 assert_eq!(last_hash, result.last_block.hash);
619 }
620
621 #[test]
622 fn test_save_blocks_multiple_calls() {
623 reth_tracing::init_test_tracing();
624 let handle = default_persistence_handle();
625
626 let ranges = [1..2, 2..3, 3..5, 5..6];
627 let mut test_block_builder = TestBlockBuilder::eth();
628 for range in ranges {
629 let blocks = test_block_builder.get_executed_blocks(range).collect::<Vec<_>>();
630 let last_hash = blocks.last().unwrap().recovered_block().hash();
631 let (tx, rx) = crossbeam_channel::bounded(1);
632
633 handle.save_blocks(full_save_input(blocks), tx).unwrap();
634
635 let result = rx.recv().unwrap();
636 assert_eq!(last_hash, result.last_block.hash);
637 }
638 }
639
640 #[test]
648 fn test_save_blocks_then_prune_preserves_new_history() {
649 use reth_db::{models::ShardedKey, tables, BlockNumberList};
650 use reth_provider::RocksDBProviderFactory;
651
652 reth_tracing::init_test_tracing();
653
654 let provider_factory = create_test_provider_factory();
655 let tracked_addr = alloy_primitives::Address::from([0xBE; 20]);
656
657 let rocksdb = provider_factory.rocksdb_provider();
659 {
660 let mut batch = rocksdb.batch();
661 let initial_blocks: Vec<u64> = (0..20).collect();
662 let shard = BlockNumberList::new_pre_sorted(initial_blocks.iter().copied());
663 batch
664 .put::<tables::AccountsHistory>(ShardedKey::new(tracked_addr, u64::MAX), &shard)
665 .unwrap();
666 batch.commit().unwrap();
667 }
668
669 let mut batch1 = rocksdb.batch();
672 batch1.append_account_history_shard(tracked_addr, 20..25u64).unwrap();
673 batch1.commit().unwrap();
674
675 let mut batch2 = rocksdb.batch();
678 batch2.prune_account_history_to(tracked_addr, 14).unwrap();
679 batch2.commit().unwrap();
680
681 let shards = rocksdb.account_history_shards(tracked_addr).unwrap();
683 let entries: Vec<u64> = shards.iter().flat_map(|(_, list)| list.iter()).collect();
684 let expected: Vec<u64> = (15..25).collect();
685 assert_eq!(entries, expected, "new entries 20..25 must survive pruning");
686 }
687
688 #[test]
689 fn test_read_only_consistency_across_reorg() {
690 reth_tracing::init_test_tracing();
691
692 reth_db::test_utils::enable_legacy_multiopen();
694
695 let provider_factory = create_test_provider_factory();
696 provider_factory.set_storage_settings_cache(reth_provider::StorageSettings::v2());
697
698 let secondary = ProviderFactoryBuilder::<MockNodeTypes>::default()
700 .open_read_only(
701 provider_factory.chain_spec(),
702 ReadOnlyConfig::from_datadir(provider_factory.db_ref().path()),
703 reth_tasks::Runtime::test(),
704 )
705 .expect("failed to open read-only provider factory");
706 secondary.set_storage_settings_cache(reth_provider::StorageSettings::v2());
707
708 let genesis_hash = init_genesis(&provider_factory).unwrap();
710 let mut test_block_builder = TestBlockBuilder::eth().with_state();
711 let signer = test_block_builder.signer;
712 let initial_balance = U256::from(10).pow(U256::from(18));
713 let block_a1 = test_block_builder.get_executed_block_with_number(1, genesis_hash);
714 let hash_a1 = block_a1.recovered_block().hash();
715 let block_a2 = test_block_builder.get_executed_block_with_number(2, hash_a1);
716 let hash_a2 = block_a2.recovered_block().hash();
717
718 let single_cost = TestBlockBuilder::<EthPrimitives>::single_tx_cost();
720 let txs_in_block1 = block_a1.recovered_block().body().transactions.len() as u64;
721
722 let balance_after_block1 = initial_balance - single_cost * U256::from(txs_in_block1);
723 let nonce_after_block1 = txs_in_block1;
724
725 let provider_rw = provider_factory.database_provider_rw().unwrap();
726 let input = SaveBlocksInput::new(vec![block_a1, block_a2], 0, 0, 2, 2);
727 provider_rw.save_blocks(&input).unwrap();
728 provider_rw.commit().unwrap();
729
730 let pre_reorg_provider = secondary.provider().unwrap();
733 assert_eq!(
734 pre_reorg_provider.sealed_header(2).unwrap().as_ref().map(|h| h.hash()),
735 Some(hash_a2),
736 "secondary must see block 2 after initial append"
737 );
738
739 {
741 let primary_state_at_1 = provider_factory.history_by_block_number(1).unwrap();
742 let primary_account = primary_state_at_1.basic_account(&signer).unwrap();
743 assert!(primary_account.is_some(), "primary: signer must exist at block 1");
744 }
745
746 {
748 let state_at_1 = secondary.history_by_block_number(1).unwrap();
749 let account_at_1 = state_at_1.basic_account(&signer).unwrap();
750 assert!(account_at_1.is_some(), "signer account must exist at block 1");
751 let account_at_1 = account_at_1.unwrap();
752 assert_eq!(account_at_1.balance, balance_after_block1, "signer balance at block 1");
753 assert_eq!(account_at_1.nonce, nonce_after_block1, "signer nonce at block 1");
754 }
755
756 let block_b2 = test_block_builder.get_executed_block_with_number(2, hash_a1);
760 let hash_b2 = block_b2.recovered_block().hash();
761 let txs_in_block_b2 = block_b2.recovered_block().body().transactions.len() as u64;
762 assert_ne!(hash_a2, hash_b2, "reorg block must differ");
763
764 let balance_after_reorg_block2 =
766 balance_after_block1 - single_cost * U256::from(txs_in_block_b2);
767 let nonce_after_reorg_block2 = nonce_after_block1 + txs_in_block_b2;
768
769 #[expect(clippy::redundant_clone)]
776 let pf = provider_factory.clone();
777 let reorg_handle = std::thread::spawn(move || {
778 let provider_rw = pf.database_provider_rw().unwrap();
779 let frontiers = provider_rw.remove_block_and_execution_above(1).unwrap();
780 assert_eq!(frontiers.partial_state_trie, 1);
781 provider_rw.commit().unwrap();
782
783 let provider_rw = pf.database_provider_rw().unwrap();
784 let input = SaveBlocksInput::new(vec![block_b2], 1, 1, 2, 2);
785 provider_rw.save_blocks(&input).unwrap();
786 provider_rw.commit().unwrap();
787 });
788
789 std::thread::sleep(std::time::Duration::from_millis(100));
791
792 assert_eq!(
795 pre_reorg_provider.sealed_header(2).unwrap().as_ref().map(|h| h.hash()),
796 Some(hash_a2),
797 "pre-reorg provider must still see the original block 2"
798 );
799 assert_eq!(
800 pre_reorg_provider.sealed_header(1).unwrap().as_ref().map(|h| h.hash()),
801 Some(hash_a1),
802 "pre-reorg provider must still see block 1"
803 );
804
805 let state_at_1 = pre_reorg_provider.try_into_history_at_block(1).unwrap();
809 let account = state_at_1.basic_account(&signer).unwrap();
810 assert!(
811 account.is_some(),
812 "pre-reorg RO tx must still read signer at block 1 during reorg"
813 );
814 let account = account.unwrap();
815 assert_eq!(
816 account.balance, balance_after_block1,
817 "pre-reorg RO tx: signer balance at block 1 during reorg"
818 );
819 assert_eq!(
820 account.nonce, nonce_after_block1,
821 "pre-reorg RO tx: signer nonce at block 1 during reorg"
822 );
823 drop(state_at_1);
824 reorg_handle.join().expect("reorg thread panicked");
825
826 let obs_header = secondary.provider().unwrap().sealed_header(2).unwrap();
828 assert_eq!(
829 obs_header.as_ref().map(|h| h.hash()),
830 Some(hash_b2),
831 "secondary must see the reorged block 2, not the old one"
832 );
833
834 let obs_header = secondary.provider().unwrap().sealed_header(1).unwrap();
836 assert_eq!(
837 obs_header.as_ref().map(|h| h.hash()),
838 Some(hash_a1),
839 "secondary must still see block 1"
840 );
841
842 let state_at_1 = secondary.history_by_block_number(1).unwrap();
844 let account_at_1 = state_at_1.basic_account(&signer).unwrap();
845 assert!(account_at_1.is_some(), "signer account must exist at block 1 after reorg");
846 let account_at_1 = account_at_1.unwrap();
847 assert_eq!(
848 account_at_1.balance, balance_after_block1,
849 "signer balance at block 1 must survive reorg"
850 );
851 assert_eq!(
852 account_at_1.nonce, nonce_after_block1,
853 "signer nonce at block 1 must survive reorg"
854 );
855
856 let state_at_2 = secondary.history_by_block_number(2).unwrap();
858 let account_at_2 = state_at_2.basic_account(&signer).unwrap();
859 assert!(account_at_2.is_some(), "signer account must exist at block 2 after reorg");
860 let account_at_2 = account_at_2.unwrap();
861 assert_eq!(
862 account_at_2.balance, balance_after_reorg_block2,
863 "signer balance at block 2 must reflect reorged execution"
864 );
865 assert_eq!(
866 account_at_2.nonce, nonce_after_reorg_block2,
867 "signer nonce at block 2 must reflect reorged execution"
868 );
869 }
870}