Skip to main content

reth_node_ethereum/snap/
context.rs

1//! Reports the node's headers, peers and forkchoice movement to a snap bootstrap run.
2
3use alloy_primitives::B256;
4use reth_network_p2p::download::DownloadClient;
5use reth_provider::{
6    providers::ProviderNodeTypes, BlockNumReader, DatabaseProviderFactory, ProviderFactory,
7};
8use reth_snap_sync::{SnapSyncContext, SnapSyncError};
9use reth_tracing::tracing::debug;
10use std::{pin::pin, time::Duration};
11use tokio::{sync::watch, time::Instant};
12
13// Sampling well below the block time resumes a stalled step soon after new peers connect.
14const DEFAULT_SAMPLE_INTERVAL: Duration = Duration::from_secs(2);
15
16// A peer that starts serving the pivot state, or one replacing another, leaves the peer count
17// where it was, so a step stalled on such a peer only resumes by retrying.
18const DEFAULT_RETRY_INTERVAL: Duration = Duration::from_secs(30);
19
20// `SnapSyncContext` for one bootstrap run inside the snap backfill. Headers only advance between
21// runs, so a new forkchoice target ends a wait and lets the backfill refresh them.
22#[derive(Debug)]
23pub(crate) struct NodeSnapContext<N: ProviderNodeTypes, C> {
24    factory: ProviderFactory<N>,
25    // Peer counts come from the client serving the run's requests.
26    client: C,
27    // Forkchoice targets forwarded by the engine.
28    targets: watch::Receiver<B256>,
29    // Finalized blocks forwarded by the engine, the zero hash until one arrives.
30    finalized: watch::Receiver<B256>,
31    // How often to check for new peers.
32    interval: Duration,
33    // How long to wait before retrying with the same peers.
34    retry: Duration,
35}
36
37impl<N: ProviderNodeTypes, C> NodeSnapContext<N, C> {
38    pub(crate) const fn new(
39        factory: ProviderFactory<N>,
40        client: C,
41        targets: watch::Receiver<B256>,
42        finalized: watch::Receiver<B256>,
43    ) -> Self {
44        Self {
45            factory,
46            client,
47            targets,
48            finalized,
49            interval: DEFAULT_SAMPLE_INTERVAL,
50            retry: DEFAULT_RETRY_INTERVAL,
51        }
52    }
53
54    #[cfg(test)]
55    const fn with_intervals(mut self, interval: Duration, retry: Duration) -> Self {
56        self.interval = interval;
57        self.retry = retry;
58        self
59    }
60}
61
62impl<N, C> SnapSyncContext for NodeSnapContext<N, C>
63where
64    N: ProviderNodeTypes,
65    C: DownloadClient,
66{
67    fn head(&self) -> Result<u64, SnapSyncError> {
68        Ok(self.factory.database_provider_ro()?.last_block_number()?)
69    }
70
71    fn finalized(&self) -> Result<Option<u64>, SnapSyncError> {
72        let hash = *self.finalized.borrow();
73        if hash.is_zero() {
74            return Ok(None)
75        }
76        // Only a finalized block whose header is synced can anchor the pivot.
77        Ok(self.factory.provider()?.block_number(hash)?)
78    }
79
80    async fn wait_for_progress(&mut self, _head: u64) -> bool {
81        // Peers connected before the wait already failed to serve the stalled step.
82        let peers = self.client.num_connected_peers();
83        let mut retry = pin!(tokio::time::sleep(self.retry));
84        let mut sample = tokio::time::interval_at(Instant::now() + self.interval, self.interval);
85        loop {
86            tokio::select! {
87                // New headers need the pipeline, which only runs once this run stops. A closed
88                // channel means the backfill is gone.
89                _ = self.targets.changed() => return false,
90                // Peers the count cannot distinguish are retried instead of waited out.
91                () = &mut retry => {
92                    debug!(target: "sync::snap", peers, "Retrying the stalled snap step");
93                    return true
94                }
95                _ = sample.tick() => {}
96            }
97            if self.client.num_connected_peers() > peers {
98                return true
99            }
100        }
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use reth_db::{tables, transaction::DbTxMut};
108    use reth_network_peers::PeerId;
109    use reth_provider::{
110        test_utils::{create_test_provider_factory, insert_headers, MockNodeTypesWithDB},
111        DBProvider,
112    };
113    use reth_testing_utils::generators::{self, random_header_range};
114    use std::sync::atomic::{AtomicUsize, Ordering};
115
116    const TEST_INTERVAL: Duration = Duration::from_millis(5);
117    const TEST_RETRY: Duration = Duration::from_millis(500);
118
119    fn context(
120        peers: &TestPeers,
121    ) -> (watch::Sender<B256>, NodeSnapContext<MockNodeTypesWithDB, &TestPeers>) {
122        let (targets, receiver) = watch::channel(B256::repeat_byte(1));
123        let finalized = watch::channel(B256::ZERO).1;
124        let context =
125            NodeSnapContext::new(create_test_provider_factory(), peers, receiver, finalized)
126                .with_intervals(TEST_INTERVAL, TEST_RETRY);
127        (targets, context)
128    }
129
130    // A peer count the test moves between samples.
131    #[derive(Debug, Default)]
132    struct TestPeers(AtomicUsize);
133
134    impl DownloadClient for TestPeers {
135        fn report_bad_message(&self, _peer_id: PeerId) {}
136
137        fn num_connected_peers(&self) -> usize {
138            self.0.load(Ordering::Relaxed)
139        }
140    }
141
142    #[test]
143    fn the_finalized_block_resolves_once_its_header_is_synced() {
144        let factory = create_test_provider_factory();
145        let headers = random_header_range(&mut generators::rng(), 0..4, B256::ZERO);
146        insert_headers(&factory, &headers);
147        // The header stage indexes hashes alongside the headers.
148        let provider = factory.database_provider_rw().unwrap();
149        for header in &headers {
150            provider.tx_ref().put::<tables::HeaderNumbers>(header.hash(), header.number).unwrap();
151        }
152        provider.commit().unwrap();
153        let peers = TestPeers::default();
154        let (finalized, receiver) = watch::channel(B256::ZERO);
155        let context = NodeSnapContext::new(factory, &peers, watch::channel(B256::ZERO).1, receiver);
156
157        // Nothing is known as finalized until the engine reports a block.
158        assert_eq!(context.finalized().unwrap(), None);
159
160        finalized.send(headers[2].hash()).unwrap();
161        assert_eq!(context.finalized().unwrap(), Some(2));
162
163        // A finalized block whose header isn't synced yet can't anchor a pivot.
164        finalized.send(B256::repeat_byte(0xff)).unwrap();
165        assert_eq!(context.finalized().unwrap(), None);
166    }
167
168    #[tokio::test]
169    async fn a_new_target_ends_the_wait_for_headers() {
170        let peers = TestPeers::default();
171        let (targets, mut context) = context(&peers);
172
173        targets.send(B256::repeat_byte(2)).unwrap();
174
175        assert!(!context.wait_for_progress(0).await);
176    }
177
178    #[tokio::test]
179    async fn a_dropped_backfill_ends_the_wait() {
180        let peers = TestPeers::default();
181        let (targets, mut context) = context(&peers);
182
183        drop(targets);
184
185        assert!(!context.wait_for_progress(0).await);
186    }
187
188    #[tokio::test]
189    async fn new_peers_end_the_wait() {
190        let peers = TestPeers::default();
191        let (_targets, mut context) = context(&peers);
192
193        // Peers count as new only once the wait has sampled its baseline.
194        let (progressed, ()) = tokio::join!(context.wait_for_progress(0), async {
195            tokio::time::sleep(TEST_INTERVAL).await;
196            peers.0.fetch_add(1, Ordering::Relaxed);
197        });
198
199        assert!(progressed);
200    }
201
202    #[tokio::test]
203    async fn an_unchanged_peer_count_retries_once_the_wait_is_out() {
204        // A peer that starts serving the state mid-wait keeps the count where it was.
205        let peers = TestPeers(AtomicUsize::new(1));
206        let (_targets, mut context) = context(&peers);
207
208        assert!(context.wait_for_progress(0).await);
209    }
210}