1use crate::{
33 components::{NodeComponents, NodeComponentsBuilder},
34 hooks::OnComponentInitializedHook,
35 BuilderContext, ExExLauncher, NodeAdapter, PrimitivesTy,
36};
37use alloy_eips::eip2124::Head;
38use alloy_primitives::{BlockNumber, B256};
39use eyre::Context;
40use rayon::ThreadPoolBuilder;
41use reth_chainspec::{Chain, EthChainSpec, EthereumHardforks};
42use reth_config::{config::EtlConfig, PruneConfig};
43use reth_consensus::noop::NoopConsensus;
44use reth_db_api::{
45 database::Database, database_metrics::DatabaseMetrics, models::PartialStateTrieUnwindMarker,
46};
47use reth_db_common::init::{
48 init_genesis_with_settings, init_genesis_with_settings_and_validate, InitStorageError,
49};
50use reth_downloaders::{bodies::noop::NoopBodiesDownloader, headers::noop::NoopHeaderDownloader};
51use reth_engine_local::MiningMode;
52use reth_evm::{noop::NoopEvmConfig, ConfigureEvm};
53use reth_exex::ExExManagerHandle;
54use reth_fs_util as fs;
55use reth_network_p2p::headers::client::HeadersClient;
56use reth_node_api::{FullNodeTypes, NodeTypes, NodeTypesWithDB, NodeTypesWithDBAdapter};
57use reth_node_core::{
58 args::{DefaultEraHost, PruneConfigKind},
59 dirs::{ChainPath, DataDirPath},
60 node_config::NodeConfig,
61 primitives::BlockHeader,
62 version::version_metadata,
63};
64use reth_node_metrics::{
65 chain::ChainSpecInfo,
66 hooks::Hooks,
67 recorder::install_prometheus_recorder,
68 server::{MetricServer, MetricServerConfig},
69 storage::StorageSettingsInfo,
70 version::VersionInfo,
71};
72use reth_provider::{
73 providers::{NodeTypesForProvider, ProviderNodeTypes, RocksDBProvider, StaticFileProvider},
74 BalStoreHandle, BlockHashReader, BlockNumReader, DBProvider, DatabaseProviderFactory,
75 MetadataProvider, MetadataWriter, ProviderError, ProviderFactory, ProviderResult,
76 RocksDBBalStore, RocksDBProviderFactory, StageCheckpointReader, StaticFileProviderBuilder,
77 StaticFileProviderFactory, StorageSettingsCache,
78};
79use reth_prune::{PruneMode, PruneModes, PrunerBuilder};
80use reth_rpc_builder::config::RethRpcServerConfig;
81use reth_rpc_layer::JwtSecret;
82use reth_stages::{
83 sets::DefaultStages,
84 stages::{EraImportSource, MerkleStage},
85 MetricEvent, PipelineBuilder, PipelineTarget, StageId, StageSet,
86};
87use reth_static_file::{blocks_per_file_for_prune_distance, StaticFileProducer, StaticFileSegment};
88use reth_storage_overlay::OverlayManager;
89use reth_tasks::TaskExecutor;
90use reth_tracing::{
91 throttle,
92 tracing::{debug, error, info, warn},
93};
94use reth_transaction_pool::TransactionPool;
95use std::{num::NonZeroUsize, sync::Arc, thread::available_parallelism, time::Duration};
96use tokio::sync::{
97 mpsc::{unbounded_channel, UnboundedSender},
98 oneshot, watch,
99};
100
101use futures::{future::Either, stream, Stream, StreamExt};
102use reth_node_ethstats::EthStatsService;
103use reth_node_events::{cl::ConsensusLayerHealthEvents, node::NodeEvent};
104
105#[derive(Debug, Clone)]
124pub struct LaunchContext {
125 pub task_executor: TaskExecutor,
127 pub data_dir: ChainPath<DataDirPath>,
129}
130
131impl LaunchContext {
132 pub const fn new(task_executor: TaskExecutor, data_dir: ChainPath<DataDirPath>) -> Self {
134 Self { task_executor, data_dir }
135 }
136
137 pub const fn with<T>(self, attachment: T) -> LaunchContextWith<T> {
139 LaunchContextWith { inner: self, attachment }
140 }
141
142 pub fn with_loaded_toml_config<ChainSpec>(
147 self,
148 config: NodeConfig<ChainSpec>,
149 ) -> eyre::Result<LaunchContextWith<WithConfigs<ChainSpec>>>
150 where
151 ChainSpec: EthChainSpec + reth_chainspec::EthereumHardforks,
152 {
153 let toml_config = self.load_toml_config(&config)?;
154 Ok(self.with(WithConfigs { config, toml_config }))
155 }
156
157 pub fn load_toml_config<ChainSpec>(
162 &self,
163 config: &NodeConfig<ChainSpec>,
164 ) -> eyre::Result<reth_config::Config>
165 where
166 ChainSpec: EthChainSpec + reth_chainspec::EthereumHardforks,
167 {
168 let config_path = config.config.clone().unwrap_or_else(|| self.data_dir.config());
169
170 let mut toml_config = reth_config::Config::from_path(&config_path)
171 .wrap_err_with(|| format!("Could not load config file {config_path:?}"))?;
172
173 Self::save_pruning_config(&mut toml_config, config, &config_path)?;
174
175 info!(target: "reth::cli", path = ?config_path, "Configuration loaded");
176
177 toml_config.peers.trusted_nodes_only |= config.network.trusted_only;
180
181 toml_config.static_files =
183 config.static_files.merge_with_config(toml_config.static_files, config.pruning.minimal);
184
185 Ok(toml_config)
186 }
187
188 fn save_pruning_config<ChainSpec>(
191 reth_config: &mut reth_config::Config,
192 config: &NodeConfig<ChainSpec>,
193 config_path: impl AsRef<std::path::Path>,
194 ) -> eyre::Result<()>
195 where
196 ChainSpec: EthChainSpec + reth_chainspec::EthereumHardforks,
197 {
198 let mut should_save = reth_config.prune.segments.migrate();
199
200 if let Some(prune_config) = config.prune_config() {
201 if reth_config.prune != prune_config {
202 reth_config.set_prune_config(prune_config);
203 should_save = true;
204 }
205 } else if !reth_config.prune.is_default() {
206 info!(target: "reth::cli", "Pruning configuration is present in the config file, but no CLI arguments are provided. Using config from file.");
207 }
208
209 if should_save {
210 info!(target: "reth::cli", "Saving prune config to toml file");
211 reth_config.save(config_path.as_ref())?;
212 }
213
214 Ok(())
215 }
216
217 pub fn with_configured_globals(self, reserved_cpu_cores: usize) -> Self {
219 self.configure_globals(reserved_cpu_cores);
220 self
221 }
222
223 pub fn configure_globals(&self, reserved_cpu_cores: usize) {
228 match fdlimit::raise_fd_limit() {
231 Ok(fdlimit::Outcome::LimitRaised { from, to }) => {
232 debug!(from, to, "Raised file descriptor limit");
233 }
234 Ok(fdlimit::Outcome::Unsupported) => {}
235 Err(err) => warn!(%err, "Failed to raise file descriptor limit"),
236 }
237
238 let _ = reserved_cpu_cores;
242 let num_threads = available_parallelism().map_or(1, NonZeroUsize::get);
243 if let Err(err) = ThreadPoolBuilder::new()
244 .num_threads(num_threads)
245 .thread_name(|i| format!("rayon-{i:02}"))
246 .build_global()
247 {
248 warn!(%err, "Failed to build global thread pool")
249 }
250 }
251}
252
253#[derive(Debug, Clone)]
264pub struct LaunchContextWith<T> {
265 pub inner: LaunchContext,
267 pub attachment: T,
269}
270
271impl<T> LaunchContextWith<T> {
272 pub fn configure_globals(&self, reserved_cpu_cores: u64) {
277 self.inner.configure_globals(reserved_cpu_cores.try_into().unwrap());
278 }
279
280 pub const fn data_dir(&self) -> &ChainPath<DataDirPath> {
282 &self.inner.data_dir
283 }
284
285 pub const fn task_executor(&self) -> &TaskExecutor {
287 &self.inner.task_executor
288 }
289
290 pub fn attach<A>(self, attachment: A) -> LaunchContextWith<Attached<T, A>> {
292 LaunchContextWith {
293 inner: self.inner,
294 attachment: Attached::new(self.attachment, attachment),
295 }
296 }
297
298 pub fn inspect<F>(self, f: F) -> Self
301 where
302 F: FnOnce(&Self),
303 {
304 f(&self);
305 self
306 }
307}
308
309impl<ChainSpec> LaunchContextWith<WithConfigs<ChainSpec>> {
310 pub fn with_resolved_peers(mut self) -> eyre::Result<Self> {
312 if !self.attachment.config.network.trusted_peers.is_empty() {
313 info!(target: "reth::cli", "Adding trusted nodes");
314
315 self.attachment
316 .toml_config
317 .peers
318 .trusted_nodes
319 .extend(self.attachment.config.network.trusted_peers.clone());
320 }
321 Ok(self)
322 }
323}
324
325impl<L, R> LaunchContextWith<Attached<L, R>> {
326 pub const fn left(&self) -> &L {
328 &self.attachment.left
329 }
330
331 pub const fn right(&self) -> &R {
333 &self.attachment.right
334 }
335
336 pub const fn left_mut(&mut self) -> &mut L {
338 &mut self.attachment.left
339 }
340
341 pub const fn right_mut(&mut self) -> &mut R {
343 &mut self.attachment.right
344 }
345}
346impl<R, ChainSpec: EthChainSpec> LaunchContextWith<Attached<WithConfigs<ChainSpec>, R>> {
347 pub fn with_adjusted_configs(self) -> Self {
353 self.ensure_etl_datadir().with_adjusted_instance_ports()
354 }
355
356 pub fn ensure_etl_datadir(mut self) -> Self {
358 if self.toml_config_mut().stages.etl.dir.is_none() {
359 let etl_path = EtlConfig::from_datadir(self.data_dir().data_dir());
360 if etl_path.exists() {
361 if let Err(err) = fs::remove_dir_all(&etl_path) {
363 warn!(target: "reth::cli", ?etl_path, %err, "Failed to remove ETL path on launch");
364 }
365 }
366 self.toml_config_mut().stages.etl.dir = Some(etl_path);
367 }
368
369 self
370 }
371
372 pub fn with_adjusted_instance_ports(mut self) -> Self {
374 self.node_config_mut().adjust_instance_ports();
375 self
376 }
377
378 pub const fn configs(&self) -> &WithConfigs<ChainSpec> {
380 self.attachment.left()
381 }
382
383 pub const fn node_config(&self) -> &NodeConfig<ChainSpec> {
385 &self.left().config
386 }
387
388 pub const fn node_config_mut(&mut self) -> &mut NodeConfig<ChainSpec> {
390 &mut self.left_mut().config
391 }
392
393 pub const fn toml_config(&self) -> &reth_config::Config {
395 &self.left().toml_config
396 }
397
398 pub const fn toml_config_mut(&mut self) -> &mut reth_config::Config {
400 &mut self.left_mut().toml_config
401 }
402
403 pub fn chain_spec(&self) -> Arc<ChainSpec> {
405 self.node_config().chain.clone()
406 }
407
408 pub fn genesis_hash(&self) -> B256 {
410 self.node_config().chain.genesis_hash()
411 }
412
413 pub fn chain_id(&self) -> Chain {
415 self.node_config().chain.chain()
416 }
417
418 pub const fn is_dev(&self) -> bool {
420 self.node_config().dev.dev
421 }
422
423 pub fn prune_config(&self) -> PruneConfig
427 where
428 ChainSpec: reth_chainspec::EthereumHardforks,
429 {
430 let Some(mut node_prune_config) = self.node_config().prune_config() else {
431 return self.toml_config().prune.clone();
433 };
434
435 node_prune_config.merge(self.toml_config().prune.clone());
437 node_prune_config
438 }
439
440 pub fn prune_modes(&self) -> PruneModes
442 where
443 ChainSpec: reth_chainspec::EthereumHardforks,
444 {
445 self.prune_config().segments
446 }
447
448 pub fn pruner_builder(&self) -> PrunerBuilder
450 where
451 ChainSpec: reth_chainspec::EthereumHardforks,
452 {
453 PrunerBuilder::new(self.prune_config())
454 }
455
456 pub fn auth_jwt_secret(&self) -> eyre::Result<JwtSecret> {
458 let default_jwt_path = self.data_dir().jwt();
459 let secret = self.node_config().rpc.auth_jwt_secret(default_jwt_path)?;
460 Ok(secret)
461 }
462
463 pub fn dev_mining_mode<Pool>(&self, pool: Pool) -> MiningMode<Pool>
465 where
466 Pool: TransactionPool + Unpin,
467 {
468 self.node_config().dev_mining_mode(pool)
469 }
470}
471
472impl<DB, ChainSpec> LaunchContextWith<Attached<WithConfigs<ChainSpec>, DB>>
473where
474 DB: Database + Clone + 'static,
475 ChainSpec: EthChainSpec + EthereumHardforks + 'static,
476{
477 pub async fn create_provider_factory<N, Evm>(
481 &self,
482 overlay_manager: OverlayManager<N::Primitives>,
483 rocksdb_provider: Option<RocksDBProvider>,
484 disabled_stages: &[StageId],
485 ) -> eyre::Result<ProviderFactory<N>>
486 where
487 N: ProviderNodeTypes<DB = DB, ChainSpec = ChainSpec>,
488 Evm: ConfigureEvm<Primitives = N::Primitives> + 'static,
489 {
490 let static_files_config = &self.toml_config().static_files;
492 static_files_config.validate()?;
493
494 let prune_config = self.prune_config();
495
496 let mut blocks_per_file = static_files_config.as_blocks_per_file_map();
497 if blocks_per_file.get(StaticFileSegment::Receipts).is_none() &&
502 let Some(PruneMode::Distance(distance)) = prune_config.segments.receipts
503 {
504 blocks_per_file
505 .insert(StaticFileSegment::Receipts, blocks_per_file_for_prune_distance(distance));
506 }
507
508 let static_file_provider =
510 StaticFileProviderBuilder::read_write(self.data_dir().static_files())
511 .with_metrics()
512 .with_blocks_per_file_for_segments(&blocks_per_file)
513 .with_genesis_block_number(self.chain_spec().genesis().number.unwrap_or_default())
514 .build()?;
515
516 let rocksdb_provider = if let Some(provider) = rocksdb_provider {
518 provider
519 } else {
520 RocksDBProvider::builder(self.data_dir().rocksdb())
521 .with_default_tables()
522 .with_metrics()
523 .with_statistics()
524 .build()?
525 };
526
527 let bal_store = self
528 .node_config()
529 .db
530 .balstore_cache_size
531 .map(|distance| {
532 RocksDBBalStore::with_buffer_retention_distance(rocksdb_provider.clone(), distance)
533 })
534 .unwrap_or_else(|| RocksDBBalStore::new(rocksdb_provider.clone()));
535 let bal_store = BalStoreHandle::new(bal_store);
536 let factory = ProviderFactory::new(
537 self.right().clone(),
538 self.chain_spec(),
539 static_file_provider,
540 rocksdb_provider,
541 self.task_executor().clone(),
542 )?
543 .with_prune_modes(prune_config.segments)
544 .with_minimum_pruning_distance(prune_config.minimum_pruning_distance)
545 .with_overlay_manager(overlay_manager)
546 .with_bal_store(bal_store);
547
548 let (rocksdb_unwind, static_file_unwind) = factory.check_consistency()?;
552 let provider_ro = factory.database_provider_ro()?;
553 let (partial_trie_unwind, has_persisted_partial_trie_unwind) =
556 get_partial_trie_unwind_marker(&provider_ro)?;
557 drop(provider_ro);
558 let persist_partial_trie_unwind =
559 !has_persisted_partial_trie_unwind && partial_trie_unwind.is_some();
560 let partial_trie_unwind_target =
561 partial_trie_unwind.map(|marker| marker.partial_state_trie);
562 let storage_unwind = [rocksdb_unwind, static_file_unwind].into_iter().flatten().min();
567 let storage_unwind = storage_unwind.filter(|unwind_block| {
568 partial_trie_unwind_target.is_none_or(|partial_trie| *unwind_block < partial_trie)
569 });
570
571 if partial_trie_unwind_target.is_some() || storage_unwind.is_some() {
572 let build_unwind_pipeline = |walk_all_changed_branch_children| {
573 let (_tip_tx, tip_rx) = watch::channel(B256::ZERO);
574 let mut stages = DefaultStages::new(
575 factory.clone(),
576 tip_rx,
577 Arc::new(NoopConsensus::default()),
578 NoopHeaderDownloader::default(),
579 NoopBodiesDownloader::default(),
580 NoopEvmConfig::<Evm>::default(),
581 self.toml_config().stages.clone(),
582 self.prune_modes(),
583 None,
584 )
585 .builder()
586 .disable_all(disabled_stages);
587
588 if walk_all_changed_branch_children {
589 stages =
591 stages.set(MerkleStage::new_unwind(true)).enable(StageId::MerkleUnwind);
592 }
593
594 PipelineBuilder::default().add_stages(stages).build(
595 factory.clone(),
596 StaticFileProducer::new(factory.clone(), self.prune_modes()),
597 )
598 };
599 let mut unwinds = Vec::with_capacity(2);
600
601 if let Some(unwind_block) = partial_trie_unwind_target {
602 unwinds.push((
603 PipelineTarget::Unwind(unwind_block),
604 "partial state trie".to_owned(),
605 build_unwind_pipeline(true),
606 true,
607 ));
608 }
609
610 if let Some(unwind_block) = storage_unwind {
611 let inconsistency_source = match (rocksdb_unwind, static_file_unwind) {
614 (Some(_), Some(_)) => "RocksDB and static file",
615 (Some(_), None) => "RocksDB",
616 (None, Some(_)) => "static file",
617 (None, None) => unreachable!(),
618 };
619 assert_ne!(
620 unwind_block, 0,
621 "A {inconsistency_source} inconsistency was found that would trigger an unwind to block 0"
622 );
623 unwinds.push((
624 PipelineTarget::Unwind(unwind_block),
625 inconsistency_source.to_owned(),
626 build_unwind_pipeline(false),
627 false,
628 ));
629 }
630
631 if persist_partial_trie_unwind {
632 let provider_rw = factory.database_provider_rw()?;
634 write_partial_trie_unwind_marker(
635 &provider_rw,
636 partial_trie_unwind.expect("partial trie unwind marker must exist"),
637 )?;
638 provider_rw.commit()?;
639 }
640
641 let (tx, rx) = oneshot::channel();
642 let factory = factory.clone();
643
644 self.task_executor().spawn_critical_blocking_task("pipeline task", async move {
646 let result: Result<(), reth_stages::PipelineError> = async {
647 for (unwind_target, inconsistency_source, pipeline, clear_partial_trie_unwind) in
648 unwinds
649 {
650 info!(target: "reth::cli", %unwind_target, %inconsistency_source, "Executing unwind after consistency check.");
651 let (_, result) = pipeline.run_as_fut(Some(unwind_target)).await;
652 result.inspect_err(|err| {
653 error!(target: "reth::cli", %unwind_target, %inconsistency_source, %err, "failed to run unwind");
654 })?;
655
656 if clear_partial_trie_unwind {
657 let provider_rw = factory.database_provider_rw()?;
658 delete_partial_trie_unwind_marker(&provider_rw)?;
659 provider_rw.commit()?;
660 }
661 }
662 Ok(())
663 }
664 .await;
665 let _ = tx.send(result);
666 });
667 rx.await??;
668 }
669
670 Ok(factory)
671 }
672
673 pub async fn with_provider_factory<N, Evm>(
675 self,
676 overlay_manager: OverlayManager<N::Primitives>,
677 rocksdb_provider: Option<RocksDBProvider>,
678 disabled_stages: &[StageId],
679 ) -> eyre::Result<LaunchContextWith<Attached<WithConfigs<ChainSpec>, ProviderFactory<N>>>>
680 where
681 N: ProviderNodeTypes<DB = DB, ChainSpec = ChainSpec>,
682 Evm: ConfigureEvm<Primitives = N::Primitives> + 'static,
683 {
684 let factory = self
685 .create_provider_factory::<N, Evm>(overlay_manager, rocksdb_provider, disabled_stages)
686 .await?;
687 let ctx = LaunchContextWith {
688 inner: self.inner,
689 attachment: self.attachment.map_right(|_| factory),
690 };
691
692 Ok(ctx)
693 }
694}
695
696impl<T> LaunchContextWith<Attached<WithConfigs<T::ChainSpec>, ProviderFactory<T>>>
697where
698 T: ProviderNodeTypes,
699{
700 pub const fn database(&self) -> &T::DB {
702 self.right().db_ref()
703 }
704
705 pub const fn provider_factory(&self) -> &ProviderFactory<T> {
707 self.right()
708 }
709
710 pub fn static_file_provider(&self) -> StaticFileProvider<T::Primitives> {
712 self.right().static_file_provider()
713 }
714
715 pub async fn with_prometheus_server(self) -> eyre::Result<Self>
719 where
720 T::ChainSpec: EthereumHardforks,
721 {
722 self.start_prometheus_endpoint().await?;
723 Ok(self)
724 }
725
726 pub async fn start_prometheus_endpoint(&self) -> eyre::Result<()>
728 where
729 T::ChainSpec: EthereumHardforks,
730 {
731 install_prometheus_recorder().spawn_upkeep();
733
734 let listen_addr = self.node_config().metrics.prometheus;
735 if let Some(addr) = listen_addr {
736 let prune_config = self.prune_config();
737 let pruning_mode =
738 PruneConfigKind::from_config(&prune_config, self.chain_spec().as_ref()).as_str();
739 let storage_settings =
743 if self.provider_factory().get_stage_checkpoint(StageId::Headers)?.is_some() {
744 self.provider_factory().cached_storage_settings()
745 } else {
746 self.node_config().storage_settings()
747 };
748 let config = MetricServerConfig::new(
749 addr,
750 VersionInfo {
751 version: version_metadata().cargo_pkg_version.as_ref(),
752 build_timestamp: version_metadata().vergen_build_timestamp.as_ref(),
753 cargo_features: version_metadata().vergen_cargo_features.as_ref(),
754 git_sha: version_metadata().vergen_git_sha.as_ref(),
755 target_triple: version_metadata().vergen_cargo_target_triple.as_ref(),
756 build_profile: version_metadata().build_profile_name.as_ref(),
757 },
758 ChainSpecInfo { name: self.chain_id().to_string() },
759 self.task_executor().clone(),
760 metrics_hooks(self.provider_factory()),
761 self.data_dir().pprof_dumps(),
762 )
763 .with_storage_settings_info(StorageSettingsInfo {
764 storage_v2: storage_settings.storage_v2,
765 pruning_mode,
766 prune_config: serde_json::to_string(&prune_config)
767 .expect("serializing PruneConfig should not fail"),
768 })
769 .with_push_gateway(
770 self.node_config().metrics.push_gateway_url.clone(),
771 self.node_config().metrics.push_gateway_interval,
772 );
773
774 MetricServer::new(config).serve().await?;
775 }
776
777 Ok(())
778 }
779
780 pub fn with_genesis(self) -> Result<Self, InitStorageError> {
782 init_genesis_with_settings_and_validate(
783 self.provider_factory(),
784 self.node_config().storage_settings(),
785 !self.node_config().debug.skip_genesis_validation,
786 )?;
787 Ok(self)
788 }
789
790 pub fn init_genesis(&self) -> Result<B256, InitStorageError> {
792 init_genesis_with_settings(self.provider_factory(), self.node_config().storage_settings())
793 }
794
795 pub fn with_metrics_task(
801 self,
802 ) -> LaunchContextWith<Attached<WithConfigs<T::ChainSpec>, WithMeteredProvider<T>>> {
803 let (metrics_sender, metrics_receiver) = unbounded_channel();
804
805 let with_metrics =
806 WithMeteredProvider { provider_factory: self.right().clone(), metrics_sender };
807
808 debug!(target: "reth::cli", "Spawning stages metrics listener task");
809 let sync_metrics_listener = reth_stages::MetricsListener::new(metrics_receiver);
810 self.task_executor()
811 .spawn_critical_task("stages metrics listener task", sync_metrics_listener);
812
813 LaunchContextWith {
814 inner: self.inner,
815 attachment: self.attachment.map_right(|_| with_metrics),
816 }
817 }
818}
819
820impl<N, DB>
821 LaunchContextWith<
822 Attached<WithConfigs<N::ChainSpec>, WithMeteredProvider<NodeTypesWithDBAdapter<N, DB>>>,
823 >
824where
825 N: NodeTypes,
826 DB: Database + DatabaseMetrics + Clone + Unpin + 'static,
827{
828 const fn provider_factory(&self) -> &ProviderFactory<NodeTypesWithDBAdapter<N, DB>> {
830 &self.right().provider_factory
831 }
832
833 fn sync_metrics_tx(&self) -> UnboundedSender<MetricEvent> {
835 self.right().metrics_sender.clone()
836 }
837
838 #[expect(clippy::complexity)]
840 pub fn with_blockchain_db<T, F>(
841 self,
842 create_blockchain_provider: F,
843 ) -> eyre::Result<LaunchContextWith<Attached<WithConfigs<N::ChainSpec>, WithMeteredProviders<T>>>>
844 where
845 T: FullNodeTypes<Types = N, DB = DB>,
846 F: FnOnce(ProviderFactory<NodeTypesWithDBAdapter<N, DB>>) -> eyre::Result<T::Provider>,
847 {
848 let blockchain_db = create_blockchain_provider(self.provider_factory().clone())?;
849
850 let metered_providers = WithMeteredProviders {
851 db_provider_container: WithMeteredProvider {
852 provider_factory: self.provider_factory().clone(),
853 metrics_sender: self.sync_metrics_tx(),
854 },
855 blockchain_db,
856 };
857
858 let ctx = LaunchContextWith {
859 inner: self.inner,
860 attachment: self.attachment.map_right(|_| metered_providers),
861 };
862
863 Ok(ctx)
864 }
865}
866
867impl<T>
868 LaunchContextWith<
869 Attached<WithConfigs<<T::Types as NodeTypes>::ChainSpec>, WithMeteredProviders<T>>,
870 >
871where
872 T: FullNodeTypes<Types: NodeTypesForProvider>,
873{
874 pub const fn database(&self) -> &T::DB {
876 self.provider_factory().db_ref()
877 }
878
879 pub const fn provider_factory(
881 &self,
882 ) -> &ProviderFactory<NodeTypesWithDBAdapter<T::Types, T::DB>> {
883 &self.right().db_provider_container.provider_factory
884 }
885
886 pub fn lookup_head(&self) -> eyre::Result<Head> {
890 self.node_config()
891 .lookup_head(self.provider_factory())
892 .wrap_err("the head block is missing")
893 }
894
895 pub fn sync_metrics_tx(&self) -> UnboundedSender<MetricEvent> {
897 self.right().db_provider_container.metrics_sender.clone()
898 }
899
900 pub const fn blockchain_db(&self) -> &T::Provider {
902 &self.right().blockchain_db
903 }
904
905 pub async fn with_components<CB>(
907 self,
908 components_builder: CB,
909 on_component_initialized: Box<
910 dyn OnComponentInitializedHook<NodeAdapter<T, CB::Components>>,
911 >,
912 ) -> eyre::Result<
913 LaunchContextWith<
914 Attached<WithConfigs<<T::Types as NodeTypes>::ChainSpec>, WithComponents<T, CB>>,
915 >,
916 >
917 where
918 CB: NodeComponentsBuilder<T>,
919 {
920 let head = self.lookup_head()?;
922
923 let builder_ctx = BuilderContext::new(
924 head,
925 self.blockchain_db().clone(),
926 self.task_executor().clone(),
927 self.configs().clone(),
928 );
929
930 debug!(target: "reth::cli", "creating components");
931 let components = components_builder.build_components(&builder_ctx).await?;
932
933 let blockchain_db = self.blockchain_db().clone();
934
935 let node_adapter = NodeAdapter {
936 components,
937 task_executor: self.task_executor().clone(),
938 provider: blockchain_db,
939 };
940
941 debug!(target: "reth::cli", "calling on_component_initialized hook");
942 on_component_initialized.on_event(node_adapter.clone())?;
943
944 let components_container = WithComponents {
945 db_provider_container: WithMeteredProvider {
946 provider_factory: self.provider_factory().clone(),
947 metrics_sender: self.sync_metrics_tx(),
948 },
949 node_adapter,
950 head,
951 };
952
953 let ctx = LaunchContextWith {
954 inner: self.inner,
955 attachment: self.attachment.map_right(|_| components_container),
956 };
957
958 Ok(ctx)
959 }
960}
961
962impl<T, CB>
963 LaunchContextWith<
964 Attached<WithConfigs<<T::Types as NodeTypes>::ChainSpec>, WithComponents<T, CB>>,
965 >
966where
967 T: FullNodeTypes<Types: NodeTypesForProvider>,
968 CB: NodeComponentsBuilder<T>,
969{
970 pub const fn provider_factory(
972 &self,
973 ) -> &ProviderFactory<NodeTypesWithDBAdapter<T::Types, T::DB>> {
974 &self.right().db_provider_container.provider_factory
975 }
976
977 pub async fn max_block<C>(&self, client: C) -> eyre::Result<Option<BlockNumber>>
980 where
981 C: HeadersClient<Header: BlockHeader>,
982 {
983 self.node_config().max_block(client, self.provider_factory().clone()).await
984 }
985
986 pub fn static_file_provider(&self) -> StaticFileProvider<<T::Types as NodeTypes>::Primitives> {
988 self.provider_factory().static_file_provider()
989 }
990
991 pub fn static_file_producer(
993 &self,
994 ) -> StaticFileProducer<ProviderFactory<NodeTypesWithDBAdapter<T::Types, T::DB>>> {
995 StaticFileProducer::new(self.provider_factory().clone(), self.prune_modes())
996 }
997
998 pub const fn head(&self) -> Head {
1000 self.right().head
1001 }
1002
1003 pub const fn node_adapter(&self) -> &NodeAdapter<T, CB::Components> {
1005 &self.right().node_adapter
1006 }
1007
1008 pub const fn node_adapter_mut(&mut self) -> &mut NodeAdapter<T, CB::Components> {
1010 &mut self.right_mut().node_adapter
1011 }
1012
1013 pub const fn blockchain_db(&self) -> &T::Provider {
1015 &self.node_adapter().provider
1016 }
1017
1018 pub fn initial_backfill_target(
1024 &self,
1025 disabled_stages: &[StageId],
1026 ) -> ProviderResult<Option<B256>> {
1027 let mut initial_target = self.node_config().debug.tip;
1028
1029 if initial_target.is_none() {
1030 initial_target = self.check_pipeline_consistency(disabled_stages)?;
1031 }
1032
1033 Ok(initial_target)
1034 }
1035
1036 pub const fn terminate_after_initial_backfill(&self) -> bool {
1042 self.node_config().debug.terminate || self.node_config().debug.max_block.is_some()
1043 }
1044
1045 fn ensure_chain_specific_db_checks(&self) -> ProviderResult<()> {
1050 if self.chain_spec().is_optimism() &&
1051 !self.is_dev() &&
1052 self.chain_id() == Chain::optimism_mainnet()
1053 {
1054 let latest = self.blockchain_db().last_block_number()?;
1055 if latest < 105235063 {
1057 error!(
1058 "Op-mainnet has been launched without importing the pre-Bedrock state. The chain can't progress without this. See also https://reth.rs/run/sync-op-mainnet.html?minimal-bootstrap-recommended"
1059 );
1060 return Err(ProviderError::BestBlockNotFound);
1061 }
1062 }
1063
1064 Ok(())
1065 }
1066
1067 pub fn check_pipeline_consistency(
1079 &self,
1080 disabled_stages: &[StageId],
1081 ) -> ProviderResult<Option<B256>> {
1082 let era_enabled = self.era_import_source().is_some();
1084 let mut all_stages = StageId::ALL
1085 .into_iter()
1086 .filter(|id| (era_enabled || id != &StageId::Era) && !disabled_stages.contains(id));
1087
1088 let first_stage = all_stages.next().expect("there must be at least one stage");
1090
1091 let first_stage_checkpoint = self
1094 .blockchain_db()
1095 .get_stage_checkpoint(first_stage)?
1096 .unwrap_or_default()
1097 .block_number;
1098
1099 for stage_id in all_stages {
1101 let stage_checkpoint = self
1102 .blockchain_db()
1103 .get_stage_checkpoint(stage_id)?
1104 .unwrap_or_default()
1105 .block_number;
1106
1107 debug!(
1110 target: "consensus::engine",
1111 first_stage_id = %first_stage,
1112 first_stage_checkpoint,
1113 stage_id = %stage_id,
1114 stage_checkpoint = stage_checkpoint,
1115 "Checking stage against first stage",
1116 );
1117 if stage_checkpoint < first_stage_checkpoint {
1118 debug!(
1119 target: "consensus::engine",
1120 first_stage_id = %first_stage,
1121 first_stage_checkpoint,
1122 inconsistent_stage_id = %stage_id,
1123 inconsistent_stage_checkpoint = stage_checkpoint,
1124 "Pipeline sync progress is inconsistent"
1125 );
1126 return self.blockchain_db().block_hash(first_stage_checkpoint);
1127 }
1128 }
1129
1130 self.ensure_chain_specific_db_checks()?;
1131
1132 Ok(None)
1133 }
1134
1135 pub fn sync_metrics_tx(&self) -> UnboundedSender<MetricEvent> {
1137 self.right().db_provider_container.metrics_sender.clone()
1138 }
1139
1140 pub const fn components(&self) -> &CB::Components {
1142 &self.node_adapter().components
1143 }
1144
1145 #[expect(clippy::type_complexity)]
1147 pub async fn launch_exex(
1148 &self,
1149 installed_exex: Vec<(
1150 String,
1151 Box<dyn crate::exex::BoxedLaunchExEx<NodeAdapter<T, CB::Components>>>,
1152 )>,
1153 ) -> eyre::Result<Option<ExExManagerHandle<PrimitivesTy<T::Types>>>> {
1154 self.exex_launcher(installed_exex).launch().await
1155 }
1156
1157 #[expect(clippy::type_complexity)]
1169 pub fn exex_launcher(
1170 &self,
1171 installed_exex: Vec<(
1172 String,
1173 Box<dyn crate::exex::BoxedLaunchExEx<NodeAdapter<T, CB::Components>>>,
1174 )>,
1175 ) -> ExExLauncher<NodeAdapter<T, CB::Components>> {
1176 ExExLauncher::new(
1177 self.head(),
1178 self.node_adapter().clone(),
1179 installed_exex,
1180 self.configs().clone(),
1181 )
1182 }
1183
1184 pub fn era_import_source(&self) -> Option<EraImportSource> {
1188 let node_config = self.node_config();
1189 if !node_config.era.enabled {
1190 return None;
1191 }
1192
1193 EraImportSource::maybe_new(
1194 node_config.era.source.path.clone(),
1195 node_config.era.source.url.clone(),
1196 || node_config.chain.chain().kind().default_era_host(),
1197 || node_config.datadir().data_dir().join("era").into(),
1198 )
1199 }
1200
1201 pub fn consensus_layer_events(
1209 &self,
1210 ) -> impl Stream<Item = NodeEvent<PrimitivesTy<T::Types>>> + 'static
1211 where
1212 T::Provider: reth_provider::CanonChainTracker,
1213 {
1214 if self.node_config().debug.tip.is_none() && !self.is_dev() {
1215 Either::Left(
1216 ConsensusLayerHealthEvents::new(Box::new(self.blockchain_db().clone()))
1217 .map(Into::into),
1218 )
1219 } else {
1220 Either::Right(stream::empty())
1221 }
1222 }
1223
1224 pub async fn spawn_ethstats<St>(&self, mut engine_events: St) -> eyre::Result<()>
1226 where
1227 St: Stream<Item = reth_engine_primitives::ConsensusEngineEvent<PrimitivesTy<T::Types>>>
1228 + Send
1229 + Unpin
1230 + 'static,
1231 {
1232 let Some(url) = self.node_config().debug.ethstats.as_ref() else { return Ok(()) };
1233
1234 let network = self.components().network().clone();
1235 let pool = self.components().pool().clone();
1236 let provider = self.node_adapter().provider.clone();
1237
1238 info!(target: "reth::cli", "Starting EthStats service at {}", url);
1239
1240 let ethstats = EthStatsService::new(url, network, provider, pool).await?;
1241
1242 let ethstats_for_events = ethstats.clone();
1244 let task_executor = self.task_executor().clone();
1245 task_executor.spawn_task(async move {
1246 while let Some(event) = engine_events.next().await {
1247 use reth_engine_primitives::ConsensusEngineEvent;
1248 match event {
1249 ConsensusEngineEvent::ForkBlockAdded(executed, duration) |
1250 ConsensusEngineEvent::CanonicalBlockAdded(executed, duration) => {
1251 let block_hash = executed.recovered_block.num_hash().hash;
1252 let block_number = executed.recovered_block.num_hash().number;
1253 if let Err(e) = ethstats_for_events
1254 .report_new_payload(block_hash, block_number, duration)
1255 .await
1256 {
1257 debug!(
1258 target: "ethstats",
1259 "Failed to report new payload: {}", e
1260 );
1261 }
1262 }
1263 _ => {
1264 }
1266 }
1267 }
1268 });
1269
1270 task_executor.spawn_task(async move { ethstats.run().await });
1272
1273 Ok(())
1274 }
1275}
1276
1277#[derive(Clone, Copy, Debug)]
1283pub struct Attached<L, R> {
1284 left: L,
1285 right: R,
1286}
1287
1288impl<L, R> Attached<L, R> {
1289 pub const fn new(left: L, right: R) -> Self {
1291 Self { left, right }
1292 }
1293
1294 pub fn map_left<F, T>(self, f: F) -> Attached<T, R>
1296 where
1297 F: FnOnce(L) -> T,
1298 {
1299 Attached::new(f(self.left), self.right)
1300 }
1301
1302 pub fn map_right<F, T>(self, f: F) -> Attached<L, T>
1304 where
1305 F: FnOnce(R) -> T,
1306 {
1307 Attached::new(self.left, f(self.right))
1308 }
1309
1310 pub const fn left(&self) -> &L {
1312 &self.left
1313 }
1314
1315 pub const fn right(&self) -> &R {
1317 &self.right
1318 }
1319
1320 pub const fn left_mut(&mut self) -> &mut L {
1322 &mut self.left
1323 }
1324
1325 pub const fn right_mut(&mut self) -> &mut R {
1327 &mut self.right
1328 }
1329}
1330
1331#[derive(Debug)]
1334pub struct WithConfigs<ChainSpec> {
1335 pub config: NodeConfig<ChainSpec>,
1337 pub toml_config: reth_config::Config,
1339}
1340
1341impl<ChainSpec> Clone for WithConfigs<ChainSpec> {
1342 fn clone(&self) -> Self {
1343 Self { config: self.config.clone(), toml_config: self.toml_config.clone() }
1344 }
1345}
1346
1347#[derive(Debug, Clone)]
1350pub struct WithMeteredProvider<N: NodeTypesWithDB> {
1351 provider_factory: ProviderFactory<N>,
1352 metrics_sender: UnboundedSender<MetricEvent>,
1353}
1354
1355#[expect(missing_debug_implementations)]
1358pub struct WithMeteredProviders<T>
1359where
1360 T: FullNodeTypes,
1361{
1362 db_provider_container: WithMeteredProvider<NodeTypesWithDBAdapter<T::Types, T::DB>>,
1363 blockchain_db: T::Provider,
1364}
1365
1366#[expect(missing_debug_implementations)]
1368pub struct WithComponents<T, CB>
1369where
1370 T: FullNodeTypes,
1371 CB: NodeComponentsBuilder<T>,
1372{
1373 db_provider_container: WithMeteredProvider<NodeTypesWithDBAdapter<T::Types, T::DB>>,
1374 node_adapter: NodeAdapter<T, CB::Components>,
1375 head: Head,
1376}
1377
1378pub fn metrics_hooks<N: NodeTypesWithDB>(provider_factory: &ProviderFactory<N>) -> Hooks {
1380 Hooks::builder()
1381 .with_hook({
1382 let db = provider_factory.db_ref().clone();
1383 move || throttle!(Duration::from_secs(5 * 60), || db.report_metrics())
1384 })
1385 .with_hook({
1386 let sfp = provider_factory.static_file_provider();
1387 move || {
1388 throttle!(Duration::from_secs(5 * 60), || {
1389 if let Err(error) = sfp.report_metrics() {
1390 error!(%error, "Failed to report metrics from static file provider");
1391 }
1392 })
1393 }
1394 })
1395 .with_hook({
1396 let rocksdb = provider_factory.rocksdb_provider();
1397 move || throttle!(Duration::from_secs(5 * 60), || rocksdb.report_metrics())
1398 })
1399 .build()
1400}
1401
1402fn get_partial_trie_unwind_marker(
1403 provider: &(impl MetadataProvider + StageCheckpointReader),
1404) -> ProviderResult<(Option<PartialStateTrieUnwindMarker>, bool)> {
1405 if let Some(marker) = provider.get_metadata(PARTIAL_STATE_TRIE_UNWIND_METADATA_KEY)? {
1406 let marker = serde_json::from_slice::<PartialStateTrieUnwindMarker>(&marker)
1407 .map_err(ProviderError::other)?;
1408 if marker.partial_state_trie >= marker.finish_block_number {
1409 return Err(ProviderError::other(std::io::Error::other(format!(
1410 "partial state trie unwind target #{} is not below original Finish #{}",
1411 marker.partial_state_trie, marker.finish_block_number,
1412 ))))
1413 }
1414 return Ok((Some(marker), true))
1415 }
1416
1417 let Some(finish_checkpoint) = provider.get_stage_checkpoint(StageId::Finish)? else {
1418 return Ok((None, false))
1419 };
1420 let Some(partial_state_trie) =
1421 finish_checkpoint.finish_stage_checkpoint().and_then(|finish| finish.partial_state_trie())
1422 else {
1423 return Ok((None, false))
1424 };
1425
1426 if partial_state_trie > finish_checkpoint.block_number {
1427 return Err(ProviderError::other(std::io::Error::other(format!(
1428 "partial state trie frontier #{partial_state_trie} is ahead of Finish #{}",
1429 finish_checkpoint.block_number,
1430 ))))
1431 }
1432
1433 Ok((
1434 (partial_state_trie < finish_checkpoint.block_number).then_some(
1435 PartialStateTrieUnwindMarker {
1436 finish_block_number: finish_checkpoint.block_number,
1437 partial_state_trie,
1438 },
1439 ),
1440 false,
1441 ))
1442}
1443
1444const PARTIAL_STATE_TRIE_UNWIND_METADATA_KEY: &str = "partial_state_trie_unwind";
1446
1447fn write_partial_trie_unwind_marker(
1448 provider: &impl MetadataWriter,
1449 marker: PartialStateTrieUnwindMarker,
1450) -> ProviderResult<()> {
1451 provider.write_metadata(
1452 PARTIAL_STATE_TRIE_UNWIND_METADATA_KEY,
1453 serde_json::to_vec(&marker).map_err(ProviderError::other)?,
1454 )
1455}
1456
1457fn delete_partial_trie_unwind_marker(provider: &impl MetadataWriter) -> ProviderResult<()> {
1458 provider.delete_metadata(PARTIAL_STATE_TRIE_UNWIND_METADATA_KEY)
1459}
1460
1461#[cfg(test)]
1462mod tests {
1463 use super::{get_partial_trie_unwind_marker, LaunchContext, NodeConfig};
1464 use reth_config::Config;
1465 use reth_db_api::models::PartialStateTrieUnwindMarker;
1466 use reth_node_core::args::PruningArgs;
1467 use reth_provider::{MetadataProvider, ProviderResult, StageCheckpointReader};
1468 use reth_stages::{FinishCheckpoint, StageCheckpoint, StageId};
1469
1470 const EXTENSION: &str = "toml";
1471
1472 struct MockProvider(Option<Vec<u8>>, Option<StageCheckpoint>);
1473
1474 impl MetadataProvider for MockProvider {
1475 fn get_metadata(&self, _: &str) -> ProviderResult<Option<Vec<u8>>> {
1476 Ok(self.0.clone())
1477 }
1478 }
1479
1480 impl StageCheckpointReader for MockProvider {
1481 fn get_stage_checkpoint(&self, id: StageId) -> ProviderResult<Option<StageCheckpoint>> {
1482 assert_eq!(id, StageId::Finish);
1483 Ok(self.1)
1484 }
1485
1486 fn get_stage_checkpoint_progress(&self, _: StageId) -> ProviderResult<Option<Vec<u8>>> {
1487 Ok(None)
1488 }
1489
1490 fn get_all_checkpoints(&self) -> ProviderResult<Vec<(String, StageCheckpoint)>> {
1491 Ok(Vec::new())
1492 }
1493 }
1494
1495 fn with_tempdir(filename: &str, proc: fn(&std::path::Path)) {
1496 let temp_dir = tempfile::tempdir().unwrap();
1497 let config_path = temp_dir.path().join(filename).with_extension(EXTENSION);
1498 proc(&config_path);
1499 temp_dir.close().unwrap()
1500 }
1501
1502 #[test]
1503 fn test_save_prune_config() {
1504 with_tempdir("prune-store-test", |config_path| {
1505 let mut reth_config = Config::default();
1506 let node_config = NodeConfig {
1507 pruning: PruningArgs {
1508 full: true,
1509 minimal: false,
1510 block_interval: None,
1511 sender_recovery_full: false,
1512 sender_recovery_distance: None,
1513 sender_recovery_before: None,
1514 transaction_lookup_full: false,
1515 transaction_lookup_distance: None,
1516 transaction_lookup_before: None,
1517 receipts_full: false,
1518 receipts_pre_merge: false,
1519 receipts_distance: None,
1520 receipts_before: None,
1521 account_history_full: false,
1522 account_history_distance: None,
1523 account_history_before: None,
1524 storage_history_full: false,
1525 storage_history_distance: None,
1526 storage_history_before: None,
1527 bodies_pre_merge: false,
1528 bodies_distance: None,
1529 receipts_log_filter: None,
1530 bodies_before: None,
1531 minimum_distance: None,
1532 },
1533 ..NodeConfig::test()
1534 };
1535 LaunchContext::save_pruning_config(&mut reth_config, &node_config, config_path)
1536 .unwrap();
1537
1538 let loaded_config = Config::from_path(config_path).unwrap();
1539
1540 assert_eq!(reth_config, loaded_config);
1541 })
1542 }
1543
1544 #[test]
1545 fn get_partial_trie_unwind_marker_uses_partial_finish_checkpoint() {
1546 let finish_checkpoint = StageCheckpoint::new(42)
1547 .with_finish_stage_checkpoint(FinishCheckpoint { partial_state_trie: Some(21) });
1548 let expected =
1549 finish_checkpoint.finish_stage_checkpoint().unwrap().partial_state_trie().map(
1550 |partial_state_trie| PartialStateTrieUnwindMarker {
1551 finish_block_number: finish_checkpoint.block_number,
1552 partial_state_trie,
1553 },
1554 );
1555
1556 assert_eq!(
1557 get_partial_trie_unwind_marker(&MockProvider(None, Some(finish_checkpoint))).unwrap(),
1558 (expected, false)
1559 );
1560
1561 let genesis_checkpoint = StageCheckpoint::new(42)
1562 .with_finish_stage_checkpoint(FinishCheckpoint { partial_state_trie: Some(0) });
1563 let expected =
1564 genesis_checkpoint.finish_stage_checkpoint().unwrap().partial_state_trie().map(
1565 |partial_state_trie| PartialStateTrieUnwindMarker {
1566 finish_block_number: genesis_checkpoint.block_number,
1567 partial_state_trie,
1568 },
1569 );
1570
1571 assert_eq!(
1572 get_partial_trie_unwind_marker(&MockProvider(None, Some(genesis_checkpoint))).unwrap(),
1573 (expected, false)
1574 );
1575 }
1576
1577 #[test]
1578 fn get_partial_trie_unwind_marker_resumes_persisted_unwind() {
1579 let marker =
1580 PartialStateTrieUnwindMarker { finish_block_number: 42, partial_state_trie: 21 };
1581
1582 assert_eq!(
1583 get_partial_trie_unwind_marker(&MockProvider(
1584 Some(serde_json::to_vec(&marker).unwrap()),
1585 Some(StageCheckpoint::new(21)),
1586 ),)
1587 .unwrap(),
1588 (Some(marker), true)
1589 );
1590 assert_eq!(
1591 get_partial_trie_unwind_marker(&MockProvider(
1592 Some(serde_json::to_vec(&marker).unwrap()),
1593 None
1594 ),)
1595 .unwrap(),
1596 (Some(marker), true)
1597 );
1598 }
1599
1600 #[test]
1601 fn get_partial_trie_unwind_marker_ignores_non_lagging_or_missing_partial_checkpoint() {
1602 let matching_finish_checkpoint = StageCheckpoint::new(42)
1603 .with_finish_stage_checkpoint(FinishCheckpoint { partial_state_trie: Some(42) });
1604 let ahead_finish_checkpoint = StageCheckpoint::new(42)
1605 .with_finish_stage_checkpoint(FinishCheckpoint { partial_state_trie: Some(43) });
1606 let missing_partial_finish_checkpoint = StageCheckpoint::new(42)
1607 .with_finish_stage_checkpoint(FinishCheckpoint { partial_state_trie: None });
1608
1609 assert_eq!(
1610 get_partial_trie_unwind_marker(&MockProvider(None, Some(matching_finish_checkpoint)),)
1611 .unwrap(),
1612 (None, false)
1613 );
1614 assert_eq!(
1615 get_partial_trie_unwind_marker(&MockProvider(
1616 None,
1617 Some(missing_partial_finish_checkpoint)
1618 ),)
1619 .unwrap(),
1620 (None, false)
1621 );
1622 assert_eq!(
1623 get_partial_trie_unwind_marker(&MockProvider(None, None)).unwrap(),
1624 (None, false)
1625 );
1626
1627 let partial_frontier = ahead_finish_checkpoint
1628 .finish_stage_checkpoint()
1629 .and_then(|finish| finish.partial_state_trie());
1630 let result =
1631 get_partial_trie_unwind_marker(&MockProvider(None, Some(ahead_finish_checkpoint)));
1632 if partial_frontier.is_some() {
1633 let error = result.unwrap_err();
1634 assert!(error.to_string().contains("ahead of Finish"), "unexpected error: {error}");
1635 } else {
1636 assert_eq!(result.unwrap(), (None, false));
1637 }
1638 }
1639
1640 #[test]
1641 fn get_partial_trie_unwind_marker_rejects_invalid_persisted_marker() {
1642 let marker =
1643 PartialStateTrieUnwindMarker { finish_block_number: 42, partial_state_trie: 42 };
1644 let error = get_partial_trie_unwind_marker(&MockProvider(
1645 Some(serde_json::to_vec(&marker).unwrap()),
1646 None,
1647 ))
1648 .unwrap_err();
1649
1650 assert!(error.to_string().contains("is not below original Finish"));
1651 }
1652
1653 #[test]
1654 fn get_partial_trie_unwind_marker_rejects_malformed_metadata() {
1655 assert!(get_partial_trie_unwind_marker(&MockProvider(Some(vec![0xff]), None)).is_err());
1656 }
1657}