Skip to main content

reth_node_ethereum/snap/
selection.rs

1//! Picks the engine's backfill at launch: the staged pipeline, or snap sync with `--snap.v2`.
2
3use super::{SnapBackfillSync, SnapHandoff};
4use reth_engine_tree::backfill::{BackfillAction, BackfillEvent, BackfillSync, PipelineSync};
5use reth_network_p2p::{headers::client::HeadersClient, snap::client::SnapClient};
6use reth_node_builder::{
7    sync::{BackfillContext, BackfillSyncBuilder, PipelineBackfill},
8    NodeConfig,
9};
10use reth_provider::{providers::ProviderNodeTypes, DatabaseProviderFactory, ProviderFactory};
11use std::task::{Context, Poll};
12
13/// Builds the Ethereum node's backfill: the staged pipeline, or snap sync with `--snap.v2`.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum EthereumBackfill {
16    /// The staged pipeline, which every node runs unless snap sync is opted into.
17    Pipeline,
18    /// Snap sync, on chains with block access lists.
19    Snap,
20}
21
22impl EthereumBackfill {
23    /// Picks the backfill for `config`'s `--snap.v2` setting.
24    pub const fn new<ChainSpec>(config: &NodeConfig<ChainSpec>) -> Self {
25        if config.network.snap_v2 {
26            Self::Snap
27        } else {
28            Self::Pipeline
29        }
30    }
31}
32
33impl<N, C> BackfillSyncBuilder<N, C> for EthereumBackfill
34where
35    N: ProviderNodeTypes,
36    C: SnapClient + HeadersClient + Clone + Unpin + 'static,
37{
38    type Backfill = EthereumBackfillSync<N, C>;
39
40    fn build(self, ctx: BackfillContext<N, C>) -> eyre::Result<Self::Backfill> {
41        // Genesis has stored the storage settings by now, so the layout check covers a fresh
42        // database too.
43        ctx.provider_factory().database_provider_ro()?.ensure_sync_mode(self == Self::Snap)?;
44        match self {
45            Self::Pipeline => PipelineBackfill.build(ctx).map(EthereumBackfillSync::Pipeline),
46            Self::Snap => {
47                let client = ctx.client().clone();
48                let provider_factory = ctx.provider_factory().clone();
49                let runtime = ctx.runtime().clone();
50                Ok(EthereumBackfillSync::Snap(Box::new(SnapBackfillSync::new(
51                    ctx.into_pipeline(),
52                    client,
53                    provider_factory,
54                    runtime,
55                ))))
56            }
57        }
58    }
59
60    fn recover(&mut self, provider_factory: &ProviderFactory<N>) -> eyre::Result<()> {
61        SnapHandoff::new(provider_factory.clone()).resume_interrupted_publish()?;
62        // The snap layout is checked in `build`, after genesis initializes it.
63        if *self == Self::Pipeline {
64            provider_factory.database_provider_ro()?.ensure_sync_mode(false)?;
65        }
66        Ok(())
67    }
68}
69
70/// The Ethereum node's backfill: the staged pipeline, or snap sync.
71#[derive(Debug)]
72pub enum EthereumBackfillSync<N: ProviderNodeTypes, C> {
73    /// The staged pipeline, which every node runs unless snap sync is opted into.
74    Pipeline(PipelineSync<N>),
75    /// Snap sync, which downloads state at a pivot before the pipeline continues above it.
76    Snap(Box<SnapBackfillSync<N, C>>),
77}
78
79impl<N, C> BackfillSync for EthereumBackfillSync<N, C>
80where
81    N: ProviderNodeTypes,
82    C: SnapClient + HeadersClient + Clone + Unpin + 'static,
83{
84    fn on_action(&mut self, action: BackfillAction) {
85        match self {
86            Self::Pipeline(sync) => sync.on_action(action),
87            Self::Snap(sync) => sync.on_action(action),
88        }
89    }
90
91    fn poll(&mut self, cx: &mut Context<'_>) -> Poll<BackfillEvent> {
92        match self {
93            Self::Pipeline(sync) => sync.poll(cx),
94            Self::Snap(sync) => sync.poll(cx),
95        }
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use crate::snap::tests::pipeline_on;
103    use alloy_primitives::B256;
104    use reth_chainspec::{ChainSpec, ChainSpecBuilder};
105    use reth_network_p2p::NoopFullBlockClient;
106    use reth_node_core::args::NetworkArgs;
107    use reth_primitives_traits::{Header, SealedHeader};
108    use reth_provider::{
109        test_utils::{
110            create_test_provider_factory_with_chain_spec, insert_headers, MockNodeTypesWithDB,
111        },
112        ChainSpecProvider, DBProvider, MetadataWriter, ProviderError, StorageSettings,
113        StorageSettingsCache,
114    };
115    use reth_snap_sync::{SnapAttemptStore, SnapGeneration};
116    use reth_stages::StageSetBuilder;
117    use reth_tasks::Runtime;
118    use std::sync::Arc;
119
120    type TestFactory = ProviderFactory<MockNodeTypesWithDB>;
121
122    fn factory(chain_spec: ChainSpec) -> TestFactory {
123        create_test_provider_factory_with_chain_spec(Arc::new(chain_spec))
124    }
125
126    fn amsterdam() -> ChainSpec {
127        ChainSpecBuilder::mainnet().amsterdam_activated().build()
128    }
129
130    // The `--snap.v2` setting on `factory`'s chain.
131    fn backfill(snap_v2: bool, factory: &TestFactory) -> EthereumBackfill {
132        let config = NodeConfig::new(factory.chain_spec())
133            .with_network(NetworkArgs { snap_v2, ..Default::default() });
134        EthereumBackfill::new(&config)
135    }
136
137    // The backfill `EthereumBackfill` builds over `factory`, or why it refused to.
138    fn build(
139        snap_v2: bool,
140        factory: TestFactory,
141    ) -> eyre::Result<EthereumBackfillSync<MockNodeTypesWithDB, NoopFullBlockClient>> {
142        let pipeline = pipeline_on(
143            &factory,
144            StageSetBuilder::default(),
145            tokio::sync::watch::channel(B256::ZERO).0,
146        );
147        backfill(snap_v2, &factory).build(BackfillContext::new(
148            pipeline,
149            NoopFullBlockClient::default(),
150            factory,
151            Runtime::test(),
152        ))
153    }
154
155    // Runs `EthereumBackfill`'s startup recovery over `factory`.
156    fn recover(snap_v2: bool, factory: &TestFactory) -> eyre::Result<()> {
157        BackfillSyncBuilder::<MockNodeTypesWithDB, NoopFullBlockClient>::recover(
158            &mut backfill(snap_v2, factory),
159            factory,
160        )
161    }
162
163    #[test]
164    fn the_staged_pipeline_runs_without_the_flag() {
165        assert!(matches!(
166            build(false, factory(amsterdam())).unwrap(),
167            EthereumBackfillSync::Pipeline(_)
168        ));
169    }
170
171    #[test]
172    fn the_flag_selects_snap_sync() {
173        let factory = factory(amsterdam());
174        recover(true, &factory).unwrap();
175
176        // Genesis supplies the layout before the backfill is built.
177        factory.set_storage_settings_cache(StorageSettings::v2());
178        assert!(matches!(build(true, factory).unwrap(), EthereumBackfillSync::Snap(_)));
179    }
180
181    #[test]
182    fn building_refuses_snap_into_a_legacy_layout() {
183        let factory = factory(amsterdam());
184        // Genesis caches the stored layout before the backfill is built.
185        factory.set_storage_settings_cache(StorageSettings::v1());
186
187        // Recovery runs before genesis stores the settings, so only the build can refuse it.
188        let error = build(true, factory).unwrap_err();
189        assert!(matches!(
190            error.downcast_ref::<ProviderError>(),
191            Some(ProviderError::SnapStorageLayoutUnsupported)
192        ));
193    }
194
195    #[test]
196    fn recovery_refuses_unverified_snap_state_without_the_flag() {
197        let factory = factory(amsterdam());
198        factory.set_storage_settings_cache(StorageSettings::v2());
199        let provider = factory.database_provider_rw().unwrap();
200        provider.write_storage_settings(StorageSettings::v2()).unwrap();
201        provider.commit().unwrap();
202        let genesis = SealedHeader::seal_slow(Header::default());
203        insert_headers(&factory, std::slice::from_ref(&genesis));
204        let provider = factory.database_provider_rw().unwrap();
205        provider
206            .start_snap_attempt(SnapGeneration::new(genesis.num_hash(), genesis.state_root))
207            .unwrap();
208        provider.commit().unwrap();
209
210        let error = recover(false, &factory).unwrap_err();
211        assert!(matches!(
212            error.downcast_ref::<ProviderError>(),
213            Some(ProviderError::SnapStateRequiresSnapSync { .. })
214        ));
215        recover(true, &factory).unwrap();
216    }
217}