kona_executor/builder/
assemble.rs

1//! [Header] assembly logic for the [StatelessL2Builder].
2
3use super::StatelessL2Builder;
4use crate::{
5    ExecutorError, ExecutorResult, TrieDBError, TrieDBProvider,
6    util::encode_holocene_eip_1559_params,
7};
8use alloc::vec::Vec;
9use alloy_consensus::{EMPTY_OMMER_ROOT_HASH, Header, Sealed};
10use alloy_eips::{Encodable2718, eip7685::EMPTY_REQUESTS_HASH};
11use alloy_evm::{EvmFactory, block::BlockExecutionResult};
12use alloy_primitives::{B256, Sealable, U256, logs_bloom};
13use alloy_trie::EMPTY_ROOT_HASH;
14use kona_genesis::RollupConfig;
15use kona_mpt::{TrieHinter, ordered_trie_with_encoder};
16use kona_protocol::{OutputRoot, Predeploys};
17use op_alloy_consensus::OpReceiptEnvelope;
18use op_alloy_rpc_types_engine::OpPayloadAttributes;
19use revm::{context::BlockEnv, database::BundleState};
20
21impl<P, H, Evm> StatelessL2Builder<'_, P, H, Evm>
22where
23    P: TrieDBProvider,
24    H: TrieHinter,
25    Evm: EvmFactory,
26{
27    /// Seals the block executed from the given [OpPayloadAttributes] and [BlockEnv], returning the
28    /// computed [Header].
29    pub(crate) fn seal_block(
30        &mut self,
31        attrs: &OpPayloadAttributes,
32        parent_hash: B256,
33        block_env: &BlockEnv,
34        ex_result: &BlockExecutionResult<OpReceiptEnvelope>,
35        bundle: BundleState,
36    ) -> ExecutorResult<Sealed<Header>> {
37        let timestamp = block_env.timestamp.saturating_to::<u64>();
38
39        // Compute the roots for the block header.
40        let state_root = self.trie_db.state_root(&bundle)?;
41        let transactions_root = ordered_trie_with_encoder(
42            // SAFETY: The OP Stack protocol will never generate a payload attributes with an empty
43            // transactions field. Panicking here is the desired behavior, as it indicates a severe
44            // protocol violation.
45            attrs.transactions.as_ref().expect("Transactions must be non-empty"),
46            |tx, buf| buf.put_slice(tx.as_ref()),
47        )
48        .root();
49        let receipts_root = compute_receipts_root(&ex_result.receipts, self.config, timestamp);
50        let withdrawals_root = if self.config.is_isthmus_active(timestamp) {
51            Some(self.message_passer_account(block_env.number.saturating_to::<u64>())?)
52        } else if self.config.is_canyon_active(timestamp) {
53            Some(EMPTY_ROOT_HASH)
54        } else {
55            None
56        };
57
58        // Compute the logs bloom from the receipts generated during block execution.
59        let logs_bloom = logs_bloom(ex_result.receipts.iter().flat_map(|r| r.logs()));
60
61        // Compute Cancun fields, if active.
62        let (blob_gas_used, excess_blob_gas) = self
63            .config
64            .is_ecotone_active(timestamp)
65            .then_some((Some(0), Some(0)))
66            .unwrap_or_default();
67
68        // At holocene activation, the base fee parameters from the payload are placed
69        // into the Header's `extra_data` field.
70        //
71        // If the payload's `eip_1559_params` are equal to `0`, then the header's `extraData`
72        // field is set to the encoded canyon base fee parameters.
73        let encoded_base_fee_params = self
74            .config
75            .is_holocene_active(timestamp)
76            .then(|| encode_holocene_eip_1559_params(self.config, attrs))
77            .transpose()?
78            .unwrap_or_default();
79
80        // The requests hash on the OP Stack, if Isthmus is active, is always the empty SHA256 hash.
81        let requests_hash = self.config.is_isthmus_active(timestamp).then_some(EMPTY_REQUESTS_HASH);
82
83        // Construct the new header.
84        let header = Header {
85            parent_hash,
86            ommers_hash: EMPTY_OMMER_ROOT_HASH,
87            beneficiary: attrs.payload_attributes.suggested_fee_recipient,
88            state_root,
89            transactions_root,
90            receipts_root,
91            withdrawals_root,
92            requests_hash,
93            logs_bloom,
94            difficulty: U256::ZERO,
95            number: block_env.number.saturating_to::<u64>(),
96            gas_limit: attrs.gas_limit.ok_or(ExecutorError::MissingGasLimit)?,
97            gas_used: ex_result.gas_used,
98            timestamp,
99            mix_hash: attrs.payload_attributes.prev_randao,
100            nonce: Default::default(),
101            base_fee_per_gas: Some(block_env.basefee),
102            blob_gas_used,
103            excess_blob_gas: excess_blob_gas.and_then(|x| x.try_into().ok()),
104            parent_beacon_block_root: attrs.payload_attributes.parent_beacon_block_root,
105            extra_data: encoded_base_fee_params,
106        }
107        .seal_slow();
108
109        Ok(header)
110    }
111
112    /// Computes the current output root of the latest executed block, based on the parent header
113    /// and the underlying state trie.
114    ///
115    /// **CONSTRUCTION:**
116    /// ```text
117    /// output_root = keccak256(version_byte .. payload)
118    /// payload = state_root .. withdrawal_storage_root .. latest_block_hash
119    /// ```
120    pub fn compute_output_root(&mut self) -> ExecutorResult<B256> {
121        let parent_number = self.trie_db.parent_block_header().number;
122
123        info!(
124            target: "block_builder",
125            state_root = ?self.trie_db.parent_block_header().state_root,
126            block_number = parent_number,
127            "Computing output root",
128        );
129
130        let storage_root = self.message_passer_account(parent_number)?;
131        let parent_header = self.trie_db.parent_block_header();
132
133        // Construct the raw output and hash it.
134        let output_root_hash =
135            OutputRoot::from_parts(parent_header.state_root, storage_root, parent_header.seal())
136                .hash();
137
138        info!(
139            target: "block_builder",
140            block_number = parent_number,
141            output_root = ?output_root_hash,
142            "Computed output root",
143        );
144
145        // Hash the output and return
146        Ok(output_root_hash)
147    }
148
149    /// Fetches the L2 to L1 message passer account from the cache or underlying trie.
150    fn message_passer_account(&mut self, block_number: u64) -> Result<B256, TrieDBError> {
151        match self.trie_db.storage_roots().get(&Predeploys::L2_TO_L1_MESSAGE_PASSER) {
152            Some(storage_root) => Ok(storage_root.blind()),
153            None => Ok(self
154                .trie_db
155                .get_trie_account(&Predeploys::L2_TO_L1_MESSAGE_PASSER, block_number)?
156                .ok_or(TrieDBError::MissingAccountInfo)?
157                .storage_root),
158        }
159    }
160}
161
162/// Computes the receipts root from the given set of receipts.
163pub fn compute_receipts_root(
164    receipts: &[OpReceiptEnvelope],
165    config: &RollupConfig,
166    timestamp: u64,
167) -> B256 {
168    // There is a minor bug in op-geth and op-erigon where in the Regolith hardfork,
169    // the receipt root calculation does not inclide the deposit nonce in the
170    // receipt encoding. In the Regolith hardfork, we must strip the deposit nonce
171    // from the receipt encoding to match the receipt root calculation.
172    if config.is_regolith_active(timestamp) && !config.is_canyon_active(timestamp) {
173        let receipts = receipts
174            .iter()
175            .cloned()
176            .map(|receipt| match receipt {
177                OpReceiptEnvelope::Deposit(mut deposit_receipt) => {
178                    deposit_receipt.receipt.deposit_nonce = None;
179                    OpReceiptEnvelope::Deposit(deposit_receipt)
180                }
181                _ => receipt,
182            })
183            .collect::<Vec<_>>();
184
185        ordered_trie_with_encoder(receipts.as_ref(), |receipt, mut buf| {
186            receipt.encode_2718(&mut buf)
187        })
188        .root()
189    } else {
190        ordered_trie_with_encoder(receipts, |receipt, mut buf| receipt.encode_2718(&mut buf)).root()
191    }
192}