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)
353 } else {
354 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 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 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 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 state.database.set_parent_block_header(header.clone());
419 Ok(ExecutionArtifacts { block_header: header, receipts })
420 }
421
422 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 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 Ok(output_root)
463 }
464
465 fn compute_receipts_root(
475 receipts: &[OpReceiptEnvelope],
476 config: &RollupConfig,
477 timestamp: u64,
478 ) -> B256 {
479 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 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 #[rstest]
537 #[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]
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}