Skip to main content

reth_exex/backfill/
stream.rs

1use super::job::BackfillJobResult;
2use crate::{BackfillJob, SingleBlockBackfillJob};
3use alloy_primitives::BlockNumber;
4use futures::{
5    stream::{FuturesOrdered, Stream},
6    StreamExt,
7};
8use reth_evm::{
9    execute::{BlockExecutionError, BlockExecutionOutput},
10    ConfigureEvm,
11};
12use reth_node_api::NodePrimitives;
13use reth_primitives_traits::RecoveredBlock;
14use reth_provider::{BlockReader, Chain, StateProviderFactory};
15use reth_prune_types::PruneModes;
16use reth_stages_api::ExecutionStageThresholds;
17use std::{
18    ops::RangeInclusive,
19    pin::Pin,
20    task::{ready, Context, Poll},
21};
22use tokio::task::JoinHandle;
23use tracing::debug;
24
25/// The default parallelism for active tasks in [`StreamBackfillJob`].
26pub(crate) const DEFAULT_PARALLELISM: usize = 4;
27/// The default batch size for active tasks in [`StreamBackfillJob`].
28const DEFAULT_BATCH_SIZE: usize = 100;
29
30/// Boxed thread-safe iterator that yields [`BackfillJobResult`]s.
31type BackfillTaskIterator<T> =
32    Box<dyn Iterator<Item = BackfillJobResult<T>> + Send + Sync + 'static>;
33
34/// Backfill task output.
35struct BackfillTaskOutput<T> {
36    job: BackfillTaskIterator<T>,
37    result: Option<BackfillJobResult<T>>,
38}
39
40/// Ordered queue of [`JoinHandle`]s that yield [`BackfillTaskOutput`]s.
41type BackfillTasks<T> = FuturesOrdered<JoinHandle<BackfillTaskOutput<T>>>;
42
43pub(super) type SingleBlockStreamItem<N> = (
44    RecoveredBlock<<N as NodePrimitives>::Block>,
45    BlockExecutionOutput<<N as NodePrimitives>::Receipt>,
46);
47type BatchBlockStreamItem<N> = Chain<N>;
48
49/// Stream for processing backfill jobs asynchronously.
50///
51/// This struct manages the execution of [`SingleBlockBackfillJob`] tasks, allowing blocks to be
52/// processed asynchronously but in order within a specified range.
53#[derive(Debug)]
54pub struct StreamBackfillJob<E, P, T> {
55    evm_config: E,
56    provider: P,
57    prune_modes: PruneModes,
58    range: RangeInclusive<BlockNumber>,
59    tasks: BackfillTasks<T>,
60    parallelism: usize,
61    batch_size: usize,
62    thresholds: ExecutionStageThresholds,
63}
64
65impl<E, P, T> StreamBackfillJob<E, P, T>
66where
67    T: Send + Sync + 'static,
68{
69    /// Configures the parallelism of the [`StreamBackfillJob`] to handle active tasks.
70    pub const fn with_parallelism(mut self, parallelism: usize) -> Self {
71        self.parallelism = parallelism;
72        self
73    }
74
75    /// Configures the batch size for the [`StreamBackfillJob`].
76    pub const fn with_batch_size(mut self, batch_size: usize) -> Self {
77        self.batch_size = batch_size;
78        self
79    }
80
81    /// Spawns a new task calling the [`BackfillTaskIterator::next`] method and pushes it to the end
82    /// of the [`BackfillTasks`] queue.
83    fn push_back(&mut self, mut job: BackfillTaskIterator<T>) {
84        self.tasks.push_back(tokio::task::spawn_blocking(move || BackfillTaskOutput {
85            result: job.next(),
86            job,
87        }));
88    }
89
90    /// Spawns a new task calling the [`BackfillTaskIterator::next`] method and pushes it to the
91    /// front of the  [`BackfillTasks`] queue.
92    fn push_front(&mut self, mut job: BackfillTaskIterator<T>) {
93        self.tasks.push_front(tokio::task::spawn_blocking(move || BackfillTaskOutput {
94            result: job.next(),
95            job,
96        }));
97    }
98
99    /// Polls the next task in the [`BackfillTasks`] queue until it returns a non-empty result.
100    fn poll_next_task(&mut self, cx: &mut Context<'_>) -> Poll<Option<BackfillJobResult<T>>> {
101        while let Some(res) = ready!(self.tasks.poll_next_unpin(cx)) {
102            let task_result = res.map_err(BlockExecutionError::other)?;
103
104            if let BackfillTaskOutput { result: Some(job_result), job } = task_result {
105                // If the task returned a non-empty result, a new task advancing the job is created
106                // and pushed to the __front__ of the queue, so that the next item of this returned
107                // next.
108                self.push_front(job);
109
110                return Poll::Ready(Some(job_result))
111            };
112        }
113
114        Poll::Ready(None)
115    }
116}
117
118impl<E, P> Stream for StreamBackfillJob<E, P, SingleBlockStreamItem<E::Primitives>>
119where
120    E: ConfigureEvm<Primitives: NodePrimitives<Block = P::Block>> + 'static,
121    P: BlockReader + StateProviderFactory + Clone + Unpin + 'static,
122{
123    type Item = BackfillJobResult<SingleBlockStreamItem<E::Primitives>>;
124
125    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
126        let this = self.get_mut();
127
128        // Spawn new tasks only if we are below the parallelism configured.
129        while this.tasks.len() < this.parallelism {
130            // Get the next block number from the range. If it is empty, we are done.
131            let Some(block_number) = this.range.next() else {
132                debug!(target: "exex::backfill", tasks = %this.tasks.len(), range = ?this.range, "No more single blocks to backfill");
133                break;
134            };
135
136            // Spawn a new task for that block
137            debug!(target: "exex::backfill", tasks = %this.tasks.len(), ?block_number, "Spawning new single block backfill task");
138            let job = Box::new(SingleBlockBackfillJob {
139                evm_config: this.evm_config.clone(),
140                provider: this.provider.clone(),
141                range: block_number..=block_number,
142                stream_parallelism: this.parallelism,
143            }) as BackfillTaskIterator<_>;
144            this.push_back(job);
145        }
146
147        this.poll_next_task(cx)
148    }
149}
150
151impl<E, P> Stream for StreamBackfillJob<E, P, BatchBlockStreamItem<E::Primitives>>
152where
153    E: ConfigureEvm<Primitives: NodePrimitives<Block = P::Block>> + 'static,
154    P: BlockReader + StateProviderFactory + Clone + Unpin + 'static,
155{
156    type Item = BackfillJobResult<BatchBlockStreamItem<E::Primitives>>;
157
158    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
159        let this = self.get_mut();
160
161        loop {
162            // Spawn new tasks only if we are below the parallelism configured.
163            while this.tasks.len() < this.parallelism {
164                // Take the next `batch_size` blocks from the range and calculate the range bounds
165                let mut range = this.range.by_ref().take(this.batch_size);
166                let start = range.next();
167                let range_bounds = start.zip(range.last().or(start));
168
169                // Create the range from the range bounds. If it is empty, we are done.
170                let Some(range) = range_bounds.map(|(first, last)| first..=last) else {
171                    debug!(target: "exex::backfill", tasks = %this.tasks.len(), range = ?this.range, "No more block batches to backfill");
172                    break;
173                };
174
175                // Spawn a new task for that range
176                debug!(target: "exex::backfill", tasks = %this.tasks.len(), ?range, "Spawning new block batch backfill task");
177                let job = Box::new(BackfillJob {
178                    evm_config: this.evm_config.clone(),
179                    provider: this.provider.clone(),
180                    prune_modes: this.prune_modes.clone(),
181                    thresholds: this.thresholds.clone(),
182                    range,
183                    stream_parallelism: this.parallelism,
184                }) as BackfillTaskIterator<_>;
185                this.push_back(job);
186            }
187
188            let res = ready!(this.poll_next_task(cx));
189
190            if res.is_some() {
191                return Poll::Ready(res);
192            }
193
194            if this.range.is_empty() {
195                // only terminate the stream if there are no more blocks to process
196                return Poll::Ready(None);
197            }
198        }
199    }
200}
201
202impl<E, P> From<SingleBlockBackfillJob<E, P>>
203    for StreamBackfillJob<E, P, SingleBlockStreamItem<E::Primitives>>
204where
205    E: ConfigureEvm,
206{
207    fn from(job: SingleBlockBackfillJob<E, P>) -> Self {
208        Self {
209            evm_config: job.evm_config,
210            provider: job.provider,
211            prune_modes: PruneModes::default(),
212            range: job.range,
213            tasks: FuturesOrdered::new(),
214            parallelism: job.stream_parallelism,
215            batch_size: 1,
216            thresholds: ExecutionStageThresholds { max_blocks: Some(1), ..Default::default() },
217        }
218    }
219}
220
221impl<E, P> From<BackfillJob<E, P>> for StreamBackfillJob<E, P, BatchBlockStreamItem<E::Primitives>>
222where
223    E: ConfigureEvm,
224{
225    fn from(job: BackfillJob<E, P>) -> Self {
226        let batch_size = job.thresholds.max_blocks.map_or(DEFAULT_BATCH_SIZE, |max| max as usize);
227        Self {
228            evm_config: job.evm_config,
229            provider: job.provider,
230            prune_modes: job.prune_modes,
231            range: job.range,
232            tasks: FuturesOrdered::new(),
233            parallelism: job.stream_parallelism,
234            batch_size,
235            thresholds: ExecutionStageThresholds {
236                max_blocks: Some(batch_size as u64),
237                ..job.thresholds
238            },
239        }
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246    use crate::{
247        backfill::test_utils::{
248            blocks_and_execution_outcome, blocks_and_execution_outputs, chain_spec,
249            execute_block_and_commit_to_database,
250        },
251        BackfillJobFactory,
252    };
253    use alloy_consensus::{constants::ETH_TO_WEI, Header, TxEip2930};
254    use alloy_primitives::{b256, Address, TxKind, U256};
255    use eyre::Result;
256    use futures::StreamExt;
257    use reth_chainspec::{ChainSpec, EthereumHardfork, MIN_TRANSACTION_GAS};
258    use reth_db_common::init::init_genesis;
259    use reth_ethereum_primitives::{Block, BlockBody, Transaction};
260    use reth_evm_ethereum::EthEvmConfig;
261    use reth_primitives_traits::{Block as _, NodePrimitives};
262    use reth_provider::{
263        providers::{BlockchainProvider, ProviderNodeTypes},
264        test_utils::create_test_provider_factory_with_chain_spec,
265        ProviderFactory,
266    };
267    use reth_stages_api::ExecutionStageThresholds;
268    use reth_testing_utils::{generators, generators::sign_tx_with_key_pair};
269    use secp256k1::Keypair;
270    use std::sync::Arc;
271
272    #[tokio::test]
273    async fn test_single_blocks() -> eyre::Result<()> {
274        reth_tracing::init_test_tracing();
275
276        // Create a key pair for the sender
277        let key_pair = generators::generate_key(&mut generators::rng());
278        let address =
279            Address::from_raw_public_key(&key_pair.public_key().serialize_uncompressed()[1..]);
280
281        let chain_spec = chain_spec(address);
282
283        let executor = EthEvmConfig::ethereum(chain_spec.clone());
284        let provider_factory = create_test_provider_factory_with_chain_spec(chain_spec.clone());
285        init_genesis(&provider_factory)?;
286        let blockchain_db = BlockchainProvider::new(provider_factory.clone())?;
287
288        // Create first 2 blocks
289        let blocks_and_execution_outcomes =
290            blocks_and_execution_outputs(provider_factory, chain_spec, key_pair)?;
291
292        // Backfill the first block
293        let factory = BackfillJobFactory::new(executor.clone(), blockchain_db.clone());
294        let mut backfill_stream = factory.backfill(1..=1).into_single_blocks().into_stream();
295
296        // execute first block
297        let (block, execution_output) = backfill_stream.next().await.unwrap().unwrap();
298        let expected_block = blocks_and_execution_outcomes[0].0.clone();
299        let expected_output = &blocks_and_execution_outcomes[0].1;
300        assert_eq!(block, expected_block);
301        assert_eq!(&execution_output, expected_output);
302
303        // expect no more blocks
304        assert!(backfill_stream.next().await.is_none());
305
306        Ok(())
307    }
308
309    #[tokio::test]
310    async fn test_batch() -> eyre::Result<()> {
311        reth_tracing::init_test_tracing();
312
313        // Create a key pair for the sender
314        let key_pair = generators::generate_key(&mut generators::rng());
315        let address =
316            Address::from_raw_public_key(&key_pair.public_key().serialize_uncompressed()[1..]);
317
318        let chain_spec = chain_spec(address);
319
320        let executor = EthEvmConfig::ethereum(chain_spec.clone());
321        let provider_factory = create_test_provider_factory_with_chain_spec(chain_spec.clone());
322        init_genesis(&provider_factory)?;
323        let blockchain_db = BlockchainProvider::new(provider_factory.clone())?;
324
325        // Create first 2 blocks
326        let (blocks, execution_outcome) =
327            blocks_and_execution_outcome(provider_factory, chain_spec, key_pair)?;
328
329        // Backfill the same range
330        let factory = BackfillJobFactory::new(executor.clone(), blockchain_db.clone())
331            .with_thresholds(ExecutionStageThresholds { max_blocks: Some(2), ..Default::default() })
332            .with_stream_parallelism(1);
333        let mut backfill_stream = factory.backfill(1..=2).into_stream();
334        let chain = backfill_stream.next().await.unwrap().unwrap();
335
336        assert!(chain.blocks_iter().map(|block| block.as_ref()).eq(&blocks));
337        assert_eq!(chain.execution_outcome(), &execution_outcome);
338
339        // expect no more blocks
340        assert!(backfill_stream.next().await.is_none());
341
342        Ok(())
343    }
344
345    fn create_blocks(
346        chain_spec: &Arc<ChainSpec>,
347        key_pair: Keypair,
348        n: u64,
349    ) -> Result<Vec<RecoveredBlock<reth_ethereum_primitives::Block>>> {
350        let mut blocks = Vec::with_capacity(n as usize);
351        let mut parent_hash = chain_spec.genesis_hash();
352
353        for (i, nonce) in (1..=n).zip(0..n) {
354            let block = Block {
355                header: Header {
356                    parent_hash,
357                    // Hardcoded receipts_root matching the original test (same tx in each block)
358                    receipts_root: b256!(
359                        "0xd3a6acf9a244d78b33831df95d472c4128ea85bf079a1d41e32ed0b7d2244c9e"
360                    ),
361                    difficulty: chain_spec.fork(EthereumHardfork::Paris).ttd().expect("Paris TTD"),
362                    number: i,
363                    gas_limit: MIN_TRANSACTION_GAS,
364                    gas_used: MIN_TRANSACTION_GAS,
365                    ..Default::default()
366                },
367                body: BlockBody {
368                    transactions: vec![sign_tx_with_key_pair(
369                        key_pair,
370                        Transaction::Eip2930(TxEip2930 {
371                            chain_id: chain_spec.chain.id(),
372                            nonce,
373                            gas_limit: MIN_TRANSACTION_GAS,
374                            gas_price: 1_500_000_000,
375                            to: TxKind::Call(Address::ZERO),
376                            value: U256::from(0.1 * ETH_TO_WEI as f64),
377                            ..Default::default()
378                        }),
379                    )],
380                    ..Default::default()
381                },
382            }
383            .try_into_recovered()?;
384
385            parent_hash = block.hash();
386            blocks.push(block);
387        }
388
389        Ok(blocks)
390    }
391
392    fn execute_and_commit_blocks<N>(
393        provider_factory: &ProviderFactory<N>,
394        chain_spec: &Arc<ChainSpec>,
395        blocks: &[RecoveredBlock<reth_ethereum_primitives::Block>],
396    ) -> Result<()>
397    where
398        N: ProviderNodeTypes<
399            Primitives: NodePrimitives<
400                Block = reth_ethereum_primitives::Block,
401                BlockBody = reth_ethereum_primitives::BlockBody,
402                Receipt = reth_ethereum_primitives::Receipt,
403            >,
404        >,
405    {
406        for block in blocks {
407            execute_block_and_commit_to_database(provider_factory, chain_spec.clone(), block)?;
408        }
409        Ok(())
410    }
411
412    #[tokio::test]
413    async fn test_batch_parallel_range_advance() -> Result<()> {
414        reth_tracing::init_test_tracing();
415
416        // Create a key pair for the sender
417        let key_pair = generators::generate_key(&mut generators::rng());
418        let address =
419            Address::from_raw_public_key(&key_pair.public_key().serialize_uncompressed()[1..]);
420
421        let chain_spec = chain_spec(address);
422
423        let executor = EthEvmConfig::ethereum(chain_spec.clone());
424        let provider_factory = create_test_provider_factory_with_chain_spec(chain_spec.clone());
425        init_genesis(&provider_factory)?;
426        let blockchain_db = BlockchainProvider::new(provider_factory.clone())?;
427
428        // Create and commit 4 blocks
429        let blocks = create_blocks(&chain_spec, key_pair, 4)?;
430        execute_and_commit_blocks(&provider_factory, &chain_spec, &blocks)?;
431
432        // Create factory with batch size 2 (via thresholds max_blocks=2) and parallelism=2
433        let factory = BackfillJobFactory::new(executor.clone(), blockchain_db.clone())
434            .with_thresholds(ExecutionStageThresholds { max_blocks: Some(2), ..Default::default() })
435            .with_stream_parallelism(2);
436
437        // Stream backfill for range 1..=4
438        let mut backfill_stream = factory.backfill(1..=4).into_stream();
439
440        // Collect the two expected chains from the stream
441        let chain1 = backfill_stream.next().await.unwrap()?;
442        let chain2 = backfill_stream.next().await.unwrap()?;
443        assert!(backfill_stream.next().await.is_none());
444
445        // Compute expected chains using non-stream BackfillJob (sequential)
446        let factory_seq =
447            BackfillJobFactory::new(executor.clone(), blockchain_db.clone()).with_thresholds(
448                ExecutionStageThresholds { max_blocks: Some(2), ..Default::default() },
449            );
450
451        let expected_chain1 =
452            factory_seq.backfill(1..=2).collect::<Result<Vec<_>, _>>()?.into_iter().next().unwrap();
453        let expected_chain2 =
454            factory_seq.backfill(3..=4).collect::<Result<Vec<_>, _>>()?.into_iter().next().unwrap();
455
456        // Assert the streamed chains match the expected sequential ones
457        assert_eq!(chain1.blocks(), expected_chain1.blocks());
458        assert_eq!(chain1.execution_outcome(), expected_chain1.execution_outcome());
459        assert_eq!(chain2.blocks(), expected_chain2.blocks());
460        assert_eq!(chain2.execution_outcome(), expected_chain2.execution_outcome());
461
462        Ok(())
463    }
464}