kona_executor/executor/
mod.rs

1//! A stateless block executor for the OP Stack.
2
3use crate::{
4    ExecutorError, ExecutorResult, TrieDBProvider,
5    constants::{L2_TO_L1_BRIDGE, OUTPUT_ROOT_VERSION, SHA256_EMPTY},
6    db::TrieDB,
7    errors::TrieDBError,
8    syscalls::{
9        ensure_create2_deployer_canyon, pre_block_beacon_root_contract_call,
10        pre_block_block_hash_contract_call,
11    },
12};
13use alloc::{string::ToString, vec::Vec};
14use alloy_consensus::{
15    EMPTY_OMMER_ROOT_HASH, EMPTY_ROOT_HASH, Header, Sealable, Sealed, Transaction,
16};
17use alloy_eips::eip2718::{Decodable2718, Encodable2718};
18use alloy_primitives::{B256, Bytes, Log, U256, keccak256, logs_bloom};
19use kona_genesis::RollupConfig;
20use kona_mpt::{TrieHinter, ordered_trie_with_encoder};
21use op_alloy_consensus::{OpReceiptEnvelope, OpTxEnvelope};
22use op_alloy_rpc_types_engine::OpPayloadAttributes;
23use revm::{
24    Evm,
25    db::{State, states::bundle_state::BundleRetention},
26    primitives::{EnvWithHandlerCfg, calc_excess_blob_gas},
27};
28
29mod builder;
30pub use builder::{KonaHandleRegister, StatelessL2BlockExecutorBuilder};
31
32mod env;
33
34mod util;
35use util::encode_holocene_eip_1559_params;
36
37/// The [ExecutionArtifacts] holds the produced block header and receipts from the execution of a
38/// block.
39#[derive(Default, Debug, Clone, PartialEq, Eq)]
40pub struct ExecutionArtifacts {
41    /// The block header.
42    pub block_header: Sealed<Header>,
43    /// The receipts generated during execution.
44    pub receipts: Vec<OpReceiptEnvelope>,
45}
46
47/// The block executor for the L2 client program. Operates off of a [TrieDB] backed [State],
48/// allowing for stateless block execution of OP Stack blocks.
49#[derive(Debug)]
50pub struct StatelessL2BlockExecutor<'a, F, H>
51where
52    F: TrieDBProvider,
53    H: TrieHinter,
54{
55    /// The [RollupConfig].
56    config: &'a RollupConfig,
57    /// The inner state database component.
58    trie_db: TrieDB<F, H>,
59    /// The [KonaHandleRegister] to use during execution.
60    handler_register: Option<KonaHandleRegister<F, H>>,
61}
62
63impl<'a, F, H> StatelessL2BlockExecutor<'a, F, H>
64where
65    F: TrieDBProvider,
66    H: TrieHinter,
67{
68    /// Constructs a new [StatelessL2BlockExecutorBuilder] with the given [RollupConfig].
69    pub fn builder(
70        config: &'a RollupConfig,
71        provider: F,
72        hinter: H,
73    ) -> StatelessL2BlockExecutorBuilder<'a, F, H> {
74        StatelessL2BlockExecutorBuilder::new(config, provider, hinter)
75    }
76
77    /// Fetches the L2 to L1 message passer account from the cache or underlying trie.
78    fn message_passer_account(
79        db: &mut TrieDB<F, H>,
80        block_number: u64,
81    ) -> Result<B256, TrieDBError> {
82        match db.storage_roots().get(&L2_TO_L1_BRIDGE) {
83            Some(storage_root) => Ok(storage_root.blind()),
84            None => Ok(db
85                .get_trie_account(&L2_TO_L1_BRIDGE, block_number)?
86                .ok_or(TrieDBError::MissingAccountInfo)?
87                .storage_root),
88        }
89    }
90
91    /// Executes the given block, returning the resulting state root.
92    ///
93    /// ## Steps
94    /// 1. Prepare the block environment.
95    /// 2. Apply the pre-block EIP-4788 contract call.
96    /// 3. Prepare the EVM with the given L2 execution payload in the block environment.
97    ///     - Reject any EIP-4844 transactions, as they are not supported on the OP Stack.
98    ///     - If the transaction is a deposit, cache the depositor account prior to execution.
99    ///     - Construct the EVM with the given configuration.
100    ///     - Execute the transaction.
101    ///     - Accumulate the gas used by the transaction to the block-scoped cumulative gas used
102    ///       counter.
103    ///     - Create a receipt envelope for the transaction.
104    /// 4. Merge all state transitions into the cache state.
105    /// 5. Compute the [state root, transactions root, receipts root, logs bloom] for the processed
106    ///    block.
107    pub fn execute_payload(
108        &mut self,
109        payload: OpPayloadAttributes,
110    ) -> ExecutorResult<ExecutionArtifacts> {
111        // Prepare the `revm` environment.
112        let base_fee_params = Self::active_base_fee_params(
113            self.config,
114            self.trie_db.parent_block_header(),
115            &payload,
116        )?;
117        let initialized_block_env = Self::prepare_block_env(
118            self.config.spec_id(payload.payload_attributes.timestamp),
119            self.trie_db.parent_block_header(),
120            &payload,
121            &base_fee_params,
122        )?;
123        let initialized_cfg = self.evm_cfg_env(payload.payload_attributes.timestamp);
124        let block_number = initialized_block_env.number.to::<u64>();
125        let base_fee = initialized_block_env.basefee.to::<u128>();
126        let gas_limit = payload.gas_limit.ok_or(ExecutorError::MissingGasLimit)?;
127        let transactions =
128            payload.transactions.as_ref().ok_or(ExecutorError::MissingTransactions)?;
129
130        info!(
131            target: "client_executor",
132            "Executing block # {block_number} | Gas limit: {gas_limit} | Tx count: {tx_len}",
133            block_number = block_number,
134            gas_limit = gas_limit,
135            tx_len = transactions.len(),
136        );
137
138        let parent_block_hash: B256 = self.trie_db.parent_block_header().seal();
139
140        // Attempt to send a payload witness hint to the host. This hint instructs the host to
141        // populate its preimage store with the preimages required to statelessly execute
142        // this payload. This feature is experimental, so if the hint fails, we continue
143        // without it and fall back on on-demand preimage fetching for execution.
144        self.trie_db
145            .hinter
146            .hint_execution_witness(parent_block_hash, &payload)
147            .map_err(|e| TrieDBError::Provider(e.to_string()))?;
148
149        let mut state =
150            State::builder().with_database(&mut self.trie_db).with_bundle_update().build();
151
152        // Apply the pre-block EIP-4788 contract call.
153        pre_block_beacon_root_contract_call(
154            &mut state,
155            self.config,
156            block_number,
157            &initialized_cfg,
158            &initialized_block_env,
159            &payload,
160        )?;
161
162        // Apply the pre-block EIP-2935 contract call.
163        pre_block_block_hash_contract_call(
164            &mut state,
165            self.config,
166            block_number,
167            &initialized_cfg,
168            &initialized_block_env,
169            parent_block_hash,
170            &payload,
171        )?;
172
173        // Ensure that the create2 contract is deployed upon transition to the Canyon hardfork.
174        ensure_create2_deployer_canyon(
175            &mut state,
176            self.config,
177            payload.payload_attributes.timestamp,
178        )?;
179
180        let mut cumulative_gas_used = 0u64;
181        let mut receipts: Vec<OpReceiptEnvelope> = Vec::with_capacity(transactions.len());
182        let is_regolith = self.config.is_regolith_active(payload.payload_attributes.timestamp);
183
184        // Construct the block-scoped EVM with the given configuration.
185        // The transaction environment is set within the loop for each transaction.
186        let mut evm = {
187            let mut base = Evm::builder().with_db(&mut state).with_env_with_handler_cfg(
188                EnvWithHandlerCfg::new_with_cfg_env(
189                    initialized_cfg.clone(),
190                    initialized_block_env.clone(),
191                    Default::default(),
192                ),
193            );
194
195            // If a handler register is provided, append it to the base EVM.
196            if let Some(handler) = self.handler_register {
197                base = base.append_handler_register(handler);
198            }
199
200            base.build()
201        };
202
203        let is_isthmus = self.config.is_isthmus_active(payload.payload_attributes.timestamp);
204
205        // Execute the transactions in the payload.
206        let decoded_txs = transactions
207            .iter()
208            .map(|raw_tx| {
209                let tx = OpTxEnvelope::decode_2718(&mut raw_tx.as_ref())
210                    .map_err(ExecutorError::RLPError)?;
211                Ok((tx, raw_tx.as_ref()))
212            })
213            .collect::<ExecutorResult<Vec<_>>>()?;
214        for (transaction, raw_transaction) in decoded_txs {
215            // The sum of the transaction’s gas limit, Tg, and the gas utilized in this block prior,
216            // must be no greater than the block’s gasLimit.
217            let block_available_gas = (gas_limit - cumulative_gas_used) as u128;
218            if (transaction.gas_limit() as u128) > block_available_gas &&
219                (is_regolith || !transaction.is_system_transaction())
220            {
221                return Err(ExecutorError::BlockGasLimitExceeded);
222            }
223
224            // Prevent EIP-7702 transactions pre-isthmus hardfork.
225            if !is_isthmus && matches!(transaction, OpTxEnvelope::Eip7702(_)) {
226                return Err(ExecutorError::UnsupportedTransactionType(transaction.tx_type() as u8));
227            }
228
229            // Modify the transaction environment with the current transaction.
230            evm = evm
231                .modify()
232                .with_tx_env(Self::prepare_tx_env(&transaction, raw_transaction)?)
233                .build();
234
235            // If the transaction is a deposit, cache the depositor account.
236            //
237            // This only needs to be done post-Regolith, as deposit nonces were not included in
238            // Bedrock. In addition, non-deposit transactions do not have deposit
239            // nonces.
240            let depositor = is_regolith
241                .then(|| {
242                    if let OpTxEnvelope::Deposit(deposit) = &transaction {
243                        evm.db_mut().load_cache_account(deposit.from).ok().cloned()
244                    } else {
245                        None
246                    }
247                })
248                .flatten();
249
250            // Execute the transaction.
251            let tx_hash = keccak256(raw_transaction);
252            debug!(
253                target: "client_executor",
254                "Executing transaction: {tx_hash}",
255            );
256            let result = evm.transact_commit().map_err(ExecutorError::ExecutionError)?;
257            debug!(
258                target: "client_executor",
259                "Transaction executed: {tx_hash} | Gas used: {gas_used} | Success: {status}",
260                gas_used = result.gas_used(),
261                status = result.is_success()
262            );
263
264            // Accumulate the gas used by the transaction.
265            cumulative_gas_used += result.gas_used();
266
267            // Create receipt envelope.
268            let receipt = OpReceiptEnvelope::<Log>::from_parts(
269                result.is_success(),
270                cumulative_gas_used,
271                result.logs(),
272                transaction.tx_type(),
273                depositor
274                    .as_ref()
275                    .map(|depositor| depositor.account_info().unwrap_or_default().nonce),
276                depositor
277                    .is_some()
278                    .then(|| {
279                        self.config
280                            .is_canyon_active(payload.payload_attributes.timestamp)
281                            .then_some(1)
282                    })
283                    .flatten(),
284            );
285            // Ensure the receipt is not an EIP-7702 receipt.
286            if matches!(receipt, OpReceiptEnvelope::Eip7702(_)) && !is_isthmus {
287                panic!(
288                    "EIP-7702 receipts are not supported by the fault proof program before Isthmus"
289                );
290            }
291            receipts.push(receipt);
292        }
293
294        info!(
295            target: "client_executor",
296            "Transaction execution complete | Cumulative gas used: {cumulative_gas_used}",
297            cumulative_gas_used = cumulative_gas_used
298        );
299
300        // Drop the EVM to free the exclusive reference to the database.
301        drop(evm);
302
303        // Merge all state transitions into the cache state.
304        debug!(target: "client_executor", "Merging state transitions");
305        state.merge_transitions(BundleRetention::Reverts);
306
307        // Take the bundle state.
308        let bundle = state.take_bundle();
309
310        // Recompute the header roots.
311        let state_root = state.database.state_root(&bundle)?;
312
313        let transactions_root = Self::compute_transactions_root(transactions.as_slice());
314        let receipts_root = Self::compute_receipts_root(
315            &receipts,
316            self.config,
317            payload.payload_attributes.timestamp,
318        );
319        debug!(
320            target: "client_executor",
321            "Computed transactions root: {transactions_root} | receipts root: {receipts_root}",
322        );
323
324        // The withdrawals root on OP Stack chains, after Canyon activation, is always the empty
325        // root hash.
326        let mut withdrawals_root = self
327            .config
328            .is_canyon_active(payload.payload_attributes.timestamp)
329            .then_some(EMPTY_ROOT_HASH);
330
331        // If the Isthmus hardfork is active, the withdrawals root is the L2 to L1 message passer
332        // account.
333        if self.config.is_isthmus_active(payload.payload_attributes.timestamp) {
334            withdrawals_root = Some(Self::message_passer_account(state.database, block_number)?);
335        }
336
337        // Compute logs bloom filter for the block.
338        let logs_bloom = logs_bloom(receipts.iter().flat_map(|receipt| receipt.logs()));
339
340        // Compute Cancun fields, if active.
341        let (blob_gas_used, excess_blob_gas) = self
342            .config
343            .is_ecotone_active(payload.payload_attributes.timestamp)
344            .then(|| {
345                let parent_header = state.database.parent_block_header();
346                let excess_blob_gas = if self.config.is_ecotone_active(parent_header.timestamp) {
347                    let parent_excess_blob_gas = parent_header.excess_blob_gas.unwrap_or_default();
348                    let parent_blob_gas_used = parent_header.blob_gas_used.unwrap_or_default();
349
350                    // TODO(isthmus): Consider the final field for EIP-7742. Since this EIP isn't
351                    // implemented yet, we can safely ignore it for now.
352                    calc_excess_blob_gas(parent_excess_blob_gas, parent_blob_gas_used, 0)
353                } else {
354                    // For the first post-fork block, both blob gas fields are evaluated to 0.
355                    calc_excess_blob_gas(0, 0, 0)
356                };
357
358                (Some(0), Some(excess_blob_gas as u128))
359            })
360            .unwrap_or_default();
361
362        // At holocene activation, the base fee parameters from the payload are placed
363        // into the Header's `extra_data` field.
364        //
365        // If the payload's `eip_1559_params` are equal to `0`, then the header's `extraData`
366        // field is set to the encoded canyon base fee parameters.
367        let encoded_base_fee_params = self
368            .config
369            .is_holocene_active(payload.payload_attributes.timestamp)
370            .then(|| encode_holocene_eip_1559_params(self.config, &payload))
371            .transpose()?
372            .unwrap_or_default();
373
374        // Compute the parent hash.
375        let parent_hash = state.database.parent_block_header().seal();
376
377        let requests_hash = self
378            .config
379            .is_isthmus_active(payload.payload_attributes.timestamp)
380            .then_some(SHA256_EMPTY);
381
382        // Construct the new header.
383        let header = Header {
384            parent_hash,
385            ommers_hash: EMPTY_OMMER_ROOT_HASH,
386            beneficiary: payload.payload_attributes.suggested_fee_recipient,
387            state_root,
388            transactions_root,
389            receipts_root,
390            withdrawals_root,
391            requests_hash,
392            logs_bloom,
393            difficulty: U256::ZERO,
394            number: block_number,
395            gas_limit,
396            gas_used: cumulative_gas_used,
397            timestamp: payload.payload_attributes.timestamp,
398            mix_hash: payload.payload_attributes.prev_randao,
399            nonce: Default::default(),
400            base_fee_per_gas: base_fee.try_into().ok(),
401            blob_gas_used,
402            excess_blob_gas: excess_blob_gas.and_then(|x| x.try_into().ok()),
403            parent_beacon_block_root: payload.payload_attributes.parent_beacon_block_root,
404            extra_data: encoded_base_fee_params,
405        }
406        .seal_slow();
407
408        info!(
409            target: "client_executor",
410            "Sealed new header | Hash: {header_hash} | State root: {state_root} | Transactions root: {transactions_root} | Receipts root: {receipts_root}",
411            header_hash = header.seal(),
412            state_root = header.state_root,
413            transactions_root = header.transactions_root,
414            receipts_root = header.receipts_root,
415        );
416
417        // Update the parent block hash in the state database.
418        state.database.set_parent_block_header(header.clone());
419        Ok(ExecutionArtifacts { block_header: header, receipts })
420    }
421
422    /// Computes the current output root of the executor, based on the parent header and the
423    /// state's underlying trie.
424    ///
425    /// **CONSTRUCTION:**
426    /// ```text
427    /// output_root = keccak256(version_byte .. payload)
428    /// payload = state_root .. withdrawal_storage_root .. latest_block_hash
429    /// ```
430    ///
431    /// ## Returns
432    /// - `Ok(output_root)`: The computed output root.
433    /// - `Err(_)`: If an error occurred while computing the output root.
434    pub fn compute_output_root(&mut self) -> ExecutorResult<B256> {
435        let parent_number = self.trie_db.parent_block_header().number;
436        let storage_root = Self::message_passer_account(&mut self.trie_db, parent_number)?;
437        let parent_header = self.trie_db.parent_block_header();
438
439        info!(
440            target: "client_executor",
441            "Computing output root | Version: {version} | State root: {state_root} | Storage root: {storage_root} | Block hash: {hash}",
442            version = OUTPUT_ROOT_VERSION,
443            state_root = self.trie_db.parent_block_header().state_root,
444            hash = parent_header.seal(),
445        );
446
447        // Construct the raw output.
448        let mut raw_output = [0u8; 128];
449        raw_output[31] = OUTPUT_ROOT_VERSION;
450        raw_output[32..64].copy_from_slice(parent_header.state_root.as_ref());
451        raw_output[64..96].copy_from_slice(storage_root.as_ref());
452        raw_output[96..128].copy_from_slice(parent_header.seal().as_ref());
453        let output_root = keccak256(raw_output);
454
455        info!(
456            target: "client_executor",
457            "Computed output root for block # {block_number} | Output root: {output_root}",
458            block_number = parent_number,
459        );
460
461        // Hash the output and return
462        Ok(output_root)
463    }
464
465    /// Computes the receipts root from the given set of receipts.
466    ///
467    /// ## Takes
468    /// - `receipts`: The receipts to compute the root for.
469    /// - `config`: The rollup config to use for the computation.
470    /// - `timestamp`: The timestamp to use for the computation.
471    ///
472    /// ## Returns
473    /// The computed receipts root.
474    fn compute_receipts_root(
475        receipts: &[OpReceiptEnvelope],
476        config: &RollupConfig,
477        timestamp: u64,
478    ) -> B256 {
479        // There is a minor bug in op-geth and op-erigon where in the Regolith hardfork,
480        // the receipt root calculation does not inclide the deposit nonce in the
481        // receipt encoding. In the Regolith hardfork, we must strip the deposit nonce
482        // from the receipt encoding to match the receipt root calculation.
483        if config.is_regolith_active(timestamp) && !config.is_canyon_active(timestamp) {
484            let receipts = receipts
485                .iter()
486                .cloned()
487                .map(|receipt| match receipt {
488                    OpReceiptEnvelope::Deposit(mut deposit_receipt) => {
489                        deposit_receipt.receipt.deposit_nonce = None;
490                        OpReceiptEnvelope::Deposit(deposit_receipt)
491                    }
492                    _ => receipt,
493                })
494                .collect::<Vec<_>>();
495
496            ordered_trie_with_encoder(receipts.as_ref(), |receipt, mut buf| {
497                receipt.encode_2718(&mut buf)
498            })
499            .root()
500        } else {
501            ordered_trie_with_encoder(receipts, |receipt, mut buf| receipt.encode_2718(&mut buf))
502                .root()
503        }
504    }
505
506    /// Computes the transactions root from the given set of encoded transactions.
507    ///
508    /// ## Takes
509    /// - `transactions`: The transactions to compute the root for.
510    ///
511    /// ## Returns
512    /// The computed transactions root.
513    fn compute_transactions_root(transactions: &[Bytes]) -> B256 {
514        ordered_trie_with_encoder(transactions, |tx, buf| buf.put_slice(tx.as_ref())).root()
515    }
516}
517
518#[cfg(test)]
519mod test {
520    use crate::test_utils::run_test_fixture;
521    use rstest::rstest;
522    use std::path::PathBuf;
523
524    // To create new test fixtures, uncomment the following test and run it with parameters filled.
525    //
526    // #[tokio::test(flavor = "multi_thread")]
527    // async fn create_fixture() {
528    //     let fixture_creator = crate::test_utils::ExecutorTestFixtureCreator::new(
529    //         "<l2_archive_el_rpc_url>",
530    //         <block_number>,
531    //         PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("testdata"),
532    //     );
533    //     fixture_creator.create_static_fixture().await;
534    // }
535
536    #[rstest]
537    #[case::small_block(10311000)] // Unichain Mainnet
538    #[case::small_block_2(10211000)] // Unichain Mainnet
539    #[case::small_block_3(10215000)] // Unichain Mainnet
540    #[case::medium_block_1(132795025)] // OP Mainnet
541    #[case::medium_block_2(132796000)] // OP Mainnet
542    #[case::medium_block_3(132797000)] // OP Mainnet
543    #[case::medium_block_4(132798000)] // OP Mainnet
544    #[case::medium_block_5(132799000)] // OP Mainnet
545    #[tokio::test]
546    async fn test_statelessly_execute_block(#[case] block_number: u64) {
547        let fixture_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
548            .join("testdata")
549            .join(format!("block-{block_number}.tar.gz"));
550
551        run_test_fixture(fixture_dir).await;
552    }
553}