Skip to main content

reth_bb/
evm.rs

1//! Big-block executor.
2//!
3//! Provides [`BbBlockExecutor`] and [`BbBlockExecutorFactory`] which handle
4//! segment boundaries within big-block payloads.
5//!
6//! [`BbBlockExecutor`] wraps [`EthBlockExecutor`] and intercepts
7//! `execute_transaction` to apply segment-boundary changes.
8
9use crate::evm_config::BigBlockSegment;
10use alloy_eips::eip7685::Requests;
11use alloy_evm::{
12    block::{
13        BlockExecutionError, BlockExecutionResult, BlockExecutor, BlockExecutorFactory,
14        BlockExecutorFor, ExecutableTx, GasOutput, OnStateHook, StateChangeSource, StateDB,
15    },
16    eth::{EthBlockExecutionCtx, EthBlockExecutor, EthEvmContext, EthTxResult},
17    precompiles::PrecompilesMap,
18    Database, EthEvm, EthEvmFactory, Evm, FromRecoveredTx, FromTxWithEncoded,
19};
20use alloy_primitives::B256;
21use reth_ethereum_primitives::{Receipt, TransactionSigned};
22use reth_evm_ethereum::RethReceiptBuilder;
23use revm::{
24    context::{BlockEnv, TxEnv},
25    context_interface::result::{EVMError, HaltReason},
26    handler::PrecompileProvider,
27    interpreter::InterpreterResult,
28    primitives::hardfork::SpecId,
29    Inspector,
30};
31use std::sync::{Arc, Mutex};
32use tracing::{debug, trace};
33
34// ---------------------------------------------------------------------------
35// BbEvmPlan — runtime segment tracking state
36// ---------------------------------------------------------------------------
37
38/// Runtime state for segment boundary tracking.
39pub(crate) struct BbEvmPlan {
40    /// The segment boundaries and environments.
41    pub(crate) segments: Vec<BigBlockSegment>,
42    /// Index of the next segment to switch to (starts at 1).
43    pub(crate) next_segment: usize,
44    /// Number of user transactions executed so far.
45    pub(crate) tx_counter: usize,
46    /// Block hashes to seed for inter-segment BLOCKHASH resolution.
47    /// Includes both prior block hashes and inter-segment hashes.
48    pub(crate) block_hashes_to_seed: Vec<(u64, B256)>,
49}
50
51impl BbEvmPlan {
52    /// Creates a new `BbEvmPlan` from segments and hardfork flags.
53    pub(crate) fn new(segments: Vec<BigBlockSegment>) -> Self {
54        // Pre-compute all inter-segment block hashes.
55        let mut block_hashes_to_seed = Vec::new();
56        for seg in segments.iter().skip(1) {
57            let finished_block_number = seg.evm_env.block_env.number.saturating_to::<u64>() - 1;
58            let finished_block_hash = seg.ctx.parent_hash;
59            block_hashes_to_seed.push((finished_block_number, finished_block_hash));
60        }
61
62        Self { segments, next_segment: 1, tx_counter: 0, block_hashes_to_seed }
63    }
64
65    /// Returns the 256 block hashes relevant to a segment with the given block
66    /// number. BLOCKHASH can look back 256 blocks, so we select entries in
67    /// `[block_number - 256, block_number)`.
68    pub(crate) fn hashes_for_block(&self, block_number: u64) -> Vec<(u64, B256)> {
69        let min = block_number.saturating_sub(256);
70        self.block_hashes_to_seed
71            .iter()
72            .copied()
73            .filter(|(n, _)| *n >= min && *n < block_number)
74            .collect()
75    }
76}
77
78impl std::fmt::Debug for BbEvmPlan {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        f.debug_struct("BbEvmPlan")
81            .field("segments", &self.segments)
82            .field("next_segment", &self.next_segment)
83            .field("tx_counter", &self.tx_counter)
84            .field("block_hashes_to_seed", &self.block_hashes_to_seed)
85            .finish()
86    }
87}
88
89// ---------------------------------------------------------------------------
90// BbBlockExecutor — handles segment boundaries
91// ---------------------------------------------------------------------------
92
93/// Function pointer that seeds block hashes into the DB's block hash cache.
94///
95/// Injected from `ConfigureEvm::create_executor` where the concrete `State<DB>`
96/// type is known, allowing `BbBlockExecutor` to reseed the ring buffer at
97/// segment boundaries without requiring additional trait bounds on `DB`.
98pub(crate) type BlockHashSeeder<DB> = fn(&mut DB, &[(u64, B256)]);
99
100/// Block executor that wraps [`EthBlockExecutor`] and handles segment-boundary
101/// changes for big-block execution.
102///
103/// At segment boundaries, the inner executor is finished (applying its
104/// end-of-block logic: post-execution system calls, withdrawal balance
105/// increments) and a new one is constructed for the next segment (applying
106/// its start-of-block logic: EIP-2935/EIP-4788 system calls).
107///
108/// Gas counters reset at each boundary so that each segment's real gas limit
109/// is used (preserving correct GASLIMIT opcode behavior). Accumulated offsets
110/// are applied to receipts and totals in `finish()`.
111pub(crate) struct BbBlockExecutor<'a, DB, I, P, Spec>
112where
113    DB: Database,
114{
115    /// The inner executor. `None` transiently during `apply_segment_boundary`.
116    inner: Option<EthBlockExecutor<'a, EthEvm<DB, I, P>, Spec, RethReceiptBuilder>>,
117    plan: Option<BbEvmPlan>,
118    /// Requests accumulated from segments that have been finished at
119    /// boundaries. Merged into the final result in `finish()`.
120    accumulated_requests: Requests,
121    /// Cumulative gas used by all segments that have been finished at
122    /// boundaries. Added to receipts and the final gas total in `finish()`.
123    gas_used_offset: u64,
124    /// Cumulative blob gas used by all segments that have been finished at
125    /// boundaries.
126    blob_gas_used_offset: u64,
127    /// Shared state hook that survives inner executor finish/reconstruct
128    /// cycles at segment boundaries. Each inner executor receives a
129    /// forwarding hook that delegates to this shared instance.
130    shared_hook: Arc<Mutex<Option<Box<dyn OnStateHook>>>>,
131    /// Callback to reseed block hashes into the DB's cache at segment
132    /// boundaries. See [`BlockHashSeeder`].
133    block_hash_seeder: Option<BlockHashSeeder<DB>>,
134}
135
136impl<'a, DB, I, P, Spec> BbBlockExecutor<'a, DB, I, P, Spec>
137where
138    DB: StateDB,
139    I: Inspector<EthEvmContext<DB>>,
140    P: PrecompileProvider<EthEvmContext<DB>, Output = InterpreterResult>,
141    Spec: alloy_evm::eth::spec::EthExecutorSpec + Clone,
142    EthEvm<DB, I, P>: Evm<
143        DB = DB,
144        Tx = TxEnv,
145        HaltReason = HaltReason,
146        Error = EVMError<DB::Error>,
147        Spec = SpecId,
148        BlockEnv = BlockEnv,
149    >,
150    TxEnv: FromRecoveredTx<TransactionSigned> + FromTxWithEncoded<TransactionSigned>,
151{
152    pub(crate) fn new(
153        evm: EthEvm<DB, I, P>,
154        ctx: EthBlockExecutionCtx<'a>,
155        spec: Spec,
156        receipt_builder: RethReceiptBuilder,
157        plan: Option<BbEvmPlan>,
158        block_hash_seeder: Option<BlockHashSeeder<DB>>,
159    ) -> Self {
160        let inner = EthBlockExecutor::new(evm, ctx, spec, receipt_builder);
161        Self {
162            inner: Some(inner),
163            plan,
164            accumulated_requests: Requests::default(),
165            gas_used_offset: 0,
166            blob_gas_used_offset: 0,
167            shared_hook: Arc::new(Mutex::new(None)),
168            block_hash_seeder,
169        }
170    }
171
172    /// Creates a forwarding `OnStateHook` that delegates to the shared hook.
173    fn forwarding_hook(&self) -> Option<Box<dyn OnStateHook>> {
174        let shared = self.shared_hook.clone();
175        Some(Box::new(move |source: StateChangeSource, state: &revm::state::EvmState| {
176            if let Some(hook) = shared.lock().unwrap().as_mut() {
177                hook.on_state(source, state);
178            }
179        }))
180    }
181
182    const fn inner(&self) -> &EthBlockExecutor<'a, EthEvm<DB, I, P>, Spec, RethReceiptBuilder> {
183        self.inner.as_ref().expect("inner executor must exist")
184    }
185
186    const fn inner_mut(
187        &mut self,
188    ) -> &mut EthBlockExecutor<'a, EthEvm<DB, I, P>, Spec, RethReceiptBuilder> {
189        self.inner.as_mut().expect("inner executor must exist")
190    }
191
192    fn reseed_block_hashes_for(&mut self, block_number: u64) {
193        let Some(seeder) = self.block_hash_seeder else { return };
194        let hashes = match &self.plan {
195            Some(plan) => plan.hashes_for_block(block_number),
196            None => return,
197        };
198        seeder(self.inner_mut().evm_mut().db_mut(), &hashes);
199    }
200
201    fn maybe_apply_boundary(&mut self) -> Result<(), BlockExecutionError> {
202        loop {
203            let plan = match &self.plan {
204                Some(p) => p,
205                None => return Ok(()),
206            };
207
208            if plan.next_segment >= plan.segments.len() ||
209                plan.tx_counter != plan.segments[plan.next_segment].start_tx
210            {
211                return Ok(());
212            }
213
214            self.apply_segment_boundary()?;
215        }
216    }
217
218    fn apply_segment_boundary(&mut self) -> Result<(), BlockExecutionError> {
219        let plan = self.plan.as_mut().expect("plan must exist");
220        let seg_idx = plan.next_segment;
221        let prev_seg_idx = seg_idx - 1;
222
223        debug!(
224            target: "engine::bb::evm",
225            seg_idx,
226            tx_counter = plan.tx_counter,
227            "Applying segment boundary"
228        );
229
230        // Swap the inner executor's ctx to the finished segment's values so
231        // that finish() applies the correct withdrawals and post-execution
232        // system calls for that segment.
233        let prev_segment = &plan.segments[prev_seg_idx];
234        let prev_ctx = EthBlockExecutionCtx {
235            parent_hash: prev_segment.ctx.parent_hash,
236            parent_beacon_block_root: prev_segment.ctx.parent_beacon_block_root,
237            ommers: prev_segment.ctx.ommers,
238            withdrawals: prev_segment.ctx.withdrawals.clone(),
239            extra_data: prev_segment.ctx.extra_data.clone(),
240            tx_count_hint: prev_segment.ctx.tx_count_hint,
241        };
242
243        // Clone the next segment's data before we consume inner.
244        let new_segment = &plan.segments[seg_idx];
245        let new_block_env = new_segment.evm_env.block_env.clone();
246        let mut new_cfg_env = new_segment.evm_env.cfg_env.clone();
247        new_cfg_env.disable_base_fee = true;
248        let new_ctx = EthBlockExecutionCtx {
249            parent_hash: new_segment.ctx.parent_hash,
250            parent_beacon_block_root: new_segment.ctx.parent_beacon_block_root,
251            ommers: new_segment.ctx.ommers,
252            withdrawals: new_segment.ctx.withdrawals.clone(),
253            extra_data: new_segment.ctx.extra_data.clone(),
254            tx_count_hint: new_segment.ctx.tx_count_hint,
255        };
256
257        plan.next_segment += 1;
258
259        // Finish the inner executor for the completed segment. This applies
260        // post-execution system calls (EIP-7002/7251) and withdrawal balance
261        // increments via EthBlockExecutor::finish().
262        let mut inner = self.inner.take().expect("inner executor must exist");
263        inner.ctx = prev_ctx;
264        let spec = inner.spec.clone();
265        let receipt_builder = inner.receipt_builder;
266        let (mut evm, result) = inner.finish()?;
267
268        // Receipts already have globally-correct cumulative_gas_used (fixed
269        // up in commit_transaction). Update the offset with this segment's
270        // gas so that subsequent segments' receipts are adjusted correctly.
271        self.gas_used_offset += result.gas_used;
272        self.blob_gas_used_offset += result.blob_gas_used;
273        self.accumulated_requests.extend(result.requests);
274
275        let last_receipt_cumulative =
276            result.receipts.last().map(|r| r.cumulative_gas_used).unwrap_or(0);
277        let seg_block_number = prev_segment.evm_env.block_env.number.saturating_to::<u64>();
278        debug!(
279            target: "engine::bb::evm",
280            prev_seg_idx,
281            seg_block_number,
282            segment_gas_used = result.gas_used,
283            gas_used_offset = self.gas_used_offset,
284            last_receipt_cumulative,
285            receipt_count = result.receipts.len(),
286            "Finished segment"
287        );
288
289        // Swap EVM env to the next segment's values (using real gas_limit).
290        let ctx = evm.ctx_mut();
291        ctx.block = new_block_env;
292        ctx.cfg = new_cfg_env;
293
294        // Build a new inner executor for the next segment. gas_used starts
295        // at 0 so the per-transaction gas check uses this segment's real
296        // gas_limit correctly.
297        let mut new_inner = EthBlockExecutor::new(evm, new_ctx, spec, receipt_builder);
298
299        // Carry forward receipts from prior segments.
300        new_inner.receipts = result.receipts;
301
302        // Re-install the forwarding state hook so the parallel state root
303        // task continues to receive state changes.
304        if self.shared_hook.lock().unwrap().is_some() {
305            new_inner.set_state_hook(self.forwarding_hook());
306        }
307
308        self.inner = Some(new_inner);
309
310        // Reseed the block hash cache for the new segment's 256-block window
311        // before applying pre-execution changes (which may use BLOCKHASH).
312        let new_block_number = self.plan.as_ref().unwrap().segments[seg_idx]
313            .evm_env
314            .block_env
315            .number
316            .saturating_to::<u64>();
317        self.reseed_block_hashes_for(new_block_number);
318
319        // Apply pre-execution changes for the new segment (EIP-2935, EIP-4788).
320        self.inner_mut().apply_pre_execution_changes()?;
321
322        trace!(target: "engine::bb::evm", "Started segment {seg_idx}");
323
324        Ok(())
325    }
326}
327
328impl<'a, DB, I, P, Spec> BlockExecutor for BbBlockExecutor<'a, DB, I, P, Spec>
329where
330    DB: StateDB,
331    I: Inspector<EthEvmContext<DB>>,
332    P: PrecompileProvider<EthEvmContext<DB>, Output = InterpreterResult>,
333    Spec: alloy_evm::eth::spec::EthExecutorSpec + Clone,
334    EthEvm<DB, I, P>: Evm<
335        DB = DB,
336        Tx = TxEnv,
337        HaltReason = HaltReason,
338        Error = EVMError<DB::Error>,
339        Spec = SpecId,
340        BlockEnv = BlockEnv,
341    >,
342    TxEnv: FromRecoveredTx<TransactionSigned> + FromTxWithEncoded<TransactionSigned>,
343{
344    type Transaction = TransactionSigned;
345    type Receipt = Receipt;
346    type Evm = EthEvm<DB, I, P>;
347    type Result = EthTxResult<HaltReason, alloy_consensus::TxType>;
348
349    fn apply_pre_execution_changes(&mut self) -> Result<(), BlockExecutionError> {
350        // Swap the EVM's block_env and executor ctx to the first segment's
351        // values so that the initial EIP-2935/EIP-4788 system calls use the
352        // correct block number and parent hash. Without this, the outer big
353        // block header's block_number (which is synthetic) would be used,
354        // writing to wrong EIP-2935 slots and corrupting state.
355        if let Some(seg0) = self.plan.as_ref().map(|p| &p.segments[0]) {
356            let block_env = seg0.evm_env.block_env.clone();
357            let block_number = block_env.number.saturating_to::<u64>();
358            let mut cfg_env = seg0.evm_env.cfg_env.clone();
359            cfg_env.disable_base_fee = true;
360            let seg0_ctx = EthBlockExecutionCtx {
361                parent_hash: seg0.ctx.parent_hash,
362                parent_beacon_block_root: seg0.ctx.parent_beacon_block_root,
363                ommers: seg0.ctx.ommers,
364                withdrawals: seg0.ctx.withdrawals.clone(),
365                extra_data: seg0.ctx.extra_data.clone(),
366                tx_count_hint: seg0.ctx.tx_count_hint,
367            };
368
369            let inner = self.inner_mut();
370            let evm_ctx = inner.evm.ctx_mut();
371            evm_ctx.block = block_env;
372            evm_ctx.cfg = cfg_env;
373            inner.ctx = seg0_ctx;
374
375            self.reseed_block_hashes_for(block_number);
376        }
377
378        self.inner_mut().apply_pre_execution_changes()
379    }
380
381    fn execute_transaction_without_commit(
382        &mut self,
383        tx: impl ExecutableTx<Self>,
384    ) -> Result<Self::Result, BlockExecutionError> {
385        self.maybe_apply_boundary()?;
386        self.inner_mut().execute_transaction_without_commit(tx)
387    }
388
389    fn commit_transaction(
390        &mut self,
391        output: Self::Result,
392    ) -> Result<GasOutput, BlockExecutionError> {
393        let gas_used = self.inner_mut().commit_transaction(output)?;
394
395        // Fix up cumulative_gas_used on the just-committed receipt so that
396        // the receipt root task (which reads receipts incrementally) sees
397        // globally-correct values across all segments.
398        let offset = self.gas_used_offset;
399        if offset > 0 &&
400            let Some(receipt) = self.inner_mut().receipts.last_mut()
401        {
402            receipt.cumulative_gas_used += offset;
403        }
404
405        if let Some(plan) = &mut self.plan {
406            plan.tx_counter += 1;
407        }
408        Ok(gas_used)
409    }
410
411    fn finish(
412        mut self,
413    ) -> Result<(Self::Evm, BlockExecutionResult<Self::Receipt>), BlockExecutionError> {
414        // Swap the inner executor's ctx to the last segment's ctx so that
415        // EthBlockExecutor::finish() applies the correct withdrawal balance
416        // increments and post-execution system calls.
417        if let Some(last_seg) = self.plan.as_ref().map(|p| p.segments.last().unwrap()) {
418            let last_ctx = EthBlockExecutionCtx {
419                parent_hash: last_seg.ctx.parent_hash,
420                parent_beacon_block_root: last_seg.ctx.parent_beacon_block_root,
421                ommers: last_seg.ctx.ommers,
422                withdrawals: last_seg.ctx.withdrawals.clone(),
423                extra_data: last_seg.ctx.extra_data.clone(),
424                tx_count_hint: last_seg.ctx.tx_count_hint,
425            };
426            self.inner_mut().ctx = last_ctx;
427        }
428        let inner = self.inner.take().expect("inner executor must exist");
429        let (evm, mut result) = inner.finish()?;
430
431        // Receipts already have globally-correct cumulative_gas_used (fixed
432        // up in commit_transaction). Add the offset to the totals so they
433        // reflect gas across all segments.
434        let last_segment_gas = result.gas_used;
435        result.gas_used += self.gas_used_offset;
436        result.blob_gas_used += self.blob_gas_used_offset;
437
438        let last_receipt_cumulative =
439            result.receipts.last().map(|r| r.cumulative_gas_used).unwrap_or(0);
440        debug!(
441            target: "engine::bb::evm",
442            last_segment_gas,
443            gas_used_offset = self.gas_used_offset,
444            total_gas_used = result.gas_used,
445            last_receipt_cumulative,
446            receipt_count = result.receipts.len(),
447            "Finished final segment"
448        );
449
450        // Merge requests accumulated from earlier segment boundaries into
451        // the final result.
452        if !self.accumulated_requests.is_empty() {
453            let mut merged = self.accumulated_requests;
454            merged.extend(result.requests);
455            result.requests = merged;
456        }
457
458        Ok((evm, result))
459    }
460
461    fn set_state_hook(&mut self, hook: Option<Box<dyn OnStateHook>>) {
462        if self.plan.is_some() {
463            // Store the real hook in the shared slot and give the inner
464            // executor a lightweight forwarder. This way the hook survives
465            // inner executor finish/reconstruct cycles at segment boundaries.
466            *self.shared_hook.lock().unwrap() = hook;
467            let fwd = self.forwarding_hook();
468            self.inner_mut().set_state_hook(fwd);
469        } else {
470            self.inner_mut().set_state_hook(hook);
471        }
472    }
473
474    fn evm_mut(&mut self) -> &mut Self::Evm {
475        self.inner_mut().evm_mut()
476    }
477
478    fn evm(&self) -> &Self::Evm {
479        self.inner().evm()
480    }
481
482    fn receipts(&self) -> &[Self::Receipt] {
483        self.inner().receipts()
484    }
485}
486
487// ---------------------------------------------------------------------------
488// BbBlockExecutorFactory
489// ---------------------------------------------------------------------------
490
491/// Block executor factory that produces [`BbBlockExecutor`] for
492/// boundary-aware big-block execution.
493#[derive(Debug, Clone)]
494pub struct BbBlockExecutorFactory<Spec> {
495    receipt_builder: RethReceiptBuilder,
496    spec: Spec,
497    evm_factory: EthEvmFactory,
498    /// Staged plan consumed by the next [`BbBlockExecutor`].
499    pub(crate) staged_plan: Arc<Mutex<Option<BbEvmPlan>>>,
500}
501
502impl<Spec> BbBlockExecutorFactory<Spec> {
503    pub fn new(
504        receipt_builder: RethReceiptBuilder,
505        spec: Spec,
506        evm_factory: EthEvmFactory,
507    ) -> Self {
508        Self { receipt_builder, spec, evm_factory, staged_plan: Arc::new(Mutex::new(None)) }
509    }
510
511    pub const fn evm_factory(&self) -> &EthEvmFactory {
512        &self.evm_factory
513    }
514
515    pub const fn spec(&self) -> &Spec {
516        &self.spec
517    }
518
519    pub const fn receipt_builder(&self) -> &RethReceiptBuilder {
520        &self.receipt_builder
521    }
522
523    pub(crate) fn stage_plan(&self, plan: BbEvmPlan) {
524        *self.staged_plan.lock().unwrap() = Some(plan);
525    }
526
527    fn take_plan(&self) -> Option<BbEvmPlan> {
528        self.staged_plan.lock().unwrap().take()
529    }
530
531    pub(crate) fn create_executor_with_seeder<'a, DB, I>(
532        &'a self,
533        evm: EthEvm<DB, I, PrecompilesMap>,
534        ctx: EthBlockExecutionCtx<'a>,
535        block_hash_seeder: Option<BlockHashSeeder<DB>>,
536    ) -> BbBlockExecutor<'a, DB, I, PrecompilesMap, &'a Spec>
537    where
538        Spec: alloy_evm::eth::spec::EthExecutorSpec,
539        DB: StateDB + 'a,
540        I: Inspector<EthEvmContext<DB>> + 'a,
541    {
542        let plan = self.take_plan();
543        BbBlockExecutor::new(evm, ctx, &self.spec, self.receipt_builder, plan, block_hash_seeder)
544    }
545}
546
547impl<Spec> BlockExecutorFactory for BbBlockExecutorFactory<Spec>
548where
549    Spec: alloy_evm::eth::spec::EthExecutorSpec + 'static,
550    TxEnv: FromRecoveredTx<TransactionSigned> + FromTxWithEncoded<TransactionSigned>,
551{
552    type EvmFactory = EthEvmFactory;
553    type ExecutionCtx<'a> = EthBlockExecutionCtx<'a>;
554    type Transaction = TransactionSigned;
555    type Receipt = Receipt;
556
557    fn evm_factory(&self) -> &Self::EvmFactory {
558        &self.evm_factory
559    }
560
561    fn create_executor<'a, DB, I>(
562        &'a self,
563        evm: EthEvm<DB, I, PrecompilesMap>,
564        ctx: EthBlockExecutionCtx<'a>,
565    ) -> impl BlockExecutorFor<'a, Self, DB, I>
566    where
567        DB: StateDB + 'a,
568        I: Inspector<EthEvmContext<DB>> + 'a,
569    {
570        let plan = self.take_plan();
571        BbBlockExecutor::new(evm, ctx, &self.spec, self.receipt_builder, plan, None)
572    }
573}