Skip to main content

reth_exex/backfill/
job.rs

1use crate::{backfill::stream::SingleBlockStreamItem, StreamBackfillJob};
2use reth_evm::{
3    database::StateProviderDatabase, BlockExecutionError, BlockExecutionOutput, ConfigureEvm,
4    Executor,
5};
6use std::{
7    collections::BTreeMap,
8    ops::RangeInclusive,
9    time::{Duration, Instant},
10};
11
12use alloy_consensus::BlockHeader;
13use alloy_primitives::BlockNumber;
14use reth_node_api::{Block as _, BlockBody as _, NodePrimitives};
15use reth_primitives_traits::{format_gas_throughput, RecoveredBlock, SignedTransaction};
16use reth_provider::{
17    BlockReader, Chain, ExecutionOutcome, ProviderError, StateProvider, StateProviderFactory,
18    TransactionVariant,
19};
20use reth_prune_types::PruneModes;
21use reth_stages_api::ExecutionStageThresholds;
22use tracing::{debug, trace};
23
24pub(super) type BackfillJobResult<T> = Result<T, BlockExecutionError>;
25
26/// Backfill job started for a specific range.
27///
28/// It implements [`Iterator`] that executes blocks in batches according to the provided thresholds
29/// and yields [`Chain`]. In other words, this iterator can yield multiple items for the given range
30/// depending on the configured thresholds.
31#[derive(Debug)]
32pub struct BackfillJob<E, P> {
33    pub(crate) evm_config: E,
34    pub(crate) provider: P,
35    pub(crate) prune_modes: PruneModes,
36    pub(crate) thresholds: ExecutionStageThresholds,
37    pub(crate) range: RangeInclusive<BlockNumber>,
38    pub(crate) stream_parallelism: usize,
39}
40
41impl<E, P> Iterator for BackfillJob<E, P>
42where
43    E: ConfigureEvm<Primitives: NodePrimitives<Block = P::Block>> + 'static,
44    P: BlockReader<Transaction: SignedTransaction> + StateProviderFactory,
45{
46    type Item = BackfillJobResult<Chain<E::Primitives>>;
47
48    fn next(&mut self) -> Option<Self::Item> {
49        if self.range.is_empty() {
50            return None
51        }
52
53        Some(self.execute_range())
54    }
55}
56
57impl<E, P> BackfillJob<E, P>
58where
59    E: ConfigureEvm<Primitives: NodePrimitives<Block = P::Block>> + 'static,
60    P: BlockReader<Transaction: SignedTransaction> + StateProviderFactory,
61{
62    /// Converts the backfill job into a single block backfill job.
63    pub fn into_single_blocks(self) -> SingleBlockBackfillJob<E, P> {
64        self.into()
65    }
66
67    /// Converts the backfill job into a stream.
68    pub fn into_stream(self) -> StreamBackfillJob<E, P, Chain<E::Primitives>> {
69        self.into()
70    }
71
72    fn execute_range(&mut self) -> BackfillJobResult<Chain<E::Primitives>> {
73        debug!(
74            target: "exex::backfill",
75            range = ?self.range,
76            "Executing block range"
77        );
78
79        let mut executor = self.evm_config.batch_executor(StateProviderDatabase::new(
80            self.provider
81                .history_by_block_number(self.range.start().saturating_sub(1))
82                .map_err(BlockExecutionError::other)?
83                .into_evm_state_provider(),
84        ));
85
86        let mut fetch_block_duration = Duration::default();
87        let mut execution_duration = Duration::default();
88        let mut cumulative_gas = 0;
89        let batch_start = Instant::now();
90
91        let mut blocks = Vec::new();
92        let mut results = Vec::new();
93        for block_number in self.range.clone() {
94            // Fetch the block
95            let fetch_block_start = Instant::now();
96
97            // we need the block's transactions along with their hashes
98            let block = self
99                .provider
100                .sealed_block_with_senders(block_number.into(), TransactionVariant::WithHash)
101                .map_err(BlockExecutionError::other)?
102                .ok_or_else(|| ProviderError::HeaderNotFound(block_number.into()))
103                .map_err(BlockExecutionError::other)?;
104
105            fetch_block_duration += fetch_block_start.elapsed();
106
107            cumulative_gas += block.gas_used();
108
109            // Configure the executor to use the current state.
110            trace!(target: "exex::backfill", number = block_number, txs = block.body().transactions().len(), "Executing block");
111
112            // Execute the block
113            let execute_start = Instant::now();
114
115            // Unseal the block for execution
116            let (block, senders) = block.split_sealed();
117            let (header, body) = block.split_sealed_header_body();
118            let block = P::Block::new_sealed(header, body).with_senders(senders);
119
120            results.push(executor.execute_one(&block)?);
121            execution_duration += execute_start.elapsed();
122
123            // Seal the block back and save it
124            blocks.push(block);
125            // Check if we should commit now
126            if self.thresholds.is_end_of_batch(
127                block_number - *self.range.start() + 1,
128                executor.size_hint() as u64,
129                cumulative_gas,
130                batch_start.elapsed(),
131            ) {
132                break
133            }
134        }
135
136        let first_block_number = blocks.first().expect("blocks should not be empty").number();
137        let last_block_number = blocks.last().expect("blocks should not be empty").number();
138        debug!(
139            target: "exex::backfill",
140            range = ?*self.range.start()..=last_block_number,
141            block_fetch = ?fetch_block_duration,
142            execution = ?execution_duration,
143            throughput = format_gas_throughput(cumulative_gas, execution_duration),
144            "Finished executing block range"
145        );
146        self.range = last_block_number + 1..=*self.range.end();
147
148        let outcome =
149            ExecutionOutcome::from_blocks(first_block_number, executor.into_state(), results);
150        let chain = Chain::new(blocks, outcome, BTreeMap::new());
151        Ok(chain)
152    }
153}
154
155/// Single block Backfill job started for a specific range.
156///
157/// It implements [`Iterator`] which executes a block each time the
158/// iterator is advanced and yields ([`RecoveredBlock`], [`BlockExecutionOutput`])
159#[derive(Debug, Clone)]
160pub struct SingleBlockBackfillJob<E, P> {
161    pub(crate) evm_config: E,
162    pub(crate) provider: P,
163    pub(crate) range: RangeInclusive<BlockNumber>,
164    pub(crate) stream_parallelism: usize,
165}
166
167impl<E, P> Iterator for SingleBlockBackfillJob<E, P>
168where
169    E: ConfigureEvm<Primitives: NodePrimitives<Block = P::Block>> + 'static,
170    P: BlockReader + StateProviderFactory,
171{
172    type Item = BackfillJobResult<(
173        RecoveredBlock<P::Block>,
174        BlockExecutionOutput<<E::Primitives as NodePrimitives>::Receipt>,
175    )>;
176
177    fn next(&mut self) -> Option<Self::Item> {
178        self.range.next().map(|block_number| self.execute_block(block_number))
179    }
180}
181
182impl<E, P> SingleBlockBackfillJob<E, P>
183where
184    E: ConfigureEvm<Primitives: NodePrimitives<Block = P::Block>> + 'static,
185    P: BlockReader + StateProviderFactory,
186{
187    /// Converts the single block backfill job into a stream.
188    pub fn into_stream(self) -> StreamBackfillJob<E, P, SingleBlockStreamItem<E::Primitives>> {
189        self.into()
190    }
191
192    #[expect(clippy::type_complexity)]
193    pub(crate) fn execute_block(
194        &self,
195        block_number: u64,
196    ) -> BackfillJobResult<(
197        RecoveredBlock<P::Block>,
198        BlockExecutionOutput<<E::Primitives as NodePrimitives>::Receipt>,
199    )> {
200        // Fetch the block with senders for execution.
201        let block_with_senders = self
202            .provider
203            .recovered_block(block_number.into(), TransactionVariant::WithHash)
204            .map_err(BlockExecutionError::other)?
205            .ok_or_else(|| ProviderError::HeaderNotFound(block_number.into()))
206            .map_err(BlockExecutionError::other)?;
207
208        // Configure the executor to use the previous block's state.
209        let executor = self.evm_config.batch_executor(StateProviderDatabase::new(
210            self.provider
211                .history_by_block_number(block_number.saturating_sub(1))
212                .map_err(BlockExecutionError::other)?
213                .into_evm_state_provider(),
214        ));
215
216        trace!(target: "exex::backfill", number = block_number, txs = block_with_senders.body().transaction_count(), "Executing block");
217
218        let block_execution_output = executor.execute(&block_with_senders)?;
219
220        Ok((block_with_senders, block_execution_output))
221    }
222}
223
224impl<E, P> From<BackfillJob<E, P>> for SingleBlockBackfillJob<E, P> {
225    fn from(job: BackfillJob<E, P>) -> Self {
226        Self {
227            evm_config: job.evm_config,
228            provider: job.provider,
229            range: job.range,
230            stream_parallelism: job.stream_parallelism,
231        }
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use crate::{
238        backfill::{
239            job::ExecutionStageThresholds,
240            test_utils::{blocks_and_execution_outputs, chain_spec, to_execution_outcome},
241        },
242        BackfillJobFactory,
243    };
244    use alloy_consensus::BlockHeader;
245    use alloy_primitives::Address;
246    use reth_db_common::init::init_genesis;
247    use reth_evm_ethereum::EthEvmConfig;
248    use reth_provider::{
249        providers::BlockchainProvider, test_utils::create_test_provider_factory_with_chain_spec,
250    };
251    use reth_testing_utils::generators;
252
253    #[test]
254    fn test_backfill() -> eyre::Result<()> {
255        reth_tracing::init_test_tracing();
256
257        // Create a key pair for the sender
258        let key_pair = generators::generate_key(&mut generators::rng());
259        let address =
260            Address::from_raw_public_key(&key_pair.public_key().serialize_uncompressed()[1..]);
261
262        let chain_spec = chain_spec(address);
263
264        let executor = EthEvmConfig::ethereum(chain_spec.clone());
265        let provider_factory = create_test_provider_factory_with_chain_spec(chain_spec.clone());
266        init_genesis(&provider_factory)?;
267        let blockchain_db = BlockchainProvider::new(provider_factory.clone())?;
268
269        let blocks_and_execution_outputs =
270            blocks_and_execution_outputs(provider_factory, chain_spec, key_pair)?;
271        let (block, block_execution_output) = blocks_and_execution_outputs.first().unwrap();
272        let execution_outcome = to_execution_outcome(block.number, block_execution_output);
273
274        // Backfill the first block
275        let factory = BackfillJobFactory::new(executor, blockchain_db);
276        let job = factory.backfill(1..=1);
277        let chains = job.collect::<Result<Vec<_>, _>>()?;
278
279        // Assert that the backfill job produced the same chain as we got before when we were
280        // executing only the first block
281        assert_eq!(chains.len(), 1);
282        let chain = chains.into_iter().next().unwrap();
283        assert_eq!(chain.blocks().len(), 1);
284        assert_eq!(chain.blocks().get(&1).map(|block| block.as_ref()), Some(block));
285        assert_eq!(chain.execution_outcome(), &execution_outcome);
286
287        Ok(())
288    }
289
290    #[test]
291    fn test_single_block_backfill() -> eyre::Result<()> {
292        reth_tracing::init_test_tracing();
293
294        // Create a key pair for the sender
295        let key_pair = generators::generate_key(&mut generators::rng());
296        let address =
297            Address::from_raw_public_key(&key_pair.public_key().serialize_uncompressed()[1..]);
298
299        let chain_spec = chain_spec(address);
300
301        let executor = EthEvmConfig::ethereum(chain_spec.clone());
302        let provider_factory = create_test_provider_factory_with_chain_spec(chain_spec.clone());
303        init_genesis(&provider_factory)?;
304        let blockchain_db = BlockchainProvider::new(provider_factory.clone())?;
305
306        let blocks_and_execution_outcomes =
307            blocks_and_execution_outputs(provider_factory, chain_spec, key_pair)?;
308
309        // Backfill the first block
310        let factory = BackfillJobFactory::new(executor, blockchain_db);
311        let job = factory.backfill(1..=1);
312        let single_job = job.into_single_blocks();
313        let block_execution_it = single_job.into_iter();
314
315        // Assert that the backfill job only produces a single block
316        let blocks_and_outcomes = block_execution_it.collect::<Vec<_>>();
317        assert_eq!(blocks_and_outcomes.len(), 1);
318
319        // Assert that the backfill job single block iterator produces the expected output for each
320        // block
321        for (i, res) in blocks_and_outcomes.into_iter().enumerate() {
322            let (block, execution_output) = res?;
323
324            let expected_block = blocks_and_execution_outcomes[i].0.clone();
325            let expected_output = &blocks_and_execution_outcomes[i].1;
326
327            assert_eq!(block, expected_block);
328            assert_eq!(&execution_output, expected_output);
329        }
330
331        Ok(())
332    }
333
334    /// Verify that ExEx backfill (using `history_by_block_number`) produces identical execution
335    /// results to the pipeline path (using `LatestStateProvider`).
336    ///
337    /// This is a regression test for an issue reported on mainnet where backfill fails around
338    /// blocks 1.7M-3.8M with "transaction gas limit X is more than blocks available gas Y",
339    /// suggesting the state provider used during backfill may return different state than what the
340    /// pipeline used during initial sync.
341    #[test]
342    fn test_backfill_state_provider_parity() -> eyre::Result<()> {
343        reth_tracing::init_test_tracing();
344
345        let key_pair = generators::generate_key(&mut generators::rng());
346        let address =
347            Address::from_raw_public_key(&key_pair.public_key().serialize_uncompressed()[1..]);
348
349        let chain_spec = chain_spec(address);
350
351        let executor = EthEvmConfig::ethereum(chain_spec.clone());
352        let provider_factory = create_test_provider_factory_with_chain_spec(chain_spec.clone());
353        init_genesis(&provider_factory)?;
354        let blockchain_db = BlockchainProvider::new(provider_factory.clone())?;
355
356        // Execute blocks via LatestStateProvider (pipeline-style) and commit to DB.
357        // This mirrors what the pipeline's ExecutionStage does.
358        let pipeline_results =
359            blocks_and_execution_outputs(provider_factory, chain_spec, key_pair)?;
360
361        // Now re-execute via SingleBlockBackfillJob which uses history_by_block_number.
362        let factory = BackfillJobFactory::new(executor, blockchain_db);
363        let job = factory.backfill(1..=2);
364        let single_job = job.into_single_blocks();
365        let backfill_results: Vec<_> = single_job.into_iter().collect::<Result<Vec<_>, _>>()?;
366
367        assert_eq!(
368            pipeline_results.len(),
369            backfill_results.len(),
370            "should produce same number of block results"
371        );
372
373        for (i, ((pipeline_block, pipeline_output), (backfill_block, backfill_output))) in
374            pipeline_results.iter().zip(backfill_results).enumerate()
375        {
376            assert_eq!(
377                backfill_block, *pipeline_block,
378                "block {i} mismatch between pipeline and backfill"
379            );
380
381            assert_eq!(
382                backfill_output.receipts, pipeline_output.receipts,
383                "block {i}: receipts differ — gas accounting divergence between \
384                 LatestStateProvider and history_by_block_number"
385            );
386
387            assert_eq!(
388                backfill_output.gas_used, pipeline_output.gas_used,
389                "block {i}: gas_used differs"
390            );
391
392            assert_eq!(
393                &backfill_output, pipeline_output,
394                "block {i}: full execution output differs between pipeline and backfill"
395            );
396        }
397
398        Ok(())
399    }
400
401    /// Same as above but for the batch `BackfillJob` path (`execute_range`), which also uses
402    /// `history_by_block_number`. Verifies the batch execution outcome matches what the pipeline
403    /// produced block-by-block.
404    #[test]
405    fn test_backfill_batch_state_provider_parity() -> eyre::Result<()> {
406        reth_tracing::init_test_tracing();
407
408        let key_pair = generators::generate_key(&mut generators::rng());
409        let address =
410            Address::from_raw_public_key(&key_pair.public_key().serialize_uncompressed()[1..]);
411
412        let chain_spec = chain_spec(address);
413
414        let executor = EthEvmConfig::ethereum(chain_spec.clone());
415        let provider_factory = create_test_provider_factory_with_chain_spec(chain_spec.clone());
416        init_genesis(&provider_factory)?;
417        let blockchain_db = BlockchainProvider::new(provider_factory.clone())?;
418
419        let pipeline_results =
420            blocks_and_execution_outputs(provider_factory, chain_spec, key_pair)?;
421
422        // Re-execute via batch BackfillJob (execute_range) using history_by_block_number
423        let factory = BackfillJobFactory::new(executor, blockchain_db);
424        let job = factory.backfill(1..=2);
425        let chains = job.collect::<Result<Vec<_>, _>>()?;
426
427        assert_eq!(chains.len(), 1, "two blocks without threshold should yield one chain");
428        let chain = chains.into_iter().next().unwrap();
429
430        // Compare each block's receipts from the chain against the pipeline outputs
431        for (i, (pipeline_block, pipeline_output)) in pipeline_results.iter().enumerate() {
432            let block_number = pipeline_block.number();
433            let chain_block = chain.blocks().get(&block_number).expect("block should be in chain");
434            assert_eq!(
435                chain_block.as_ref(),
436                pipeline_block,
437                "block {i}: block mismatch in batch backfill"
438            );
439
440            let chain_receipts = &chain.execution_outcome().receipts[i];
441            assert_eq!(
442                chain_receipts, &pipeline_output.receipts,
443                "block {i}: receipts differ in batch backfill — potential gas accounting \
444                 divergence between LatestStateProvider and history_by_block_number"
445            );
446        }
447
448        let mut expected = to_execution_outcome(1, &pipeline_results[0].1);
449        expected.extend(to_execution_outcome(2, &pipeline_results[1].1));
450        expected.state_mut().reverts.sort();
451        let mut actual = chain.execution_outcome().clone();
452        actual.state_mut().reverts.sort();
453        assert_eq!(actual, expected, "batch state and reverts must match sequential execution");
454
455        Ok(())
456    }
457
458    #[test]
459    fn test_backfill_with_batch_threshold() -> eyre::Result<()> {
460        reth_tracing::init_test_tracing();
461
462        // Create a key pair for the sender
463        let key_pair = generators::generate_key(&mut generators::rng());
464        let address =
465            Address::from_raw_public_key(&key_pair.public_key().serialize_uncompressed()[1..]);
466
467        let chain_spec = chain_spec(address);
468
469        let executor = EthEvmConfig::ethereum(chain_spec.clone());
470        let provider_factory = create_test_provider_factory_with_chain_spec(chain_spec.clone());
471        init_genesis(&provider_factory)?;
472        let blockchain_db = BlockchainProvider::new(provider_factory.clone())?;
473
474        let blocks_and_execution_outputs =
475            blocks_and_execution_outputs(provider_factory, chain_spec, key_pair)?;
476        let (block1, output1) = blocks_and_execution_outputs[0].clone();
477        let (block2, output2) = blocks_and_execution_outputs[1].clone();
478
479        // Backfill with max_blocks=1, expect two separate chains
480        let factory = BackfillJobFactory::new(executor, blockchain_db).with_thresholds(
481            ExecutionStageThresholds { max_blocks: Some(1), ..Default::default() },
482        );
483        let job = factory.backfill(1..=2);
484        let chains = job.collect::<Result<Vec<_>, _>>()?;
485
486        // Assert two chains, each with one block
487        assert_eq!(chains.len(), 2);
488
489        let chain1 = chains[0].clone();
490        assert_eq!(chain1.blocks().len(), 1);
491        assert_eq!(chain1.blocks().get(&1).map(|block| block.as_ref()), Some(&block1));
492        assert_eq!(chain1.execution_outcome(), &to_execution_outcome(1, &output1));
493
494        let chain2 = chains[1].clone();
495        assert_eq!(chain2.blocks().len(), 1);
496        assert_eq!(chain2.blocks().get(&2).map(|block| block.as_ref()), Some(&block2));
497        assert_eq!(chain2.execution_outcome(), &to_execution_outcome(2, &output2));
498
499        Ok(())
500    }
501}