1use 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#[derive(Default, Debug, Clone, PartialEq, Eq)]
40pub struct ExecutionArtifacts {
41 pub block_header: Sealed<Header>,
43 pub receipts: Vec<OpReceiptEnvelope>,
45}
46
47#[derive(Debug)]
50pub struct StatelessL2BlockExecutor<'a, F, H>
51where
52 F: TrieDBProvider,
53 H: TrieHinter,
54{
55 config: &'a RollupConfig,
57 trie_db: TrieDB<F, H>,
59 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 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 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 pub fn execute_payload(
108 &mut self,
109 payload: OpPayloadAttributes,
110 ) -> ExecutorResult<ExecutionArtifacts> {
111 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 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 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 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_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 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 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 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 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 if !is_isthmus && matches!(transaction, OpTxEnvelope::Eip7702(_)) {
226 return Err(ExecutorError::UnsupportedTransactionType(transaction.tx_type() as u8));
227 }
228
229 evm = evm
231 .modify()
232 .with_tx_env(Self::prepare_tx_env(&transaction, raw_transaction)?)
233 .build();
234
235 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 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 cumulative_gas_used += result.gas_used();
266
267 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 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(evm);
302
303 debug!(target: "client_executor", "Merging state transitions");
305 state.merge_transitions(BundleRetention::Reverts);
306
307 let bundle = state.take_bundle();
309
310 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 let mut withdrawals_root = self
327 .config
328 .is_canyon_active(payload.payload_attributes.timestamp)
329 .then_some(EMPTY_ROOT_HASH);
330
331 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 let logs_bloom = logs_bloom(receipts.iter().flat_map(|receipt| receipt.logs()));
339
340 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 calc_excess_blob_gas(parent_excess_blob_gas, parent_blob_gas_used, 0)
351 } else {
352 calc_excess_blob_gas(0, 0, 0)
354 };
355
356 (Some(0), Some(excess_blob_gas as u128))
357 })
358 .unwrap_or_default();
359
360 let encoded_base_fee_params = self
366 .config
367 .is_holocene_active(payload.payload_attributes.timestamp)
368 .then(|| encode_holocene_eip_1559_params(self.config, &payload))
369 .transpose()?
370 .unwrap_or_default();
371
372 let parent_hash = state.database.parent_block_header().seal();
374
375 let requests_hash = self
376 .config
377 .is_isthmus_active(payload.payload_attributes.timestamp)
378 .then_some(SHA256_EMPTY);
379
380 let header = Header {
382 parent_hash,
383 ommers_hash: EMPTY_OMMER_ROOT_HASH,
384 beneficiary: payload.payload_attributes.suggested_fee_recipient,
385 state_root,
386 transactions_root,
387 receipts_root,
388 withdrawals_root,
389 requests_hash,
390 logs_bloom,
391 difficulty: U256::ZERO,
392 number: block_number,
393 gas_limit,
394 gas_used: cumulative_gas_used,
395 timestamp: payload.payload_attributes.timestamp,
396 mix_hash: payload.payload_attributes.prev_randao,
397 nonce: Default::default(),
398 base_fee_per_gas: base_fee.try_into().ok(),
399 blob_gas_used,
400 excess_blob_gas: excess_blob_gas.and_then(|x| x.try_into().ok()),
401 parent_beacon_block_root: payload.payload_attributes.parent_beacon_block_root,
402 extra_data: encoded_base_fee_params,
403 }
404 .seal_slow();
405
406 info!(
407 target: "client_executor",
408 "Sealed new header | Hash: {header_hash} | State root: {state_root} | Transactions root: {transactions_root} | Receipts root: {receipts_root}",
409 header_hash = header.seal(),
410 state_root = header.state_root,
411 transactions_root = header.transactions_root,
412 receipts_root = header.receipts_root,
413 );
414
415 state.database.set_parent_block_header(header.clone());
417 Ok(ExecutionArtifacts { block_header: header, receipts })
418 }
419
420 pub fn compute_output_root(&mut self) -> ExecutorResult<B256> {
433 let parent_number = self.trie_db.parent_block_header().number;
434 let storage_root = Self::message_passer_account(&mut self.trie_db, parent_number)?;
435 let parent_header = self.trie_db.parent_block_header();
436
437 info!(
438 target: "client_executor",
439 "Computing output root | Version: {version} | State root: {state_root} | Storage root: {storage_root} | Block hash: {hash}",
440 version = OUTPUT_ROOT_VERSION,
441 state_root = self.trie_db.parent_block_header().state_root,
442 hash = parent_header.seal(),
443 );
444
445 let mut raw_output = [0u8; 128];
447 raw_output[31] = OUTPUT_ROOT_VERSION;
448 raw_output[32..64].copy_from_slice(parent_header.state_root.as_ref());
449 raw_output[64..96].copy_from_slice(storage_root.as_ref());
450 raw_output[96..128].copy_from_slice(parent_header.seal().as_ref());
451 let output_root = keccak256(raw_output);
452
453 info!(
454 target: "client_executor",
455 "Computed output root for block # {block_number} | Output root: {output_root}",
456 block_number = parent_number,
457 );
458
459 Ok(output_root)
461 }
462
463 fn compute_receipts_root(
473 receipts: &[OpReceiptEnvelope],
474 config: &RollupConfig,
475 timestamp: u64,
476 ) -> B256 {
477 if config.is_regolith_active(timestamp) && !config.is_canyon_active(timestamp) {
482 let receipts = receipts
483 .iter()
484 .cloned()
485 .map(|receipt| match receipt {
486 OpReceiptEnvelope::Deposit(mut deposit_receipt) => {
487 deposit_receipt.receipt.deposit_nonce = None;
488 OpReceiptEnvelope::Deposit(deposit_receipt)
489 }
490 _ => receipt,
491 })
492 .collect::<Vec<_>>();
493
494 ordered_trie_with_encoder(receipts.as_ref(), |receipt, mut buf| {
495 receipt.encode_2718(&mut buf)
496 })
497 .root()
498 } else {
499 ordered_trie_with_encoder(receipts, |receipt, mut buf| receipt.encode_2718(&mut buf))
500 .root()
501 }
502 }
503
504 fn compute_transactions_root(transactions: &[Bytes]) -> B256 {
512 ordered_trie_with_encoder(transactions, |tx, buf| buf.put_slice(tx.as_ref())).root()
513 }
514}
515
516#[cfg(test)]
517mod test {
518 use crate::test_utils::run_test_fixture;
519 use rstest::rstest;
520 use std::path::PathBuf;
521
522 #[rstest]
535 #[case::small_block(10311000)] #[case::small_block_2(10211000)] #[case::small_block_3(10215000)] #[case::medium_block_1(132795025)] #[case::medium_block_2(132796000)] #[case::medium_block_3(132797000)] #[case::medium_block_4(132798000)] #[case::medium_block_5(132799000)] #[tokio::test]
544 async fn test_statelessly_execute_block(#[case] block_number: u64) {
545 let fixture_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
546 .join("testdata")
547 .join(format!("block-{block_number}.tar.gz"));
548
549 run_test_fixture(fixture_dir).await;
550 }
551}