1use alloy_consensus::{Transaction, TransactionEnvelope, TxReceipt};
2use alloy_eips::eip2718::{Encodable2718, Typed2718};
3use alloy_evm::{
4 Database, Evm, EvmFactory, RecoveredTx,
5 block::{
6 BlockExecutionError, BlockExecutionResult, BlockExecutor, BlockExecutorFactory,
7 BlockExecutorFor, ExecutableTx, OnStateHook, StateDB,
8 },
9 eth::{
10 EthBlockExecutionCtx, EthBlockExecutor, EthTxResult, receipt_builder::ReceiptBuilder,
11 spec::EthExecutorSpec,
12 },
13 tx::{FromRecoveredTx, FromTxWithEncoded},
14};
15use alloy_primitives::{Address, B256, Log, TxKind, U256, keccak256};
16use arb_chainspec;
17use arb_primitives::{
18 multigas::{MultiGas, NUM_RESOURCE_KIND},
19 signed_tx::ArbTransactionExt,
20 tx_types::ArbTxType,
21};
22use arbos::{
23 arbos_state::ArbosState,
24 burn::SystemBurner,
25 internal_tx::{self, InternalTxContext},
26 l1_pricing, retryables,
27 tx_processor::{
28 EndTxFeeDistribution, EndTxRetryableParams, SubmitRetryableParams, compute_poster_gas,
29 compute_submit_retryable_fees,
30 },
31 util::{self as arb_util, BalanceError, tx_type_has_poster_costs},
32};
33use reth_evm::TransactionEnvMut;
34use revm::{
35 context::{TxEnv, result::ExecutionResult},
36 database::State,
37 inspector::Inspector,
38};
39
40use crate::{
41 context::ArbBlockExecutionCtx,
42 executor::DefaultArbOsHooks,
43 hooks::{ArbOsHooks, EndTxContext},
44 state_overlay::StateOverlay,
45};
46
47pub trait ArbTransactionEnv: TransactionEnvMut {
52 fn set_gas_price(&mut self, gas_price: u128);
54 fn set_gas_priority_fee(&mut self, fee: Option<u128>);
56 fn set_value(&mut self, value: U256);
58}
59
60impl ArbTransactionEnv for TxEnv {
61 fn set_gas_price(&mut self, gas_price: u128) {
62 self.gas_price = gas_price;
63 }
64 fn set_gas_priority_fee(&mut self, fee: Option<u128>) {
65 self.gas_priority_fee = fee;
66 }
67 fn set_value(&mut self, value: U256) {
68 self.value = value;
69 }
70}
71
72pub trait ArbScheduledTxDrain {
78 fn drain_scheduled_txs(&mut self) -> Vec<Vec<u8>>;
81}
82
83impl<'a, Evm, Spec, R: ReceiptBuilder> ArbScheduledTxDrain for ArbBlockExecutor<'a, Evm, Spec, R> {
84 fn drain_scheduled_txs(&mut self) -> Vec<Vec<u8>> {
85 self.arb_hooks
86 .as_mut()
87 .map(|hooks| std::mem::take(&mut hooks.tx_proc.scheduled_txs))
88 .unwrap_or_default()
89 }
90}
91
92#[derive(Debug, Clone)]
97pub struct ArbBlockExecutorFactory<R, Spec, EvmF> {
98 receipt_builder: R,
99 spec: Spec,
100 evm_factory: EvmF,
101 allow_debug_precompiles: bool,
102}
103
104impl<R, Spec, EvmF> ArbBlockExecutorFactory<R, Spec, EvmF> {
105 pub fn new(receipt_builder: R, spec: Spec, evm_factory: EvmF) -> Self {
106 Self {
107 receipt_builder,
108 spec,
109 evm_factory,
110 allow_debug_precompiles: false,
111 }
112 }
113
114 pub fn with_allow_debug_precompiles(mut self, allow: bool) -> Self {
115 self.allow_debug_precompiles = allow;
116 self
117 }
118
119 pub fn allow_debug_precompiles(&self) -> bool {
120 self.allow_debug_precompiles
121 }
122
123 pub fn arb_evm_factory(&self) -> &EvmF {
124 &self.evm_factory
125 }
126
127 pub fn create_arb_executor<'a, DB, I>(
132 &'a self,
133 evm: EvmF::Evm<&'a mut State<DB>, I>,
134 ctx: EthBlockExecutionCtx<'a>,
135 chain_id: u64,
136 ) -> ArbBlockExecutor<'a, EvmF::Evm<&'a mut State<DB>, I>, &'a Spec, &'a R>
137 where
138 DB: Database + 'a,
139 R: ReceiptBuilder,
140 Spec: EthExecutorSpec + Clone,
141 I: Inspector<EvmF::Context<&'a mut State<DB>>> + 'a,
142 EvmF: EvmFactory + crate::evm::ArbEvmFactoryStaged,
143 {
144 let extra_bytes = ctx.extra_data.as_ref();
145 let (delayed_messages_read, l2_block_number) = decode_extra_fields(extra_bytes);
146 let arb_ctx = ArbBlockExecutionCtx {
147 parent_hash: ctx.parent_hash,
148 parent_beacon_block_root: ctx.parent_beacon_block_root,
149 extra_data: extra_bytes[..core::cmp::min(extra_bytes.len(), 32)].to_vec(),
150 delayed_messages_read,
151 l2_block_number,
152 chain_id,
153 ..Default::default()
154 };
155 ArbBlockExecutor {
156 inner: EthBlockExecutor::new(evm, ctx, &self.spec, &self.receipt_builder),
157 arb_hooks: None,
158 arb_ctx,
159 precompile_ctx: self.evm_factory.staged_precompile_ctx().unwrap_or_default(),
163 pending_tx: None,
164 block_gas_left: 0,
165 user_txs_processed: 0,
166 gas_used_for_l1: Vec::new(),
167 multi_gas_used: Vec::new(),
168 expected_balance_delta: 0,
169 zombie_accounts: rustc_hash::FxHashSet::default(),
170 finalise_deleted: rustc_hash::FxHashSet::default(),
171 touched_accounts: rustc_hash::FxHashSet::default(),
172 multi_gas_current_fees: std::sync::OnceLock::new(),
173 state_overlay: StateOverlay::new(),
174 multi_gas_sink: crate::multi_gas::MultiGasSink::default(),
175 }
176 }
177}
178
179impl<R, Spec, EvmF> BlockExecutorFactory for ArbBlockExecutorFactory<R, Spec, EvmF>
180where
181 R: ReceiptBuilder<
182 Transaction: Transaction + Encodable2718 + ArbTransactionExt,
183 Receipt: TxReceipt<Log = Log> + arb_primitives::SetArbReceiptFields,
184 > + 'static,
185 Spec: EthExecutorSpec + Clone + 'static,
186 EvmF: EvmFactory<
187 Tx: FromRecoveredTx<R::Transaction>
188 + FromTxWithEncoded<R::Transaction>
189 + ArbTransactionEnv,
190 > + crate::evm::ArbEvmFactoryStaged,
191 Self: 'static,
192{
193 type EvmFactory = EvmF;
194 type ExecutionCtx<'a> = EthBlockExecutionCtx<'a>;
195 type Transaction = R::Transaction;
196 type Receipt = R::Receipt;
197
198 fn evm_factory(&self) -> &Self::EvmFactory {
199 &self.evm_factory
200 }
201
202 fn create_executor<'a, DB, I>(
203 &'a self,
204 _evm: EvmF::Evm<DB, I>,
205 _ctx: Self::ExecutionCtx<'a>,
206 ) -> impl BlockExecutorFor<'a, Self, DB, I>
207 where
208 DB: StateDB + 'a,
209 I: Inspector<EvmF::Context<DB>> + 'a,
210 {
211 unreachable!(
212 "BlockExecutorFactory::create_executor must not be called directly on \
213 ArbBlockExecutorFactory; all execution goes through the \
214 ConfigureEvm::create_executor override on ArbEvmConfig"
215 );
216 #[allow(unreachable_code)]
217 EthBlockExecutor::new(_evm, _ctx, &self.spec, &self.receipt_builder)
218 }
219}
220
221struct PendingArbTx {
227 sender: Address,
228 tx_gas_limit: u64,
229 arb_tx_type: Option<ArbTxType>,
230 poster_gas: u64,
231 evm_gas_used: u64,
233 charged_multi_gas: MultiGas,
234 gas_price_positive: bool,
235 stylus_data_fee: U256,
236 retry_context: Option<PendingRetryContext>,
237 coinbase_tip_per_gas: u128,
238 capped_gas_price: bool,
242 actual_gas_price: U256,
246}
247
248struct PendingRetryContext {
250 ticket_id: alloy_primitives::B256,
251 refund_to: Address,
252 max_refund: U256,
253 submission_fee_refund: U256,
254 call_value: U256,
256}
257
258pub struct ArbBlockExecutor<'a, Evm, Spec, R: ReceiptBuilder> {
266 pub inner: EthBlockExecutor<'a, Evm, Spec, R>,
268 pub arb_hooks: Option<DefaultArbOsHooks>,
270 pub arb_ctx: ArbBlockExecutionCtx,
272 pub precompile_ctx: std::sync::Arc<arb_context::ArbPrecompileCtx>,
274 pending_tx: Option<PendingArbTx>,
276 pub block_gas_left: u64,
279 user_txs_processed: u64,
282 pub gas_used_for_l1: Vec<u64>,
285 pub multi_gas_used: Vec<MultiGas>,
287 expected_balance_delta: i128,
290 zombie_accounts: rustc_hash::FxHashSet<Address>,
293 finalise_deleted: rustc_hash::FxHashSet<Address>,
296 touched_accounts: rustc_hash::FxHashSet<Address>,
299 multi_gas_current_fees: std::sync::OnceLock<[U256; NUM_RESOURCE_KIND]>,
306 state_overlay: StateOverlay,
310 multi_gas_sink: crate::multi_gas::MultiGasSink,
314}
315
316impl<'a, Evm, Spec, R: ReceiptBuilder> ArbBlockExecutor<'a, Evm, Spec, R> {
317 pub fn with_hooks(mut self, hooks: DefaultArbOsHooks) -> Self {
319 self.arb_hooks = Some(hooks);
320 self
321 }
322
323 pub fn with_arb_ctx(mut self, ctx: ArbBlockExecutionCtx) -> Self {
325 self.arb_ctx = ctx;
326 self
327 }
328
329 pub fn set_multi_gas_sink(&mut self, sink: crate::multi_gas::MultiGasSink) {
332 self.multi_gas_sink = sink;
333 }
334
335 pub fn zombie_accounts(&self) -> rustc_hash::FxHashSet<Address> {
341 self.zombie_accounts.clone()
342 }
343
344 pub fn finalise_deleted(&self) -> &rustc_hash::FxHashSet<Address> {
347 &self.finalise_deleted
348 }
349
350 pub fn deduct_failed_tx_gas(&mut self, is_user_tx: bool) {
355 const TX_GAS: u64 = 21_000;
356 self.block_gas_left = self.block_gas_left.saturating_sub(TX_GAS);
357 if is_user_tx {
358 self.user_txs_processed += 1;
359 }
360 }
361
362 pub fn drain_scheduled_txs(&mut self) -> Vec<Vec<u8>> {
366 self.arb_hooks
367 .as_mut()
368 .map(|hooks| std::mem::take(&mut hooks.tx_proc.scheduled_txs))
369 .unwrap_or_default()
370 }
371}
372
373fn load_state_params<D: Database>(
378 arb_ctx: &mut ArbBlockExecutionCtx,
379 precompile_ctx: &mut std::sync::Arc<arb_context::ArbPrecompileCtx>,
380 arb_hooks: &mut Option<DefaultArbOsHooks>,
381 state: &mut revm::database::State<D>,
382 arb_state: &ArbosState<D, impl arbos::burn::Burner>,
383) {
384 let arbos_version = arb_state.arbos_version();
385 arb_ctx.arbos_version = arbos_version;
386
387 precompile_ctx.reset_tx();
394 precompile_ctx.reset_caller_stack();
395 precompile_ctx
396 .block
397 .cache_l1_block_number(arb_ctx.l2_block_number, arb_ctx.l1_block_number);
398
399 if arbos_version >= arb_chainspec::arbos_version::ARBOS_VERSION_60 {
400 let cap = arb_state
401 .programs
402 .params(state)
403 .map(|p| p.block_cache_size as usize)
404 .unwrap_or(0);
405 precompile_ctx.block.reset_recent_wasms(cap);
406 } else {
407 precompile_ctx.block.reset_recent_wasms(0);
408 }
409
410 if let Ok(backlog) = arb_state.l2_pricing_state.gas_backlog(state) {
411 precompile_ctx.block.set_current_gas_backlog(backlog);
412 }
413
414 if let Ok(addr) = arb_state.network_fee_account(state) {
415 arb_ctx.network_fee_account = addr;
416 }
417 if let Ok(addr) = arb_state.infra_fee_account(state) {
418 arb_ctx.infra_fee_account = addr;
419 }
420 if let Ok(level) = arb_state.brotli_compression_level(state) {
421 arb_ctx.brotli_compression_level = level;
422 }
423 if let Ok(price) = arb_state.l1_pricing_state.price_per_unit(state) {
424 arb_ctx.l1_price_per_unit = price;
425 }
426 if let Ok(min_fee) = arb_state.l2_pricing_state.min_base_fee_wei(state) {
427 arb_ctx.min_base_fee = min_fee;
428 }
429
430 let per_block_gas_limit = arb_state
431 .l2_pricing_state
432 .per_block_gas_limit(state)
433 .unwrap_or(0);
434 let per_tx_gas_limit = arb_state
435 .l2_pricing_state
436 .per_tx_gas_limit(state)
437 .unwrap_or(0);
438
439 let calldata_pricing_increase_enabled = arbos_version
440 >= arb_chainspec::arbos_version::ARBOS_VERSION_40
441 && arb_state
442 .features
443 .is_increased_calldata_price_enabled(state)
444 .unwrap_or(false);
445
446 let collect_tips_enabled = arb_state.collect_tips(state).unwrap_or(false);
447
448 let hooks = DefaultArbOsHooks::new(
449 arb_ctx.coinbase,
450 arbos_version,
451 arb_ctx.network_fee_account,
452 arb_ctx.infra_fee_account,
453 arb_ctx.min_base_fee,
454 per_block_gas_limit,
455 per_tx_gas_limit,
456 false,
457 arb_ctx.l1_base_fee,
458 calldata_pricing_increase_enabled,
459 collect_tips_enabled,
460 );
461 *arb_hooks = Some(hooks);
462}
463
464fn populate_l2_block_hash_window(
468 block: &arb_context::BlockCtx,
469 current_l2: u64,
470 parent_hash: B256,
471 mut lookup: impl FnMut(u64) -> Option<B256>,
472) {
473 let Some(parent) = current_l2.checked_sub(1) else {
474 return;
475 };
476 block.cache_l2_block_hash(parent, parent_hash);
477 for offset in 2..=256u64 {
478 let Some(n) = current_l2.checked_sub(offset) else {
479 break;
480 };
481 if block.cached_l2_block_hash(n).is_some() {
482 continue;
483 }
484 match lookup(n) {
485 Some(hash) => block.cache_l2_block_hash(n, hash),
486 None => break,
487 }
488 }
489}
490
491impl<'db, DB, E, Spec, R> ArbBlockExecutor<'_, E, Spec, R>
492where
493 DB: Database + 'db,
494 E: Evm<
495 DB = &'db mut State<DB>,
496 Tx: FromRecoveredTx<R::Transaction>
497 + FromTxWithEncoded<R::Transaction>
498 + ArbTransactionEnv,
499 >,
500 Spec: EthExecutorSpec,
501 R: ReceiptBuilder<
502 Transaction: Transaction + Encodable2718 + ArbTransactionExt,
503 Receipt: TxReceipt<Log = Log>,
504 >,
505 R::Transaction: TransactionEnvelope,
506{
507 #[cold]
511 #[inline(never)]
512 fn refresh_state_params(&mut self) {
513 let arbos_version = self.arb_ctx.arbos_version;
514 let mut calldata_pricing_increase_enabled = false;
515 let mut collect_tips_enabled = self
516 .arb_hooks
517 .as_ref()
518 .map(|h| h.collect_tips_enabled)
519 .unwrap_or(false);
520 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
521 if let Ok(arb_state) = ArbosState::open(db, SystemBurner::new(None, false)) {
522 let state_ref = unsafe { arb_state.backing_storage.state_mut() };
524 if let Ok(net) = arb_state.network_fee_account(state_ref) {
525 self.arb_ctx.network_fee_account = net;
526 }
527 if let Ok(infra) = arb_state.infra_fee_account(state_ref) {
528 self.arb_ctx.infra_fee_account = infra;
529 }
530 if let Ok(min_fee) = arb_state.l2_pricing_state.min_base_fee_wei(state_ref) {
531 self.arb_ctx.min_base_fee = min_fee;
532 }
533 if let Ok(level) = arb_state.brotli_compression_level(state_ref) {
534 self.arb_ctx.brotli_compression_level = level;
535 }
536 calldata_pricing_increase_enabled = arbos_version
537 >= arb_chainspec::arbos_version::ARBOS_VERSION_40
538 && arb_state
539 .features
540 .is_increased_calldata_price_enabled(state_ref)
541 .unwrap_or(false);
542 collect_tips_enabled = arb_state
543 .collect_tips(state_ref)
544 .unwrap_or(collect_tips_enabled);
545 }
546 if let Some(hooks) = self.arb_hooks.as_mut() {
547 hooks.network_fee_account = self.arb_ctx.network_fee_account;
548 hooks.infra_fee_account = self.arb_ctx.infra_fee_account;
549 hooks.min_base_fee = self.arb_ctx.min_base_fee;
550 hooks.calldata_pricing_increase_enabled = calldata_pricing_increase_enabled;
551 hooks.collect_tips_enabled = collect_tips_enabled;
552 }
553 }
554
555 fn execute_submit_retryable(
559 &mut self,
560 ticket_id: alloy_primitives::B256,
561 tx_type: <R::Transaction as TransactionEnvelope>::TxType,
562 mut info: arb_primitives::SubmitRetryableInfo,
563 ) -> Result<
564 EthTxResult<E::HaltReason, <R::Transaction as TransactionEnvelope>::TxType>,
565 BlockExecutionError,
566 > {
567 let sender = info.from;
568
569 let is_filtered = {
574 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
575 let arb_state = ArbosState::open(db, SystemBurner::new(None, false))
576 .map_err(BlockExecutionError::other)?;
577 if arb_state.filtered_transactions.is_filtered_free(ticket_id) {
578 let state_ref = unsafe { arb_state.backing_storage.state_mut() };
582 let recipient = arb_state
583 .filtered_funds_recipient_or_default(state_ref)
584 .map_err(BlockExecutionError::other)?;
585 info.fee_refund_addr = recipient;
586 info.beneficiary = recipient;
587 true
588 } else {
589 false
590 }
591 };
592
593 let block = self.inner.evm().block();
595 let current_time = revm::context::Block::timestamp(block).to::<u64>();
596 let effective_base_fee = self.arb_ctx.basefee;
597
598 let overlay = &mut self.state_overlay;
599 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
600
601 let _ = arb_util::mint_balance(&sender, info.deposit_value, |f, t, a| {
603 apply_balance_op(db, overlay, f, t, a)
604 });
605 self.touched_accounts.insert(sender);
606
607 let dep_i128: i128 = info.deposit_value.try_into().unwrap_or(i128::MAX);
609 self.expected_balance_delta = self.expected_balance_delta.saturating_add(dep_i128);
610
611 let _ = db.load_cache_account(sender);
613 let balance_after_mint = db
614 .cache
615 .accounts
616 .get(&sender)
617 .and_then(|a| a.account.as_ref())
618 .map(|a| a.info.balance)
619 .unwrap_or(U256::ZERO);
620
621 let params = SubmitRetryableParams {
622 ticket_id,
623 from: sender,
624 fee_refund_addr: info.fee_refund_addr,
625 deposit_value: info.deposit_value,
626 retry_value: info.retry_value,
627 gas_fee_cap: info.gas_fee_cap,
628 gas: info.gas,
629 max_submission_fee: info.max_submission_fee,
630 retry_data_len: info.retry_data.len(),
631 l1_base_fee: info.l1_base_fee,
632 effective_base_fee,
633 current_time,
634 balance_after_mint,
635 infra_fee_account: self.arb_ctx.infra_fee_account,
636 min_base_fee: self.arb_ctx.min_base_fee,
637 arbos_version: self.arb_ctx.arbos_version,
638 };
639
640 let fees = compute_submit_retryable_fees(¶ms);
641
642 let user_gas = info.gas;
643
644 if let Some(ref err) = fees.error {
648 tracing::warn!(
649 target: "arb::executor",
650 ticket_id = %ticket_id,
651 error = %err,
652 "submit retryable fee validation failed"
653 );
654
655 self.pending_tx = Some(PendingArbTx {
656 sender,
657 tx_gas_limit: user_gas,
658 arb_tx_type: Some(ArbTxType::ArbitrumSubmitRetryableTx),
659 poster_gas: 0,
660 evm_gas_used: 0,
661
662 charged_multi_gas: MultiGas::default(),
663 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
664 stylus_data_fee: U256::ZERO,
665 retry_context: None,
666 coinbase_tip_per_gas: 0,
667 capped_gas_price: false,
668 actual_gas_price: self.arb_ctx.basefee,
669 });
670
671 return Ok(EthTxResult {
672 result: revm::context::result::ResultAndState {
673 result: ExecutionResult::Revert {
674 gas: synthetic_result_gas(0),
675 logs: Vec::new(),
676 output: alloy_primitives::Bytes::new(),
677 },
678 state: Default::default(),
679 },
680 blob_gas_used: 0,
681 tx_type,
682 });
683 }
684
685 let overlay = &mut self.state_overlay;
686 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
687
688 if !fees.submission_fee.is_zero() {
690 let _ = arb_util::transfer_balance(
694 Some(&sender),
695 Some(&self.arb_ctx.network_fee_account),
696 fees.submission_fee,
697 |f, t, a| apply_balance_op(db, overlay, f, t, a),
698 );
699 self.touched_accounts.insert(sender);
700 self.touched_accounts
701 .insert(self.arb_ctx.network_fee_account);
702 }
703
704 let _ = arb_util::transfer_balance(
706 Some(&sender),
707 Some(&info.fee_refund_addr),
708 fees.submission_fee_refund,
709 |f, t, a| apply_balance_op(db, overlay, f, t, a),
710 );
711 self.touched_accounts.insert(sender);
712 self.touched_accounts.insert(info.fee_refund_addr);
713
714 let escrow_outcome = arb_util::transfer_balance(
718 Some(&sender),
719 Some(&fees.escrow),
720 info.retry_value,
721 |f, t, a| apply_balance_op(db, overlay, f, t, a),
722 );
723 if matches!(
724 escrow_outcome,
725 Err(BalanceError::InsufficientBalance { .. })
726 ) {
727 self.touched_accounts.insert(sender);
728 self.touched_accounts.insert(fees.escrow);
729 let _ = arb_util::transfer_balance(
731 Some(&self.arb_ctx.network_fee_account),
732 Some(&sender),
733 fees.submission_fee,
734 |f, t, a| apply_balance_op(db, overlay, f, t, a),
735 );
736 self.touched_accounts
737 .insert(self.arb_ctx.network_fee_account);
738 let _ = arb_util::transfer_balance(
740 Some(&sender),
741 Some(&info.fee_refund_addr),
742 fees.withheld_submission_fee,
743 |f, t, a| apply_balance_op(db, overlay, f, t, a),
744 );
745 self.touched_accounts.insert(info.fee_refund_addr);
746
747 self.pending_tx = Some(PendingArbTx {
748 sender,
749 tx_gas_limit: user_gas,
750 arb_tx_type: Some(ArbTxType::ArbitrumSubmitRetryableTx),
751 poster_gas: 0,
752 evm_gas_used: 0,
753
754 charged_multi_gas: MultiGas::default(),
755 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
756 stylus_data_fee: U256::ZERO,
757 retry_context: None,
758 coinbase_tip_per_gas: 0,
759 capped_gas_price: false,
760 actual_gas_price: self.arb_ctx.basefee,
761 });
762
763 return Ok(EthTxResult {
764 result: revm::context::result::ResultAndState {
765 result: ExecutionResult::Revert {
766 gas: synthetic_result_gas(0),
767 logs: Vec::new(),
768 output: alloy_primitives::Bytes::new(),
769 },
770 state: Default::default(),
771 },
772 blob_gas_used: 0,
773 tx_type,
774 });
775 }
776 self.touched_accounts.insert(sender);
777 self.touched_accounts.insert(fees.escrow);
778
779 if info.retry_value.is_zero() {
783 materialise_empty(db, overlay, fees.escrow, &mut self.touched_accounts);
784 }
785
786 let arb_state = ArbosState::open(db, SystemBurner::new(None, false))
788 .map_err(BlockExecutionError::other)?;
789 let state_ref = unsafe { arb_state.backing_storage.state_mut() };
791 let _ = arb_state.retryable_state.create_retryable(
792 state_ref,
793 ticket_id,
794 fees.timeout,
795 sender,
796 info.retry_to,
797 info.retry_value,
798 info.beneficiary,
799 &info.retry_data,
800 );
801
802 let mut receipt_logs: Vec<Log> = Vec::new();
804 receipt_logs.push(Log {
805 address: arb_precompiles::ARBRETRYABLETX_ADDRESS,
806 data: alloy_primitives::LogData::new_unchecked(
807 vec![arb_precompiles::ticket_created_topic(), ticket_id],
808 alloy_primitives::Bytes::new(),
809 ),
810 });
811
812 let overlay = &mut self.state_overlay;
813 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
814
815 if fees.can_pay_for_gas {
817 if self.arb_ctx.infra_fee_account != Address::ZERO {
819 let _ = arb_util::transfer_balance(
820 Some(&sender),
821 Some(&self.arb_ctx.infra_fee_account),
822 fees.infra_cost,
823 |f, t, a| apply_balance_op(db, overlay, f, t, a),
824 );
825 self.touched_accounts.insert(sender);
826 self.touched_accounts.insert(self.arb_ctx.infra_fee_account);
827 }
828 if !fees.network_cost.is_zero() {
830 let _ = arb_util::transfer_balance(
831 Some(&sender),
832 Some(&self.arb_ctx.network_fee_account),
833 fees.network_cost,
834 |f, t, a| apply_balance_op(db, overlay, f, t, a),
835 );
836 self.touched_accounts.insert(sender);
837 self.touched_accounts
838 .insert(self.arb_ctx.network_fee_account);
839 }
840 let _ = arb_util::transfer_balance(
842 Some(&sender),
843 Some(&info.fee_refund_addr),
844 fees.gas_price_refund,
845 |f, t, a| apply_balance_op(db, overlay, f, t, a),
846 );
847 self.touched_accounts.insert(sender);
848 self.touched_accounts.insert(info.fee_refund_addr);
849
850 if !is_filtered {
852 let arb_state = ArbosState::open(db, SystemBurner::new(None, false))
855 .map_err(BlockExecutionError::other)?;
856 let state_ref = unsafe { arb_state.backing_storage.state_mut() };
858 match arb_state
859 .retryable_state
860 .open_retryable(state_ref, ticket_id, 0)
861 {
862 Ok(Some(retryable)) => {
863 let _ = retryable.increment_num_tries(state_ref);
864
865 match retryable.make_tx(
866 state_ref,
867 U256::from(self.arb_ctx.chain_id),
868 0, effective_base_fee,
870 user_gas,
871 ticket_id,
872 info.fee_refund_addr,
873 fees.available_refund,
874 fees.submission_fee,
875 ) {
876 Ok(retry_tx) => {
877 let retry_tx_hash = {
879 let mut enc = Vec::new();
880 enc.push(ArbTxType::ArbitrumRetryTx.as_u8());
881 alloy_rlp::Encodable::encode(&retry_tx, &mut enc);
882 keccak256(&enc)
883 };
884
885 let mut event_data = Vec::with_capacity(128);
887 event_data.extend_from_slice(
888 &B256::left_padding_from(&user_gas.to_be_bytes()).0,
889 );
890 event_data.extend_from_slice(
891 &B256::left_padding_from(info.fee_refund_addr.as_slice()).0,
892 );
893 event_data
894 .extend_from_slice(&fees.available_refund.to_be_bytes::<32>());
895 event_data
896 .extend_from_slice(&fees.submission_fee.to_be_bytes::<32>());
897
898 receipt_logs.push(Log {
899 address: arb_precompiles::ARBRETRYABLETX_ADDRESS,
900 data: alloy_primitives::LogData::new_unchecked(
901 vec![
902 arb_precompiles::redeem_scheduled_topic(),
903 ticket_id,
904 retry_tx_hash,
905 B256::left_padding_from(&0u64.to_be_bytes()),
906 ],
907 event_data.into(),
908 ),
909 });
910
911 if let Some(hooks) = self.arb_hooks.as_mut() {
912 let mut encoded = Vec::new();
913 encoded.push(ArbTxType::ArbitrumRetryTx.as_u8());
914 alloy_rlp::Encodable::encode(&retry_tx, &mut encoded);
915 hooks.tx_proc.scheduled_txs.push(encoded);
916 } else {
917 tracing::warn!(
918 target: "arb::executor",
919 "Cannot schedule auto-redeem: arb_hooks is None"
920 );
921 }
922 }
923 Err(_) => {
924 tracing::warn!(
925 target: "arb::executor",
926 "Auto-redeem make_tx failed"
927 );
928 }
929 }
930 }
931 Ok(None) => {
932 tracing::warn!(
933 target: "arb::executor",
934 %ticket_id,
935 "open_retryable returned None after create"
936 );
937 }
938 Err(_) => {
939 tracing::warn!(
940 target: "arb::executor",
941 "open_retryable failed"
942 );
943 }
944 }
945 }
946 } else if !fees.gas_cost_refund.is_zero() {
947 let _ = arb_util::transfer_balance(
949 Some(&sender),
950 Some(&info.fee_refund_addr),
951 fees.gas_cost_refund,
952 |f, t, a| apply_balance_op(db, overlay, f, t, a),
953 );
954 self.touched_accounts.insert(sender);
955 self.touched_accounts.insert(info.fee_refund_addr);
956 }
957
958 let gas_used = if fees.can_pay_for_gas { user_gas } else { 0 };
964 self.pending_tx = Some(PendingArbTx {
965 sender,
966 tx_gas_limit: user_gas,
967 arb_tx_type: Some(ArbTxType::ArbitrumSubmitRetryableTx),
968 poster_gas: 0,
969 evm_gas_used: gas_used,
970 charged_multi_gas: if fees.can_pay_for_gas {
971 MultiGas::single_dim_gas(user_gas)
972 } else {
973 MultiGas::default()
974 },
975 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
976 stylus_data_fee: U256::ZERO,
977 retry_context: None,
978 coinbase_tip_per_gas: 0,
979 capped_gas_price: false,
980 actual_gas_price: self.arb_ctx.basefee,
981 });
982
983 let ticket_bytes = alloy_primitives::Bytes::copy_from_slice(ticket_id.as_slice());
987
988 if is_filtered {
989 Ok(EthTxResult {
990 result: revm::context::result::ResultAndState {
991 result: ExecutionResult::Revert {
992 gas: synthetic_result_gas(gas_used),
993 logs: Vec::new(),
994 output: ticket_bytes,
995 },
996 state: Default::default(),
997 },
998 blob_gas_used: 0,
999 tx_type,
1000 })
1001 } else {
1002 Ok(EthTxResult {
1003 result: revm::context::result::ResultAndState {
1004 result: ExecutionResult::Success {
1005 reason: revm::context::result::SuccessReason::Return,
1006 gas: synthetic_result_gas(gas_used),
1007 output: revm::context::result::Output::Call(ticket_bytes),
1008 logs: receipt_logs,
1009 },
1010 state: Default::default(),
1011 },
1012 blob_gas_used: 0,
1013 tx_type,
1014 })
1015 }
1016 }
1017}
1018
1019impl<'db, DB, E, Spec, R> BlockExecutor for ArbBlockExecutor<'_, E, Spec, R>
1020where
1021 DB: Database + 'db,
1022 E: Evm<
1023 DB = &'db mut State<DB>,
1024 Tx: FromRecoveredTx<R::Transaction>
1025 + FromTxWithEncoded<R::Transaction>
1026 + ArbTransactionEnv,
1027 >,
1028 Spec: EthExecutorSpec,
1029 R: ReceiptBuilder<
1030 Transaction: Transaction + Encodable2718 + ArbTransactionExt,
1031 Receipt: TxReceipt<Log = Log> + arb_primitives::SetArbReceiptFields,
1032 >,
1033 R::Transaction: TransactionEnvelope,
1034{
1035 type Transaction = R::Transaction;
1036 type Receipt = R::Receipt;
1037 type Evm = E;
1038 type Result = EthTxResult<E::HaltReason, <R::Transaction as TransactionEnvelope>::TxType>;
1039
1040 fn apply_pre_execution_changes(&mut self) -> Result<(), BlockExecutionError> {
1041 self.inner.apply_pre_execution_changes()?;
1042
1043 {
1045 let block = self.inner.evm().block();
1046 let timestamp = revm::context::Block::timestamp(block).to::<u64>();
1047 if self.arb_ctx.block_timestamp == 0 {
1048 self.arb_ctx.block_timestamp = timestamp;
1049 }
1050 self.arb_ctx.coinbase = revm::context::Block::beneficiary(block);
1051 self.arb_ctx.basefee = U256::from(revm::context::Block::basefee(block));
1052 if self.arb_ctx.chain_id == 0 {
1057 self.arb_ctx.chain_id = self.inner.evm().chain_id();
1058 }
1059 if let Some(prevrandao) = revm::context::Block::prevrandao(block)
1060 && self.arb_ctx.l1_block_number == 0
1061 {
1062 self.arb_ctx.l1_block_number =
1063 crate::config::l1_block_number_from_mix_hash(&prevrandao);
1064 }
1065 }
1066
1067 if self.arb_ctx.l2_block_number > 0 {
1072 self.precompile_ctx
1073 .block
1074 .cache_l1_block_number(self.arb_ctx.l2_block_number, self.arb_ctx.l1_block_number);
1075 }
1076
1077 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1081 let arb_state = ArbosState::open(db, SystemBurner::new(None, false))
1082 .map_err(BlockExecutionError::other)?;
1083 let state_ref = unsafe { arb_state.backing_storage.state_mut() };
1086
1087 let _ = arb_state.l2_pricing_state.commit_multi_gas_fees(state_ref);
1088
1089 if let Ok(base_fee) = arb_state.l2_pricing_state.base_fee_wei(state_ref) {
1090 self.arb_ctx.basefee = base_fee;
1091 }
1092
1093 load_state_params(
1094 &mut self.arb_ctx,
1095 &mut self.precompile_ctx,
1096 &mut self.arb_hooks,
1097 state_ref,
1098 &arb_state,
1099 );
1100
1101 self.block_gas_left = arb_state
1102 .l2_pricing_state
1103 .per_block_gas_limit(state_ref)
1104 .unwrap_or(0);
1105
1106 if let Ok(l1_block_number) = arb_state.blockhashes.l1_block_number(state_ref) {
1107 let lower = l1_block_number.saturating_sub(256);
1108 for n in lower..l1_block_number {
1109 if let Ok(Some(hash)) = arb_state.blockhashes.block_hash(state_ref, n) {
1112 state_ref.block_hashes.insert(n, hash);
1113 }
1114 }
1115 }
1116
1117 {
1121 let parent_hash = self.arb_ctx.parent_hash;
1122 let current_l2 = self.arb_ctx.l2_block_number;
1123 let block = std::sync::Arc::clone(&self.precompile_ctx.block);
1124 populate_l2_block_hash_window(&block, current_l2, parent_hash, |n| {
1125 match state_ref.database.block_hash(n) {
1126 Ok(hash) if hash != B256::ZERO => Some(hash),
1127 _ => None,
1128 }
1129 });
1130 }
1131
1132 tracing::trace!(
1133 target: "arb::executor",
1134 l1_block = self.arb_ctx.l1_block_number,
1135 delayed_msgs = self.arb_ctx.delayed_messages_read,
1136 chain_id = self.arb_ctx.chain_id,
1137 basefee = %self.arb_ctx.basefee,
1138 arbos_version = self.arb_ctx.arbos_version,
1139 has_hooks = self.arb_hooks.is_some(),
1140 "starting block execution"
1141 );
1142
1143 Ok(())
1144 }
1145
1146 fn execute_transaction_without_commit(
1147 &mut self,
1148 tx: impl ExecutableTx<Self>,
1149 ) -> Result<Self::Result, BlockExecutionError> {
1150 let (tx_env, recovered) = tx.into_parts();
1152 let sender = *recovered.signer();
1153 let tx_type_raw = recovered.tx().ty();
1154 let tx_gas_limit = recovered.tx().gas_limit();
1155 let tx_value = recovered.tx().value();
1156 let envelope_tx_type = recovered.tx().tx_type();
1157 let intrinsic_multi_gas = tx_intrinsic_multi_gas(
1158 recovered.tx(),
1159 arb_chainspec::spec_id_by_arbos_version(self.arb_ctx.arbos_version),
1160 );
1161 let calldata_floor_gas = if self
1164 .arb_hooks
1165 .as_ref()
1166 .map(|h| h.is_calldata_pricing_increase_enabled())
1167 .unwrap_or(false)
1168 {
1169 tx_floor_data_gas(recovered.tx())
1170 } else {
1171 0
1172 };
1173
1174 let arb_tx_type = ArbTxType::from_u8(tx_type_raw).ok();
1176 let is_arb_internal = arb_tx_type == Some(ArbTxType::ArbitrumInternalTx);
1177 let is_arb_deposit = arb_tx_type == Some(ArbTxType::ArbitrumDepositTx);
1178 let is_submit_retryable = arb_tx_type == Some(ArbTxType::ArbitrumSubmitRetryableTx);
1179 let is_retry_tx = arb_tx_type == Some(ArbTxType::ArbitrumRetryTx);
1180 let is_contract_tx = arb_tx_type == Some(ArbTxType::ArbitrumContractTx);
1181 let has_poster_costs = tx_type_has_poster_costs(tx_type_raw);
1182
1183 let is_user_tx =
1187 !is_arb_internal && !is_arb_deposit && !is_submit_retryable && !is_retry_tx;
1188 const TX_GAS_MIN: u64 = 21_000;
1189 if is_user_tx && self.block_gas_left < TX_GAS_MIN {
1190 return Err(BlockExecutionError::msg("block gas limit reached"));
1191 }
1192
1193 crate::evm::reset_stylus_pages(&self.precompile_ctx);
1195 crate::evm::clear_poster_balance_correction();
1196 self.precompile_ctx.reset_tx();
1197 self.precompile_ctx.reset_caller_stack();
1198 self.state_overlay.reset_tx();
1199 if let Some(hooks) = self.arb_hooks.as_mut() {
1200 hooks.tx_proc.poster_fee = U256::ZERO;
1201 hooks.tx_proc.poster_gas = 0;
1202 hooks.tx_proc.compute_hold_gas = 0;
1203 hooks.tx_proc.current_retryable = None;
1204 hooks.tx_proc.current_refund_to = None;
1205 hooks.tx_proc.scheduled_txs.clear();
1206 }
1207
1208 let actual_gas_price: U256 = {
1213 let base_fee = self.arb_ctx.basefee;
1214 let base_fee_u128: u128 = base_fee.try_into().unwrap_or(u128::MAX);
1215 let max_fee: u128 = revm::context_interface::Transaction::gas_price(&tx_env);
1216 let effective: u128 =
1217 match revm::context_interface::Transaction::max_priority_fee_per_gas(&tx_env) {
1218 Some(max_priority) => {
1219 std::cmp::min(max_fee, base_fee_u128.saturating_add(max_priority))
1220 }
1221 None => max_fee,
1222 };
1223 let drop = self
1224 .arb_hooks
1225 .as_ref()
1226 .map(|h| h.drop_tip())
1227 .unwrap_or(false);
1228 if drop || effective == 0 {
1229 base_fee
1230 } else {
1231 U256::from(effective)
1232 }
1233 };
1234
1235 if is_arb_internal {
1239 use arbos::tx_processor::ARBOS_ADDRESS;
1240
1241 if sender != ARBOS_ADDRESS {
1242 return Err(BlockExecutionError::msg(
1243 "internal tx not from ArbOS address",
1244 ));
1245 }
1246
1247 let tx_data = recovered.tx().input().to_vec();
1248 let tx_type = recovered.tx().tx_type();
1249 let mut tx_err = None;
1250
1251 if let Some(selector) = tx_data.first_chunk::<4>() {
1252 let is_start_block = *selector == internal_tx::INTERNAL_TX_START_BLOCK_METHOD_ID;
1253
1254 if is_start_block
1255 && let Ok(start_data) = internal_tx::decode_start_block_data(&tx_data)
1256 {
1257 self.arb_ctx.l1_base_fee = start_data.l1_base_fee;
1258 self.arb_ctx.time_passed = start_data.time_passed;
1259 }
1260
1261 let (block_number, current_time) = {
1262 let block = self.inner.evm().block();
1263 (
1264 revm::context::Block::number(block).to::<u64>(),
1265 revm::context::Block::timestamp(block).to::<u64>(),
1266 )
1267 };
1268 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1269 let mut arb_state = ArbosState::open(db, SystemBurner::new(None, false))
1270 .map_err(BlockExecutionError::other)?;
1271 let closure_storage = arb_state.backing_storage.clone();
1278 let ctx = InternalTxContext {
1279 block_number,
1280 current_time,
1281 prev_hash: self.arb_ctx.parent_hash,
1282 };
1283
1284 if is_start_block
1286 && arb_state.arbos_version() >= arb_chainspec::arbos_version::ARBOS_VERSION_40
1287 {
1288 process_parent_block_hash(
1290 unsafe { closure_storage.state_mut() },
1291 self.arb_ctx.l2_block_number,
1292 ctx.prev_hash,
1293 );
1294 }
1295
1296 let touched_ptr = &mut self.touched_accounts as *mut rustc_hash::FxHashSet<Address>;
1297 let zombie_ptr = &mut self.zombie_accounts as *mut rustc_hash::FxHashSet<Address>;
1298 let finalise_ptr = &self.finalise_deleted as *const rustc_hash::FxHashSet<Address>;
1299 let overlay_ptr = &mut self.state_overlay as *mut StateOverlay;
1300 let arbos_ver = self.arb_ctx.arbos_version;
1301 let transfer_storage = closure_storage.clone();
1302 let balance_storage = closure_storage.clone();
1303 let mut do_transfer = move |from: Address, to: Address, amount: U256| {
1304 unsafe {
1306 let state = transfer_storage.state_mut();
1307 if amount.is_zero()
1308 && arbos_ver < arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS
1309 {
1310 create_zombie_if_deleted(
1311 state,
1312 &mut *overlay_ptr,
1313 from,
1314 &*finalise_ptr,
1315 &mut *zombie_ptr,
1316 &mut *touched_ptr,
1317 );
1318 }
1319 let _ = apply_balance_op(
1326 state,
1327 &mut *overlay_ptr,
1328 Some(&from),
1329 Some(&to),
1330 amount,
1331 );
1332 if !amount.is_zero() {
1333 (*zombie_ptr).remove(&from);
1334 }
1335 (*zombie_ptr).remove(&to);
1336 (*touched_ptr).insert(from);
1337 (*touched_ptr).insert(to);
1338 }
1339 Ok(())
1340 };
1341 let mut do_balance = move |addr: Address| -> U256 {
1342 unsafe { get_balance(balance_storage.state_mut(), addr) }
1344 };
1345 if let Err(e) = internal_tx::apply_internal_tx_update(
1351 unsafe { closure_storage.state_mut() },
1352 &tx_data,
1353 &mut arb_state,
1354 &ctx,
1355 &mut do_transfer,
1356 &mut do_balance,
1357 ) {
1358 tracing::warn!(
1359 target: "arb::executor",
1360 error = %e,
1361 "internal tx processing failed"
1362 );
1363 tx_err = Some(e);
1364 }
1365
1366 if is_start_block {
1367 let state_ref = unsafe { arb_state.backing_storage.state_mut() };
1369 if let Ok(l1_block_number) = arb_state.blockhashes.l1_block_number(state_ref) {
1370 self.arb_ctx.l1_block_number = l1_block_number;
1371 }
1372
1373 load_state_params(
1374 &mut self.arb_ctx,
1375 &mut self.precompile_ctx,
1376 &mut self.arb_hooks,
1377 state_ref,
1378 &arb_state,
1379 );
1380
1381 if let Ok(l1_block_number) = arb_state.blockhashes.l1_block_number(state_ref) {
1382 let lower = l1_block_number.saturating_sub(256);
1383 for n in lower..l1_block_number {
1384 if let Ok(Some(hash)) = arb_state.blockhashes.block_hash(state_ref, n) {
1385 state_ref.block_hashes.insert(n, hash);
1386 }
1387 }
1388 }
1389 }
1390 }
1391
1392 self.pending_tx = Some(PendingArbTx {
1394 sender,
1395 tx_gas_limit: 0,
1396 arb_tx_type: Some(ArbTxType::ArbitrumInternalTx),
1397 poster_gas: 0,
1398 evm_gas_used: 0,
1399
1400 charged_multi_gas: MultiGas::default(),
1401 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
1402 stylus_data_fee: U256::ZERO,
1403 retry_context: None,
1404 coinbase_tip_per_gas: 0,
1405 capped_gas_price: false,
1406 actual_gas_price: self.arb_ctx.basefee,
1407 });
1408
1409 if let Some(err) = tx_err {
1411 return Err(BlockExecutionError::other(err));
1412 }
1413
1414 return Ok(EthTxResult {
1415 result: revm::context::result::ResultAndState {
1416 result: ExecutionResult::Success {
1417 reason: revm::context::result::SuccessReason::Return,
1418 gas: synthetic_result_gas(0),
1419 output: revm::context::result::Output::Call(alloy_primitives::Bytes::new()),
1420 logs: Vec::new(),
1421 },
1422 state: Default::default(),
1423 },
1424 blob_gas_used: 0,
1425 tx_type,
1426 });
1427 }
1428
1429 if is_arb_deposit {
1432 let value = recovered.tx().value();
1433 let mut to = match recovered.tx().kind() {
1434 TxKind::Call(addr) => addr,
1435 TxKind::Create => {
1436 return Err(BlockExecutionError::msg("deposit tx has no To address"));
1437 }
1438 };
1439 let tx_type = recovered.tx().tx_type();
1440 let tx_hash = recovered.tx().trie_hash();
1441
1442 let mut is_filtered = false;
1446 {
1447 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1448 let arb_state = ArbosState::open(db, SystemBurner::new(None, false))
1449 .map_err(BlockExecutionError::other)?;
1450 if arb_state.filtered_transactions.is_filtered_free(tx_hash) {
1451 let state_ref = unsafe { arb_state.backing_storage.state_mut() };
1453 to = arb_state
1454 .filtered_funds_recipient_or_default(state_ref)
1455 .map_err(BlockExecutionError::other)?;
1456 is_filtered = true;
1457 }
1458 }
1459
1460 let overlay = &mut self.state_overlay;
1461 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1462 let _ = arb_util::mint_balance(&sender, value, |f, t, a| {
1463 apply_balance_op(db, overlay, f, t, a)
1464 });
1465 let _ = arb_util::transfer_balance(Some(&sender), Some(&to), value, |f, t, a| {
1466 apply_balance_op(db, overlay, f, t, a)
1467 });
1468 self.touched_accounts.insert(sender);
1469 self.touched_accounts.insert(to);
1470
1471 let value_i128: i128 = value.try_into().unwrap_or(i128::MAX);
1473 self.expected_balance_delta = self.expected_balance_delta.saturating_add(value_i128);
1474
1475 self.pending_tx = Some(PendingArbTx {
1476 sender,
1477 tx_gas_limit: 0,
1478 arb_tx_type: Some(ArbTxType::ArbitrumDepositTx),
1479 poster_gas: 0,
1480 evm_gas_used: 0,
1481
1482 charged_multi_gas: MultiGas::default(),
1483 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
1484 stylus_data_fee: U256::ZERO,
1485 retry_context: None,
1486 coinbase_tip_per_gas: 0,
1487 capped_gas_price: false,
1488 actual_gas_price: self.arb_ctx.basefee,
1489 });
1490
1491 let result = if is_filtered {
1495 ExecutionResult::Revert {
1496 gas: synthetic_result_gas(0),
1497 logs: Vec::new(),
1498 output: alloy_primitives::Bytes::from("filtered transaction"),
1499 }
1500 } else {
1501 ExecutionResult::Success {
1502 reason: revm::context::result::SuccessReason::Return,
1503 gas: synthetic_result_gas(0),
1504 output: revm::context::result::Output::Call(alloy_primitives::Bytes::new()),
1505 logs: Vec::new(),
1506 }
1507 };
1508
1509 return Ok(EthTxResult {
1510 result: revm::context::result::ResultAndState {
1511 result,
1512 state: Default::default(),
1513 },
1514 blob_gas_used: 0,
1515 tx_type,
1516 });
1517 }
1518
1519 if is_submit_retryable && let Some(info) = recovered.tx().submit_retryable_info() {
1521 let ticket_id = recovered.tx().trie_hash();
1522 let tx_type = recovered.tx().tx_type();
1523 return self.execute_submit_retryable(ticket_id, tx_type, info);
1524 }
1525
1526 let mut retry_pre_exec_undo: Option<(Address, U256, Address, U256)> = None;
1530 let mut retry_context = None;
1531 if is_retry_tx && let Some(info) = recovered.tx().retry_tx_info() {
1532 let current_time = {
1533 let block = self.inner.evm().block();
1534 revm::context::Block::timestamp(block).to::<u64>()
1535 };
1536 let overlay = &mut self.state_overlay;
1537 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1538
1539 let retryable = {
1542 let arb_state = ArbosState::open(db, SystemBurner::new(None, false))
1543 .map_err(BlockExecutionError::other)?;
1544 let state_ref = unsafe { arb_state.backing_storage.state_mut() };
1546 arb_state
1547 .retryable_state
1548 .open_retryable(state_ref, info.ticket_id, current_time)
1549 .map(|opt| opt.map(|_| ()))
1550 };
1551
1552 match retryable {
1553 Ok(Some(_)) => {
1554 let escrow = retryables::retryable_escrow_address(info.ticket_id);
1556 let value = recovered.tx().value();
1557
1558 if value.is_zero()
1561 && self.arb_ctx.arbos_version
1562 < arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS
1563 {
1564 create_zombie_if_deleted(
1565 db,
1566 overlay,
1567 escrow,
1568 &self.finalise_deleted,
1569 &mut self.zombie_accounts,
1570 &mut self.touched_accounts,
1571 );
1572 }
1573
1574 let escrow_outcome = arb_util::transfer_balance(
1575 Some(&escrow),
1576 Some(&sender),
1577 value,
1578 |f, t, a| apply_balance_op(db, overlay, f, t, a),
1579 );
1580 if matches!(
1581 escrow_outcome,
1582 Err(BalanceError::InsufficientBalance { .. })
1583 ) {
1584 let tx_type = recovered.tx().tx_type();
1586 self.pending_tx = Some(PendingArbTx {
1587 sender,
1588 tx_gas_limit: 0,
1589 arb_tx_type: Some(ArbTxType::ArbitrumRetryTx),
1590 poster_gas: 0,
1591 evm_gas_used: 0,
1592
1593 charged_multi_gas: MultiGas::default(),
1594 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
1595 stylus_data_fee: U256::ZERO,
1596 retry_context: None,
1597 coinbase_tip_per_gas: 0,
1598 capped_gas_price: false,
1599 actual_gas_price: self.arb_ctx.basefee,
1600 });
1601 return Ok(EthTxResult {
1602 result: revm::context::result::ResultAndState {
1603 result: ExecutionResult::Revert {
1604 gas: synthetic_result_gas(0),
1605 logs: Vec::new(),
1606 output: alloy_primitives::Bytes::new(),
1607 },
1608 state: Default::default(),
1609 },
1610 blob_gas_used: 0,
1611 tx_type,
1612 });
1613 }
1614
1615 if !value.is_zero() {
1617 self.zombie_accounts.remove(&escrow);
1618 }
1619 self.zombie_accounts.remove(&sender);
1620 self.touched_accounts.insert(escrow);
1621 self.touched_accounts.insert(sender);
1622
1623 let prepaid = self
1625 .arb_ctx
1626 .basefee
1627 .saturating_mul(U256::from(tx_gas_limit));
1628 let _ = arb_util::mint_balance(&sender, prepaid, |f, t, a| {
1629 apply_balance_op(db, overlay, f, t, a)
1630 });
1631 retry_pre_exec_undo = Some((sender, prepaid, escrow, value));
1632
1633 overlay.drain_and_apply(db, &self.zombie_accounts);
1639
1640 if let Some(hooks) = self.arb_hooks.as_mut() {
1642 hooks
1643 .tx_proc
1644 .prepare_retry_tx(info.ticket_id, info.refund_to);
1645 }
1646
1647 retry_context = Some(PendingRetryContext {
1648 ticket_id: info.ticket_id,
1649 refund_to: info.refund_to,
1650 max_refund: info.max_refund,
1651 submission_fee_refund: info.submission_fee_refund,
1652 call_value: recovered.tx().value(),
1653 });
1654 }
1655 Ok(None) => {
1656 let tx_type = recovered.tx().tx_type();
1658 self.pending_tx = Some(PendingArbTx {
1659 sender,
1660 tx_gas_limit: 0,
1661 arb_tx_type: Some(ArbTxType::ArbitrumRetryTx),
1662 poster_gas: 0,
1663 evm_gas_used: 0,
1664
1665 charged_multi_gas: MultiGas::default(),
1666 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
1667 stylus_data_fee: U256::ZERO,
1668 retry_context: None,
1669 coinbase_tip_per_gas: 0,
1670 capped_gas_price: false,
1671 actual_gas_price: self.arb_ctx.basefee,
1672 });
1673 let err_msg = format!("retryable ticket {} not found", info.ticket_id,);
1674 return Ok(EthTxResult {
1675 result: revm::context::result::ResultAndState {
1676 result: ExecutionResult::Revert {
1677 gas: synthetic_result_gas(0),
1678 logs: Vec::new(),
1679 output: alloy_primitives::Bytes::from(err_msg.into_bytes()),
1680 },
1681 state: Default::default(),
1682 },
1683 blob_gas_used: 0,
1684 tx_type,
1685 });
1686 }
1687 Err(_) => {
1688 let tx_type = recovered.tx().tx_type();
1690 self.pending_tx = Some(PendingArbTx {
1691 sender,
1692 tx_gas_limit: 0,
1693 arb_tx_type: Some(ArbTxType::ArbitrumRetryTx),
1694 poster_gas: 0,
1695 evm_gas_used: 0,
1696
1697 charged_multi_gas: MultiGas::default(),
1698 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
1699 stylus_data_fee: U256::ZERO,
1700 retry_context: None,
1701 coinbase_tip_per_gas: 0,
1702 capped_gas_price: false,
1703 actual_gas_price: self.arb_ctx.basefee,
1704 });
1705 return Ok(EthTxResult {
1706 result: revm::context::result::ResultAndState {
1707 result: ExecutionResult::Revert {
1708 gas: synthetic_result_gas(0),
1709 logs: Vec::new(),
1710 output: alloy_primitives::Bytes::from(
1711 format!("error opening retryable {}", info.ticket_id,)
1712 .into_bytes(),
1713 ),
1714 },
1715 state: Default::default(),
1716 },
1717 blob_gas_used: 0,
1718 tx_type,
1719 });
1720 }
1721 }
1722 }
1723
1724 let mut poster_gas = 0u64;
1727 let mut compute_hold_gas = 0u64;
1728 let calldata_units: u64 = if has_poster_costs {
1729 let level = self.arb_ctx.brotli_compression_level;
1730 let coinbase = self.arb_ctx.coinbase;
1731 let tx_ref = recovered.tx();
1732 let units = if coinbase == l1_pricing::BATCH_POSTER_ADDRESS {
1733 let tx_bytes_ref = tx_ref;
1734 tx_ref.poster_units_for(level, &mut || {
1735 l1_pricing::poster_units_from_bytes(&tx_bytes_ref.encoded_2718(), level)
1736 })
1737 } else {
1738 0
1739 };
1740 let poster_cost = self
1741 .arb_ctx
1742 .l1_price_per_unit
1743 .saturating_mul(U256::from(units));
1744
1745 if let Some(hooks) = self.arb_hooks.as_mut() {
1746 hooks.tx_proc.poster_gas = compute_poster_gas(
1747 poster_cost,
1748 actual_gas_price,
1749 false,
1750 self.arb_ctx.min_base_fee,
1751 );
1752 hooks.tx_proc.poster_fee =
1753 actual_gas_price.saturating_mul(U256::from(hooks.tx_proc.poster_gas));
1754 poster_gas = hooks.tx_proc.poster_gas;
1755 }
1756
1757 units
1758 } else {
1759 0
1760 };
1761
1762 if let Some(hooks) = self.arb_hooks.as_mut()
1767 && !hooks.is_eth_call
1768 {
1769 let spec = arb_chainspec::spec_id_by_arbos_version(self.arb_ctx.arbos_version);
1770 let intrinsic_estimate = estimate_intrinsic_gas(recovered.tx(), spec);
1771 let gas_after_intrinsic = tx_gas_limit.saturating_sub(intrinsic_estimate);
1772 let gas_after_poster = gas_after_intrinsic.saturating_sub(poster_gas);
1773
1774 let max_compute =
1775 if hooks.arbos_version < arb_chainspec::arbos_version::ARBOS_VERSION_50 {
1776 hooks.per_block_gas_limit
1777 } else {
1778 hooks.per_tx_gas_limit.saturating_sub(intrinsic_estimate)
1779 };
1780
1781 if max_compute > 0 && gas_after_poster > max_compute {
1782 compute_hold_gas = gas_after_poster - max_compute;
1783 hooks.tx_proc.compute_hold_gas = compute_hold_gas;
1784 }
1785 }
1786
1787 if is_user_tx
1792 && self.arb_ctx.arbos_version < arb_chainspec::arbos_version::ARBOS_VERSION_50
1793 && self.user_txs_processed > 0
1794 {
1795 const TX_GAS: u64 = 21_000;
1796 let compute_gas = tx_gas_limit.saturating_sub(poster_gas).max(TX_GAS);
1797 if compute_gas > self.block_gas_left {
1798 return Err(BlockExecutionError::msg("block gas limit reached"));
1799 }
1800 }
1801
1802 let tx_hash_for_filter = recovered.tx().trie_hash();
1806 let is_filtered = {
1807 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1808 let arb_state = ArbosState::open(db, SystemBurner::new(None, false))
1809 .map_err(BlockExecutionError::other)?;
1810 if calldata_units > 0 {
1811 let state_ref = unsafe { arb_state.backing_storage.state_mut() };
1813 let _ = arb_state
1814 .l1_pricing_state
1815 .add_to_units_since_update(state_ref, calldata_units);
1816 }
1817 arb_state
1818 .filtered_transactions
1819 .is_filtered_free(tx_hash_for_filter)
1820 };
1821
1822 let mut tx_env = tx_env;
1829 let gas_deduction = poster_gas.saturating_add(compute_hold_gas);
1830 if gas_deduction > 0 {
1831 let evm_gas_limit_before = revm::context_interface::Transaction::gas_limit(&tx_env);
1832 tx_env.set_gas_limit(evm_gas_limit_before.saturating_sub(gas_deduction));
1833 }
1834
1835 {
1840 let correction = actual_gas_price
1841 .saturating_mul(U256::from(poster_gas.saturating_add(compute_hold_gas)));
1842 let correction_u128 = correction.try_into().unwrap_or(u128::MAX);
1843 self.precompile_ctx
1844 .set_poster_balance_correction(correction_u128);
1845 crate::evm::set_poster_balance_correction(correction_u128);
1850 self.precompile_ctx.set_sender(sender);
1851 }
1852
1853 {
1856 use arbos::tx_processor::RevertedTxAction;
1857
1858 let tx_hash = tx_hash_for_filter;
1859
1860 if let Some(hooks) = self.arb_hooks.as_ref() {
1861 let action = hooks.tx_proc.reverted_tx_hook(
1862 Some(tx_hash),
1863 None, is_filtered,
1865 );
1866
1867 match action {
1868 RevertedTxAction::PreRecordedRevert { gas_to_consume } => {
1869 let overlay = &mut self.state_overlay;
1870 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1871 increment_nonce(db, overlay, sender);
1872 self.touched_accounts.insert(sender);
1873 let spec =
1877 arb_chainspec::spec_id_by_arbos_version(self.arb_ctx.arbos_version);
1878 let intrinsic = estimate_intrinsic_gas(recovered.tx(), spec);
1879 let gas_used = intrinsic
1880 .saturating_add(gas_to_consume)
1881 .saturating_add(poster_gas);
1882 let charged_multi_gas = MultiGas::single_dim_gas(poster_gas)
1883 .saturating_add(MultiGas::computation_gas(gas_to_consume));
1884 self.pending_tx = Some(PendingArbTx {
1885 sender,
1886 tx_gas_limit,
1887 arb_tx_type,
1888 poster_gas,
1889 evm_gas_used: 0,
1890 charged_multi_gas,
1891 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
1892 stylus_data_fee: U256::ZERO,
1893 retry_context,
1894 coinbase_tip_per_gas: 0,
1895 capped_gas_price: false,
1896 actual_gas_price: self.arb_ctx.basefee,
1897 });
1898 return Ok(EthTxResult {
1899 result: revm::context::result::ResultAndState {
1900 result: ExecutionResult::Revert {
1901 gas: synthetic_result_gas(gas_used),
1902 logs: Vec::new(),
1903 output: alloy_primitives::Bytes::new(),
1904 },
1905 state: Default::default(),
1906 },
1907 blob_gas_used: 0,
1908 tx_type: envelope_tx_type,
1909 });
1910 }
1911 RevertedTxAction::FilteredTx => {
1912 let overlay = &mut self.state_overlay;
1913 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1914 increment_nonce(db, overlay, sender);
1915 self.touched_accounts.insert(sender);
1916 let gas_remaining = tx_gas_limit
1918 .saturating_sub(poster_gas)
1919 .saturating_sub(compute_hold_gas);
1920 let gas_used = tx_gas_limit;
1921 let charged_multi_gas = MultiGas::single_dim_gas(poster_gas)
1922 .saturating_add(MultiGas::computation_gas(gas_remaining));
1923 self.pending_tx = Some(PendingArbTx {
1924 sender,
1925 tx_gas_limit,
1926 arb_tx_type,
1927 poster_gas,
1928 evm_gas_used: 0,
1929 charged_multi_gas,
1930 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
1931 stylus_data_fee: U256::ZERO,
1932 retry_context,
1933 coinbase_tip_per_gas: 0,
1934 capped_gas_price: false,
1935 actual_gas_price: self.arb_ctx.basefee,
1936 });
1937 return Ok(EthTxResult {
1938 result: revm::context::result::ResultAndState {
1939 result: ExecutionResult::Revert {
1940 gas: synthetic_result_gas(gas_used),
1941 logs: Vec::new(),
1942 output: alloy_primitives::Bytes::from(
1943 "filtered transaction".as_bytes(),
1944 ),
1945 },
1946 state: Default::default(),
1947 },
1948 blob_gas_used: 0,
1949 tx_type: envelope_tx_type,
1950 });
1951 }
1952 RevertedTxAction::None => {}
1953 }
1954 }
1955 }
1956
1957 let upfront_gas_price: u128 = revm::context_interface::Transaction::gas_price(&tx_env);
1963
1964 {
1967 let base_fee_u128: u128 = self.arb_ctx.basefee.try_into().unwrap_or(u128::MAX);
1968 let effective: u128 =
1969 match revm::context_interface::Transaction::max_priority_fee_per_gas(&tx_env) {
1970 Some(max_priority) => {
1971 upfront_gas_price.min(base_fee_u128.saturating_add(max_priority))
1972 }
1973 None => upfront_gas_price,
1974 };
1975 self.precompile_ctx.set_effective_gas_price(effective);
1976 }
1977
1978 let effective_tip_per_gas: u128 = {
1982 let bf: u128 = self.arb_ctx.basefee.try_into().unwrap_or(u128::MAX);
1983 let max_fee: u128 = upfront_gas_price; let max_minus_bf = max_fee.saturating_sub(bf);
1985 match revm::context_interface::Transaction::max_priority_fee_per_gas(&tx_env) {
1986 Some(max_priority) => max_priority.min(max_minus_bf),
1987 None => max_minus_bf,
1988 }
1989 };
1990
1991 let should_drop_tip = self
1999 .arb_hooks
2000 .as_ref()
2001 .map(|h| h.drop_tip())
2002 .unwrap_or(false);
2003 if should_drop_tip {
2004 let base_fee: u128 = self.arb_ctx.basefee.try_into().unwrap_or(u128::MAX);
2005 if upfront_gas_price > base_fee {
2006 tx_env.set_gas_price(base_fee);
2007 tx_env.set_gas_priority_fee(Some(0));
2008 }
2009 }
2010
2011 self.precompile_ctx
2012 .set_tx_is_aliased(arbos::util::does_tx_type_alias(tx_type_raw));
2013
2014 {
2015 let poster_fee_val = self
2016 .arb_hooks
2017 .as_ref()
2018 .map(|h| h.tx_proc.poster_fee)
2019 .unwrap_or(U256::ZERO);
2020 self.precompile_ctx
2021 .set_poster_fee(poster_fee_val.try_into().unwrap_or(u128::MAX));
2022 let retryable_id = retry_context
2023 .as_ref()
2024 .map(|ctx| ctx.ticket_id)
2025 .unwrap_or(B256::ZERO);
2026 self.precompile_ctx.set_retryable_id(retryable_id);
2027 let redeemer = retry_context
2028 .as_ref()
2029 .map(|ctx| ctx.refund_to)
2030 .unwrap_or(Address::ZERO);
2031 self.precompile_ctx.set_redeemer(redeemer);
2032 }
2033
2034 let retry_undo = retry_pre_exec_undo;
2035 let rollback_pre_exec_state =
2036 |this: &mut Self, units: u64| -> Result<(), BlockExecutionError> {
2037 this.precompile_ctx.reset_tx();
2038 let overlay = &mut this.state_overlay;
2039 let db: &mut State<DB> = this.inner.evm_mut().db_mut();
2040 if units > 0 {
2041 let arb_state = ArbosState::open(db, SystemBurner::new(None, false))
2042 .map_err(BlockExecutionError::other)?;
2043 let state_ref = unsafe { arb_state.backing_storage.state_mut() };
2045 let _ = arb_state
2046 .l1_pricing_state
2047 .subtract_from_units_since_update(state_ref, units);
2048 }
2049 if let Some((retry_sender, prepaid, escrow, escrow_value)) = retry_undo {
2050 if !prepaid.is_zero() {
2051 let _ = arb_util::burn_balance(&retry_sender, prepaid, |f, t, a| {
2052 apply_balance_op(db, overlay, f, t, a)
2053 });
2054 }
2055 if !escrow_value.is_zero() {
2056 let _ = arb_util::transfer_balance(
2061 Some(&retry_sender),
2062 Some(&escrow),
2063 escrow_value,
2064 |f, t, a| apply_balance_op(db, overlay, f, t, a),
2065 );
2066 }
2067 }
2068 Ok(())
2069 };
2070
2071 if is_user_tx {
2074 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
2075 let account = db
2076 .load_cache_account(sender)
2077 .ok()
2078 .and_then(|a| a.account_info());
2079 let sender_balance = account.as_ref().map(|a| a.balance).unwrap_or(U256::ZERO);
2080 let sender_nonce = account.as_ref().map(|a| a.nonce).unwrap_or(0);
2081
2082 if !is_contract_tx {
2084 let tx_nonce = revm::context_interface::Transaction::nonce(&tx_env);
2085 if tx_nonce != sender_nonce {
2086 rollback_pre_exec_state(self, calldata_units)?;
2087 return Err(BlockExecutionError::msg(format!(
2088 "nonce mismatch: address {sender} tx nonce {tx_nonce} != state nonce {sender_nonce}"
2089 )));
2090 }
2091 }
2092
2093 let base_fee = self.arb_ctx.basefee;
2104 if U256::from(upfront_gas_price) < base_fee {
2105 rollback_pre_exec_state(self, calldata_units)?;
2106 return Err(BlockExecutionError::msg(format!(
2107 "max fee per gas less than block base fee: address {sender}, maxFeePerGas: {upfront_gas_price}, baseFee: {base_fee}"
2108 )));
2109 }
2110
2111 let gas_cost = U256::from(tx_gas_limit) * U256::from(upfront_gas_price);
2112 let tx_value = revm::context_interface::Transaction::value(&tx_env);
2113 let total_cost = gas_cost.saturating_add(tx_value);
2114 if sender_balance < total_cost {
2115 rollback_pre_exec_state(self, calldata_units)?;
2116 return Err(BlockExecutionError::msg(format!(
2117 "insufficient funds: address {sender} have {sender_balance} want {total_cost}"
2118 )));
2119 }
2120
2121 if calldata_floor_gas > tx_gas_limit {
2122 rollback_pre_exec_state(self, calldata_units)?;
2123 return Err(BlockExecutionError::msg(format!(
2124 "insufficient gas for floor data gas: address {sender} gas limit {tx_gas_limit} floor {calldata_floor_gas}"
2125 )));
2126 }
2127 }
2128
2129 if is_retry_tx || is_contract_tx {
2135 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
2136 let sender_nonce = db
2137 .load_cache_account(sender)
2138 .map(|a| a.account_info().map(|i| i.nonce).unwrap_or(0))
2139 .unwrap_or(0);
2140 tx_env.set_nonce(sender_nonce);
2141 }
2142
2143 {
2144 let to_addr = match recovered.tx().kind() {
2145 TxKind::Call(a) => Some(a),
2146 _ => None,
2147 };
2148 if to_addr == Some(arb_precompiles::ARBWASM_ADDRESS) {
2149 self.precompile_ctx.set_stylus_call_value(tx_value);
2150 if tx_value > U256::ZERO {
2151 tx_env.set_value(U256::ZERO);
2152 }
2153 } else {
2154 self.precompile_ctx.set_stylus_call_value(U256::ZERO);
2155 }
2156 }
2157
2158 let mut output = match self
2159 .inner
2160 .execute_transaction_without_commit((tx_env, recovered))
2161 {
2162 Ok(o) => o,
2163 Err(e) => {
2164 rollback_pre_exec_state(self, calldata_units)?;
2165 return Err(e);
2166 }
2167 };
2168
2169 let evm_gas_used = output.result.result.gas_used();
2172 let gas_refunded = match &output.result.result {
2178 ExecutionResult::Success { gas, .. } => gas.inner_refunded(),
2179 _ => 0,
2180 };
2181
2182 if poster_gas > 0 {
2189 adjust_result_gas_used(&mut output.result.result, poster_gas);
2190 }
2191
2192 let mut total_donated_gas = 0u64;
2199 let mut retry_tx_hash_fixes: Vec<(usize, B256)> = Vec::new();
2201 if let ExecutionResult::Success { ref logs, .. } = output.result.result {
2202 let redeem_topic = arb_precompiles::redeem_scheduled_topic();
2203 let precompile_addr = arb_precompiles::ARBRETRYABLETX_ADDRESS;
2204
2205 for (log_idx, log) in logs.iter().enumerate() {
2206 if log.address != precompile_addr {
2207 continue;
2208 }
2209 if log.topics().is_empty() || log.topics()[0] != redeem_topic {
2210 continue;
2211 }
2212 if log.topics().len() < 4 || log.data.data.len() < 128 {
2213 continue;
2214 }
2215
2216 let ticket_id = log.topics()[1];
2217 let seq_num_bytes = log.topics()[3];
2218 let nonce =
2219 u64::from_be_bytes(seq_num_bytes.0[24..32].try_into().unwrap_or([0u8; 8]));
2220 let data = &log.data.data;
2221 let donated_gas = U256::from_be_slice(&data[0..32]).to::<u64>();
2222 total_donated_gas = total_donated_gas.saturating_add(donated_gas);
2223 let gas_donor = Address::from_slice(&data[44..64]);
2224 let max_refund = U256::from_be_slice(&data[64..96]);
2225 let submission_fee_refund = U256::from_be_slice(&data[96..128]);
2226
2227 let (encoded_retry_tx, latest_backlog) = {
2231 let current_time = {
2232 let block = self.inner.evm().block();
2233 revm::context::Block::timestamp(block).to::<u64>()
2234 };
2235 let chain_id = self.arb_ctx.chain_id;
2236 let basefee = self.arb_ctx.basefee;
2237 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
2238 let arb_state = ArbosState::open(db, SystemBurner::new(None, false))
2239 .map_err(BlockExecutionError::other)?;
2240 let state_ref = unsafe { arb_state.backing_storage.state_mut() };
2242
2243 let mut encoded_retry_tx = None;
2244 if let Ok(Some(retryable)) =
2245 arb_state
2246 .retryable_state
2247 .open_retryable(state_ref, ticket_id, current_time)
2248 {
2249 let _ = retryable.increment_num_tries(state_ref);
2250
2251 if let Ok(retry_tx) = retryable.make_tx(
2252 state_ref,
2253 U256::from(chain_id),
2254 nonce,
2255 basefee,
2256 donated_gas,
2257 ticket_id,
2258 gas_donor,
2259 max_refund,
2260 submission_fee_refund,
2261 ) {
2262 let mut encoded = Vec::new();
2263 encoded.push(ArbTxType::ArbitrumRetryTx.as_u8());
2264 alloy_rlp::Encodable::encode(&retry_tx, &mut encoded);
2265 let correct_hash = keccak256(&encoded);
2266 retry_tx_hash_fixes.push((log_idx, correct_hash));
2267 encoded_retry_tx = Some(encoded);
2268 }
2269 }
2270
2271 let _ = arb_state.l2_pricing_state.shrink_backlog(
2272 state_ref,
2273 donated_gas,
2274 MultiGas::default(),
2275 );
2276 let backlog = arb_state.l2_pricing_state.gas_backlog(state_ref).ok();
2277 (encoded_retry_tx, backlog)
2278 };
2279
2280 if let Some(encoded) = encoded_retry_tx
2281 && let Some(hooks) = self.arb_hooks.as_mut()
2282 {
2283 hooks.tx_proc.scheduled_txs.push(encoded);
2284 }
2285 if let Some(b) = latest_backlog {
2286 self.precompile_ctx.block.set_current_gas_backlog(b);
2287 }
2288 }
2289 }
2290
2291 if !retry_tx_hash_fixes.is_empty()
2295 && let ExecutionResult::Success { ref mut logs, .. } = output.result.result
2296 {
2297 for (log_idx, correct_hash) in &retry_tx_hash_fixes {
2298 if let Some(log) = logs.get_mut(*log_idx)
2299 && log.data.topics().len() > 2
2300 {
2301 let topics = log.data.topics_mut_unchecked();
2302 topics[2] = *correct_hash;
2303 }
2304 }
2305 }
2306
2307 let stylus_data_fee = if self.precompile_ctx.take_stylus_activation_addr().is_some()
2312 || self.precompile_ctx.take_stylus_keepalive_hash().is_some()
2313 {
2314 self.precompile_ctx.take_stylus_activation_data_fee()
2315 } else {
2316 U256::ZERO
2317 };
2318
2319 let stylus_multi_gas = self.precompile_ctx.stylus_multi_gas();
2325 let precompile_multi_gas = self.precompile_ctx.precompile_multi_gas();
2326 let dimensioned = stylus_multi_gas.saturating_add(precompile_multi_gas);
2327 let raw_gas_used = evm_gas_used.saturating_add(gas_refunded);
2334 let stylus_upfront_oog_gas = self.precompile_ctx.stylus_upfront_oog_gas();
2338 let dimensionable_gas = raw_gas_used.saturating_sub(stylus_upfront_oog_gas);
2339 let execution_multi_gas = match self.multi_gas_sink.lock().take() {
2340 Some(opcode_gas) => {
2341 let observed = intrinsic_multi_gas
2342 .saturating_add(opcode_gas)
2343 .saturating_add(dimensioned);
2344 let remainder = dimensionable_gas.saturating_sub(observed.single_gas());
2345 observed.saturating_add(MultiGas::computation_gas(remainder))
2346 }
2347 None => {
2348 let remainder = dimensionable_gas.saturating_sub(dimensioned.single_gas());
2349 dimensioned.saturating_add(MultiGas::computation_gas(remainder))
2350 }
2351 };
2352 debug_assert_eq!(
2356 execution_multi_gas.single_gas(),
2357 dimensionable_gas,
2358 "multi-gas split must total the dimensionable gas",
2359 );
2360 let mut charged_multi_gas =
2361 MultiGas::single_dim_gas(poster_gas).saturating_add(execution_multi_gas);
2362
2363 let gas_before_floor = output.result.result.gas_used();
2368 if calldata_floor_gas > gas_before_floor {
2369 let receipt_top_up = calldata_floor_gas - gas_before_floor;
2370 adjust_result_gas_used(&mut output.result.result, receipt_top_up);
2371 let dim_single = charged_multi_gas.single_gas();
2372 if calldata_floor_gas > dim_single {
2373 let dim_top_up = calldata_floor_gas - dim_single;
2374 charged_multi_gas =
2375 charged_multi_gas.saturating_add(MultiGas::l2_calldata_gas(dim_top_up));
2376 }
2377 }
2378
2379 let coinbase_tip_per_gas: u128 = effective_tip_per_gas;
2383 let capped_gas_price = should_drop_tip;
2384
2385 self.pending_tx = Some(PendingArbTx {
2386 sender,
2387 tx_gas_limit,
2388 arb_tx_type,
2389 poster_gas,
2390 evm_gas_used,
2391 charged_multi_gas,
2392 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
2393 stylus_data_fee,
2394 retry_context,
2395 coinbase_tip_per_gas,
2396 capped_gas_price,
2397 actual_gas_price,
2398 });
2399
2400 Ok(output)
2401 }
2402
2403 fn commit_transaction(&mut self, output: Self::Result) -> Result<u64, BlockExecutionError> {
2404 let pending = self.pending_tx.take();
2406 let gas_used_total = output.result.result.gas_used();
2407 let success = matches!(&output.result.result, ExecutionResult::Success { .. });
2408
2409 let mut withdrawal_value = U256::ZERO;
2413 if let ExecutionResult::Success { ref logs, .. } = output.result.result {
2414 let arbsys_addr = arb_precompiles::ARBSYS_ADDRESS;
2415 let l2_to_l1_tx_topic = keccak256(
2416 b"L2ToL1Tx(address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes)",
2417 );
2418 for log in logs {
2419 if log.address == arbsys_addr
2420 && !log.data.topics().is_empty()
2421 && log.data.topics()[0] == l2_to_l1_tx_topic
2422 {
2423 if log.data.data.len() >= 160 {
2426 let callvalue = U256::from_be_slice(&log.data.data[128..160]);
2427 withdrawal_value = withdrawal_value.saturating_add(callvalue);
2428 let val_i128: i128 = callvalue.try_into().unwrap_or(i128::MAX);
2429 self.expected_balance_delta =
2430 self.expected_balance_delta.saturating_sub(val_i128);
2431 }
2432 }
2433 }
2434 }
2435
2436 for addr in output.result.state.keys() {
2437 self.touched_accounts.insert(*addr);
2438 }
2439
2440 let gas_used = self.inner.commit_transaction(output)?;
2441
2442 if self
2446 .precompile_ctx
2447 .block
2448 .state_params_dirty
2449 .swap(false, std::sync::atomic::Ordering::Relaxed)
2450 {
2451 self.refresh_state_params();
2452 }
2453
2454 if let Some(ref p) = pending
2459 && !p.capped_gas_price
2460 && p.coinbase_tip_per_gas > 0
2461 && gas_used > 0
2462 {
2463 let coinbase = self.arb_ctx.coinbase;
2464 let net_acct = self.arb_ctx.network_fee_account;
2465 let compute_gas = gas_used.saturating_sub(p.poster_gas);
2466 let tip_to_network =
2467 U256::from(p.coinbase_tip_per_gas).saturating_mul(U256::from(compute_gas));
2468 if coinbase != net_acct && !tip_to_network.is_zero() {
2469 let overlay = &mut self.state_overlay;
2470 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
2471 if get_balance(db, coinbase) >= tip_to_network {
2472 let _ = arb_util::transfer_balance(
2473 Some(&coinbase),
2474 Some(&net_acct),
2475 tip_to_network,
2476 |f, t, a| apply_balance_op(db, overlay, f, t, a),
2477 );
2478 self.touched_accounts.insert(coinbase);
2479 self.touched_accounts.insert(net_acct);
2480 }
2481 }
2482 }
2483
2484 if let Some(ref p) = pending
2487 && !p.stylus_data_fee.is_zero()
2488 {
2489 let overlay = &mut self.state_overlay;
2490 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
2491 let _ = arb_util::burn_balance(&p.sender, p.stylus_data_fee, |f, t, a| {
2492 apply_balance_op(db, overlay, f, t, a)
2493 });
2494 let _ = arb_util::mint_balance(
2495 &self.arb_ctx.network_fee_account,
2496 p.stylus_data_fee,
2497 |f, t, a| apply_balance_op(db, overlay, f, t, a),
2498 );
2499 self.touched_accounts.insert(p.sender);
2500 self.touched_accounts
2501 .insert(self.arb_ctx.network_fee_account);
2502 }
2503
2504 if let Some((escrow, beneficiary)) = self.precompile_ctx.take_cancel_escrow_sweep() {
2508 let arbos_ver = self.arb_ctx.arbos_version;
2509 let touched_ptr = &mut self.touched_accounts as *mut rustc_hash::FxHashSet<Address>;
2510 let zombie_ptr = &mut self.zombie_accounts as *mut rustc_hash::FxHashSet<Address>;
2511 let finalise_ptr = &self.finalise_deleted as *const rustc_hash::FxHashSet<Address>;
2512 let overlay_ptr = &mut self.state_overlay as *mut StateOverlay;
2513 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
2514 let amount = get_balance(db, escrow);
2515
2516 unsafe {
2521 if amount.is_zero()
2522 && arbos_ver < arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS
2523 {
2524 create_zombie_if_deleted(
2525 db,
2526 &mut *overlay_ptr,
2527 escrow,
2528 &*finalise_ptr,
2529 &mut *zombie_ptr,
2530 &mut *touched_ptr,
2531 );
2532 }
2533 let _ = apply_balance_op(
2534 db,
2535 &mut *overlay_ptr,
2536 Some(&escrow),
2537 Some(&beneficiary),
2538 amount,
2539 );
2540 if !amount.is_zero() {
2541 (*zombie_ptr).remove(&escrow);
2542 }
2543 (*zombie_ptr).remove(&beneficiary);
2544 (*touched_ptr).insert(escrow);
2545 (*touched_ptr).insert(beneficiary);
2546 }
2547 }
2548
2549 if !withdrawal_value.is_zero() {
2551 let overlay = &mut self.state_overlay;
2552 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
2553 let _ = arb_util::burn_balance(
2554 &arb_precompiles::ARBSYS_ADDRESS,
2555 withdrawal_value,
2556 |f, t, a| apply_balance_op(db, overlay, f, t, a),
2557 );
2558 self.touched_accounts
2559 .insert(arb_precompiles::ARBSYS_ADDRESS);
2560 }
2561
2562 let poster_gas_for_receipt = pending.as_ref().map_or(0, |p| p.poster_gas);
2564 self.gas_used_for_l1.push(poster_gas_for_receipt);
2565 let multi_gas_for_receipt = pending
2566 .as_ref()
2567 .map_or(MultiGas::zero(), |p| p.charged_multi_gas);
2568 self.multi_gas_used.push(multi_gas_for_receipt);
2569
2570 if let Some(pending) = pending {
2572 let is_retry = pending.retry_context.is_some();
2573
2574 debug_assert!(
2576 gas_used_total <= pending.tx_gas_limit,
2577 "gas_used ({gas_used_total}) exceeds gas_limit ({})",
2578 pending.tx_gas_limit
2579 );
2580
2581 let sender_extra_gas = gas_used_total.saturating_sub(pending.evm_gas_used);
2586 if sender_extra_gas > 0 {
2587 let extra_cost = pending
2588 .actual_gas_price
2589 .saturating_mul(U256::from(sender_extra_gas));
2590 let overlay = &mut self.state_overlay;
2591 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
2592 let _ = arb_util::burn_balance(&pending.sender, extra_cost, |f, t, a| {
2593 apply_balance_op(db, overlay, f, t, a)
2594 });
2595 self.touched_accounts.insert(pending.sender);
2596 }
2597
2598 if let Some(retry_ctx) = pending.retry_context {
2599 let gas_left = pending.tx_gas_limit.saturating_sub(gas_used_total);
2601
2602 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
2603 let touched_ptr = &mut self.touched_accounts as *mut rustc_hash::FxHashSet<Address>;
2604 let zombie_ptr = &mut self.zombie_accounts as *mut rustc_hash::FxHashSet<Address>;
2605 let finalise_ptr = &self.finalise_deleted as *const rustc_hash::FxHashSet<Address>;
2606 let overlay_ptr = &mut self.state_overlay as *mut StateOverlay;
2607 let arbos_ver = self.arb_ctx.arbos_version;
2608
2609 let arb_state_retry = ArbosState::open(db, SystemBurner::new(None, false))
2610 .map_err(BlockExecutionError::other)?;
2611 let burn_storage = arb_state_retry.backing_storage.clone();
2615 let transfer_storage = arb_state_retry.backing_storage.clone();
2616 let delete_transfer_storage = arb_state_retry.backing_storage.clone();
2617 let delete_balance_storage = arb_state_retry.backing_storage.clone();
2618 let escrow_storage = arb_state_retry.backing_storage.clone();
2619
2620 let multi_dimensional_cost = if self.arb_ctx.arbos_version
2622 >= arb_chainspec::arbos_version::ARBOS_VERSION_MULTI_GAS_CONSTRAINTS
2623 {
2624 let cached = self.multi_gas_current_fees.get_or_init(|| {
2625 let state_ref = unsafe { arb_state_retry.backing_storage.state_mut() };
2627 arb_state_retry
2628 .l2_pricing_state
2629 .get_current_multi_gas_fees(state_ref)
2630 .unwrap_or([U256::ZERO; NUM_RESOURCE_KIND])
2631 });
2632 let state_ref = unsafe { arb_state_retry.backing_storage.state_mut() };
2634 arb_state_retry
2635 .l2_pricing_state
2636 .multi_dimensional_price_for_refund_with_fees(
2637 state_ref,
2638 pending.charged_multi_gas,
2639 cached,
2640 )
2641 .ok()
2642 } else {
2643 None
2644 };
2645
2646 let result = self.arb_hooks.as_ref().map(|hooks| {
2647 hooks.tx_proc.end_tx_retryable(
2648 &EndTxRetryableParams {
2649 gas_left,
2650 gas_used: gas_used_total,
2651 effective_base_fee: self.arb_ctx.basefee,
2652 from: pending.sender,
2653 refund_to: retry_ctx.refund_to,
2654 max_refund: retry_ctx.max_refund,
2655 submission_fee_refund: retry_ctx.submission_fee_refund,
2656 ticket_id: retry_ctx.ticket_id,
2657 value: U256::ZERO, success,
2659 network_fee_account: self.arb_ctx.network_fee_account,
2660 infra_fee_account: self.arb_ctx.infra_fee_account,
2661 min_base_fee: self.arb_ctx.min_base_fee,
2662 arbos_version: self.arb_ctx.arbos_version,
2663 multi_dimensional_cost,
2664 block_base_fee: self.arb_ctx.basefee,
2665 },
2666 |addr, amount| {
2667 unsafe {
2669 apply_burn_to_state(
2670 burn_storage.state_mut(),
2671 &mut *overlay_ptr,
2672 addr,
2673 amount,
2674 );
2675 (*touched_ptr).insert(addr);
2676 }
2677 },
2678 |from, to, amount| {
2679 unsafe {
2681 let state = transfer_storage.state_mut();
2682 if amount.is_zero()
2683 && arbos_ver
2684 < arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS
2685 {
2686 create_zombie_if_deleted(
2687 state,
2688 &mut *overlay_ptr,
2689 from,
2690 &*finalise_ptr,
2691 &mut *zombie_ptr,
2692 &mut *touched_ptr,
2693 );
2694 }
2695 let _ = apply_balance_op(
2700 state,
2701 &mut *overlay_ptr,
2702 Some(&from),
2703 Some(&to),
2704 amount,
2705 );
2706 if !amount.is_zero() {
2709 (*zombie_ptr).remove(&from);
2710 }
2711 (*zombie_ptr).remove(&to);
2713 (*touched_ptr).insert(from);
2714 (*touched_ptr).insert(to);
2715 }
2716 Ok(())
2717 },
2718 )
2719 });
2720
2721 if let Some(ref result) = result {
2722 if result.should_delete_retryable {
2723 let state_ref = unsafe { arb_state_retry.backing_storage.state_mut() };
2725 let _ = arb_state_retry.retryable_state.delete_retryable(
2726 state_ref,
2727 retry_ctx.ticket_id,
2728 |from, to, amount| {
2729 unsafe {
2731 let state = delete_transfer_storage.state_mut();
2732 if amount.is_zero()
2733 && arbos_ver
2734 < arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS
2735 {
2736 create_zombie_if_deleted(
2737 state,
2738 &mut *overlay_ptr,
2739 from,
2740 &*finalise_ptr,
2741 &mut *zombie_ptr,
2742 &mut *touched_ptr,
2743 );
2744 }
2745 let _ = apply_balance_op(
2752 state,
2753 &mut *overlay_ptr,
2754 Some(&from),
2755 Some(&to),
2756 amount,
2757 );
2758 if !amount.is_zero() {
2759 (*zombie_ptr).remove(&from);
2760 }
2761 (*zombie_ptr).remove(&to);
2762 (*touched_ptr).insert(from);
2763 (*touched_ptr).insert(to);
2764 }
2765 Ok(())
2766 },
2767 |addr| {
2768 unsafe { get_balance(delete_balance_storage.state_mut(), addr) }
2770 },
2771 );
2772 } else if result.should_return_value_to_escrow {
2773 unsafe {
2776 let state = escrow_storage.state_mut();
2777 if retry_ctx.call_value.is_zero()
2778 && arbos_ver < arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS
2779 {
2780 create_zombie_if_deleted(
2781 state,
2782 &mut *overlay_ptr,
2783 pending.sender,
2784 &*finalise_ptr,
2785 &mut *zombie_ptr,
2786 &mut *touched_ptr,
2787 );
2788 }
2789 let _ = arb_util::transfer_balance(
2790 Some(&pending.sender),
2791 Some(&result.escrow_address),
2792 retry_ctx.call_value,
2793 |f, t, a| {
2794 apply_balance_op(
2795 escrow_storage.state_mut(),
2796 &mut *overlay_ptr,
2797 f,
2798 t,
2799 a,
2800 )
2801 },
2802 );
2803 if !retry_ctx.call_value.is_zero() {
2805 (*zombie_ptr).remove(&pending.sender);
2806 }
2807 (*zombie_ptr).remove(&result.escrow_address);
2809 (*touched_ptr).insert(pending.sender);
2810 (*touched_ptr).insert(result.escrow_address);
2811 }
2812 }
2813
2814 let state_ref = unsafe { arb_state_retry.backing_storage.state_mut() };
2816 let _ = arb_state_retry.l2_pricing_state.grow_backlog(
2817 state_ref,
2818 result.compute_gas_for_backlog,
2819 pending.charged_multi_gas,
2820 );
2821 if let Ok(b) = arb_state_retry.l2_pricing_state.gas_backlog(state_ref) {
2822 self.precompile_ctx.block.set_current_gas_backlog(b);
2823 }
2824 }
2825 } else if matches!(
2826 pending.arb_tx_type,
2827 None | Some(ArbTxType::ArbitrumLegacyTx)
2828 | Some(ArbTxType::ArbitrumUnsignedTx)
2829 | Some(ArbTxType::ArbitrumContractTx)
2830 ) {
2831 let gas_left = pending.tx_gas_limit.saturating_sub(gas_used_total);
2835
2836 let fee_dist = self.arb_hooks.as_ref().map(|hooks| {
2837 hooks.compute_end_tx_fees(&EndTxContext {
2838 sender: pending.sender,
2839 gas_left,
2840 gas_used: gas_used_total,
2841 gas_price: self.arb_ctx.basefee,
2842 base_fee: self.arb_ctx.basefee,
2843 tx_type: pending.arb_tx_type.unwrap_or(ArbTxType::ArbitrumLegacyTx),
2844 success,
2845 refund_to: pending.sender,
2846 })
2847 });
2848
2849 if let Some(ref dist) = fee_dist {
2850 {
2851 let overlay = &mut self.state_overlay;
2852 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
2853 apply_fee_distribution(db, overlay, dist, None);
2854 }
2855 if !dist.network_fee_amount.is_zero() {
2858 self.touched_accounts.insert(dist.network_fee_account);
2859 }
2860 self.touched_accounts.insert(dist.infra_fee_account);
2861 self.touched_accounts.insert(dist.poster_fee_destination);
2862
2863 let arbos_version_active = self.arb_ctx.arbos_version;
2864 let basefee_active = self.arb_ctx.basefee;
2865 let charged_multi_gas = pending.charged_multi_gas;
2866 let poster_gas_active = pending.poster_gas;
2867 let gas_price_positive_active = pending.gas_price_positive;
2868
2869 let (refund_done, new_backlog) = {
2870 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
2871 let arb_state_post = ArbosState::open(db, SystemBurner::new(None, false))
2872 .map_err(BlockExecutionError::other)?;
2873 let refund_storage = arb_state_post.backing_storage.clone();
2878 let overlay_ptr = &mut self.state_overlay as *mut StateOverlay;
2879
2880 let mut refund_done = false;
2881 if arbos_version_active
2882 >= arb_chainspec::arbos_version::ARBOS_VERSION_MULTI_GAS_CONSTRAINTS
2883 {
2884 let total_cost =
2885 basefee_active.saturating_mul(U256::from(gas_used_total));
2886 let cached = self.multi_gas_current_fees.get_or_init(|| {
2887 let state_ref =
2889 unsafe { arb_state_post.backing_storage.state_mut() };
2890 arb_state_post
2891 .l2_pricing_state
2892 .get_current_multi_gas_fees(state_ref)
2893 .unwrap_or([U256::ZERO; NUM_RESOURCE_KIND])
2894 });
2895 let state_ref = unsafe { arb_state_post.backing_storage.state_mut() };
2897 let multi_cost = arb_state_post
2898 .l2_pricing_state
2899 .multi_dimensional_price_for_refund_with_fees(
2900 state_ref,
2901 charged_multi_gas,
2902 cached,
2903 )
2904 .unwrap_or(total_cost);
2905 if total_cost > multi_cost {
2906 let refund_amount = total_cost.saturating_sub(multi_cost);
2907 let _ = arb_util::transfer_balance(
2908 Some(&dist.network_fee_account),
2909 Some(&pending.sender),
2910 refund_amount,
2911 |f, t, a| {
2912 unsafe {
2914 apply_balance_op(
2915 refund_storage.state_mut(),
2916 &mut *overlay_ptr,
2917 f,
2918 t,
2919 a,
2920 )
2921 }
2922 },
2923 );
2924 refund_done = true;
2925 }
2926 }
2927
2928 let used_multi_gas = charged_multi_gas
2933 .saturating_sub(MultiGas::single_dim_gas(poster_gas_active));
2934
2935 let mut new_backlog: Option<u64> = None;
2936 if gas_price_positive_active {
2937 let state_ref = unsafe { arb_state_post.backing_storage.state_mut() };
2939 let _ = arb_state_post.l2_pricing_state.grow_backlog(
2940 state_ref,
2941 dist.compute_gas_for_backlog,
2942 used_multi_gas,
2943 );
2944 new_backlog =
2945 arb_state_post.l2_pricing_state.gas_backlog(state_ref).ok();
2946 }
2947 if !dist.l1_fees_to_add.is_zero() {
2948 let state_ref = unsafe { arb_state_post.backing_storage.state_mut() };
2950 let _ = arb_state_post
2951 .l1_pricing_state
2952 .add_to_l1_fees_available(state_ref, dist.l1_fees_to_add);
2953 }
2954
2955 (refund_done, new_backlog)
2956 };
2957
2958 if refund_done {
2959 self.touched_accounts.insert(dist.network_fee_account);
2960 self.touched_accounts.insert(pending.sender);
2961 }
2962 if let Some(b) = new_backlog {
2963 self.precompile_ctx.block.set_current_gas_backlog(b);
2964 }
2965 }
2966 }
2967
2968 let mut adjusted_gas_used = gas_used_total;
2972 if self.arb_ctx.arbos_version
2973 >= arb_chainspec::arbos_version::ARBOS_VERSION_FIX_REDEEM_GAS
2974 && let Some(hooks) = self.arb_hooks.as_ref()
2975 {
2976 for scheduled in &hooks.tx_proc.scheduled_txs {
2977 if let Some(retry_gas) = decode_retry_tx_gas(scheduled) {
2978 adjusted_gas_used = adjusted_gas_used.saturating_sub(retry_gas);
2979 }
2980 }
2981 }
2982
2983 const TX_GAS: u64 = 21_000;
2985 let data_gas = pending.poster_gas;
2986 let compute_used = if adjusted_gas_used < data_gas {
2987 TX_GAS
2988 } else {
2989 let compute = adjusted_gas_used - data_gas;
2990 if compute < TX_GAS { TX_GAS } else { compute }
2991 };
2992 self.block_gas_left = self.block_gas_left.saturating_sub(compute_used);
2993
2994 let is_user_tx = !matches!(
2996 pending.arb_tx_type,
2997 Some(ArbTxType::ArbitrumInternalTx)
2998 | Some(ArbTxType::ArbitrumDepositTx)
2999 | Some(ArbTxType::ArbitrumSubmitRetryableTx)
3000 | Some(ArbTxType::ArbitrumRetryTx)
3001 );
3002 if is_user_tx {
3003 self.user_txs_processed += 1;
3004 }
3005
3006 let _ = is_retry; }
3008
3009 self.precompile_ctx.reset_tx();
3010
3011 {
3021 let keccak_empty = alloy_primitives::B256::from(alloy_primitives::keccak256([]));
3022 let overlay = &mut self.state_overlay;
3023 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
3024 let to_remove: Vec<Address> = self
3025 .touched_accounts
3026 .drain()
3027 .filter(|addr| {
3028 if self.zombie_accounts.contains(addr) {
3030 return false;
3031 }
3032 if let Some(cached) = db.cache.accounts.get(addr)
3033 && let Some(ref acct) = cached.account
3034 {
3035 let is_empty = acct.info.nonce == 0
3036 && acct.info.balance.is_zero()
3037 && acct.info.code_hash == keccak_empty;
3038 return is_empty;
3039 }
3040 false
3041 })
3042 .collect();
3043
3044 for addr in &to_remove {
3052 overlay.record_pre_touch(db, *addr);
3053 if let Some(cached) = db.cache.accounts.get_mut(addr) {
3054 cached.account = None;
3055 cached.status = revm_database::AccountStatus::LoadedNotExisting;
3056 }
3057 }
3058 self.finalise_deleted.extend(to_remove);
3059 }
3060
3061 {
3062 let overlay = &mut self.state_overlay;
3063 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
3064 overlay.drain_and_apply(db, &self.zombie_accounts);
3065 }
3066
3067 Ok(gas_used)
3068 }
3069
3070 fn finish(self) -> Result<(Self::Evm, BlockExecutionResult<R::Receipt>), BlockExecutionError> {
3071 if self.expected_balance_delta != 0 {
3073 tracing::trace!(
3074 target: "arb::executor",
3075 delta = self.expected_balance_delta,
3076 "expected balance delta from deposits/withdrawals"
3077 );
3078 }
3079 let mut result = BlockExecutionResult {
3083 receipts: self.inner.receipts,
3084 requests: Default::default(),
3085 gas_used: self.inner.gas_used,
3086 blob_gas_used: self.inner.blob_gas_used,
3087 };
3088 for (i, receipt) in result.receipts.iter_mut().enumerate() {
3090 if let Some(&l1_gas) = self.gas_used_for_l1.get(i) {
3091 arb_primitives::SetArbReceiptFields::set_gas_used_for_l1(receipt, l1_gas);
3092 }
3093 if let Some(&multi_gas) = self.multi_gas_used.get(i) {
3094 arb_primitives::SetArbReceiptFields::set_multi_gas_used(receipt, multi_gas);
3095 }
3096 }
3097 Ok((self.inner.evm, result))
3098 }
3099
3100 fn set_state_hook(&mut self, hook: Option<Box<dyn OnStateHook>>) {
3101 self.inner.set_state_hook(hook);
3102 }
3103
3104 fn evm_mut(&mut self) -> &mut Self::Evm {
3105 self.inner.evm_mut()
3106 }
3107
3108 fn evm(&self) -> &Self::Evm {
3109 self.inner.evm()
3110 }
3111
3112 fn receipts(&self) -> &[Self::Receipt] {
3113 self.inner.receipts()
3114 }
3115}
3116
3117fn synthetic_result_gas(gas_used: u64) -> revm::context::result::ResultGas {
3123 revm::context::result::ResultGas::new(gas_used, gas_used, 0, 0, 0)
3124}
3125
3126fn adjust_result_gas_used<H>(result: &mut ExecutionResult<H>, extra_gas: u64) {
3131 match result {
3132 ExecutionResult::Success { gas, .. }
3133 | ExecutionResult::Revert { gas, .. }
3134 | ExecutionResult::Halt { gas, .. } => gas.set_spent(gas.spent().saturating_add(extra_gas)),
3135 }
3136}
3137
3138fn apply_mint_to_state<DB: Database>(
3140 state: &mut State<DB>,
3141 overlay: &mut StateOverlay,
3142 address: Address,
3143 amount: U256,
3144) {
3145 if amount.is_zero() {
3146 return;
3147 }
3148 overlay.record_pre_touch(state, address);
3149 if let Some(cache_acct) = state.cache.accounts.get_mut(&address) {
3150 if let Some(ref mut acct) = cache_acct.account {
3151 acct.info.balance = acct.info.balance.saturating_add(amount);
3152 } else {
3153 cache_acct.account = Some(revm_database::states::plain_account::PlainAccount {
3154 info: revm_state::AccountInfo {
3155 balance: amount,
3156 ..Default::default()
3157 },
3158 storage: Default::default(),
3159 });
3160 }
3161 }
3162}
3163
3164fn materialise_empty<DB: Database>(
3169 state: &mut State<DB>,
3170 overlay: &mut StateOverlay,
3171 addr: Address,
3172 touched: &mut rustc_hash::FxHashSet<Address>,
3173) {
3174 overlay.record_pre_touch(state, addr);
3175 let _ = state.load_cache_account(addr);
3176 if let Some(cached) = state.cache.accounts.get_mut(&addr)
3177 && cached.account.is_none()
3178 {
3179 cached.account = Some(revm_database::states::plain_account::PlainAccount {
3180 info: revm_state::AccountInfo::default(),
3181 storage: Default::default(),
3182 });
3183 cached.status = revm_database::AccountStatus::InMemoryChange;
3184 }
3185 touched.insert(addr);
3186}
3187
3188fn apply_burn_to_state<DB: Database>(
3190 state: &mut State<DB>,
3191 overlay: &mut StateOverlay,
3192 address: Address,
3193 amount: U256,
3194) {
3195 if amount.is_zero() {
3196 return;
3197 }
3198 overlay.record_pre_touch(state, address);
3199 if let Some(cache_acct) = state.cache.accounts.get_mut(&address)
3200 && let Some(ref mut acct) = cache_acct.account
3201 {
3202 acct.info.balance = acct.info.balance.saturating_sub(amount);
3203 }
3204}
3205
3206fn apply_balance_op<DB: Database>(
3214 state: &mut State<DB>,
3215 overlay: &mut StateOverlay,
3216 from: Option<&Address>,
3217 to: Option<&Address>,
3218 amount: U256,
3219) -> Result<(), BalanceError> {
3220 if amount.is_zero() {
3221 return Ok(());
3222 }
3223 match (from, to) {
3224 (Some(from_addr), Some(to_addr)) => {
3225 let available = get_balance(state, *from_addr);
3226 if available < amount {
3227 return Err(BalanceError::InsufficientBalance {
3228 account: *from_addr,
3229 available,
3230 requested: amount,
3231 });
3232 }
3233 apply_burn_to_state(state, overlay, *from_addr, amount);
3234 apply_mint_to_state(state, overlay, *to_addr, amount);
3235 }
3236 (Some(from_addr), None) => {
3237 apply_burn_to_state(state, overlay, *from_addr, amount);
3238 }
3239 (None, Some(to_addr)) => {
3240 apply_mint_to_state(state, overlay, *to_addr, amount);
3241 }
3242 (None, None) => {}
3243 }
3244 Ok(())
3245}
3246
3247fn increment_nonce<DB: Database>(
3249 state: &mut State<DB>,
3250 overlay: &mut StateOverlay,
3251 address: Address,
3252) {
3253 overlay.record_pre_touch(state, address);
3254 if let Some(cache_acct) = state.cache.accounts.get_mut(&address)
3255 && let Some(ref mut acct) = cache_acct.account
3256 {
3257 acct.info.nonce += 1;
3258 }
3259}
3260
3261fn get_balance<DB: Database>(state: &mut State<DB>, address: Address) -> U256 {
3263 match revm::Database::basic(state, address) {
3264 Ok(Some(info)) => info.balance,
3265 _ => U256::ZERO,
3266 }
3267}
3268
3269fn create_zombie_if_deleted<DB: Database>(
3275 state: &mut State<DB>,
3276 overlay: &mut StateOverlay,
3277 addr: Address,
3278 finalise_deleted: &rustc_hash::FxHashSet<Address>,
3279 zombie_accounts: &mut rustc_hash::FxHashSet<Address>,
3280 touched_accounts: &mut rustc_hash::FxHashSet<Address>,
3281) {
3282 overlay.record_pre_touch(state, addr);
3283 let account_missing = state
3284 .cache
3285 .accounts
3286 .get(&addr)
3287 .is_none_or(|c| c.account.is_none());
3288 if account_missing && finalise_deleted.contains(&addr) {
3289 if let Some(cached) = state.cache.accounts.get_mut(&addr) {
3290 cached.account = Some(revm_database::states::plain_account::PlainAccount {
3291 info: revm_state::AccountInfo::default(),
3292 storage: Default::default(),
3293 });
3294 cached.status = revm_database::AccountStatus::InMemoryChange;
3295 }
3296 zombie_accounts.insert(addr);
3297 touched_accounts.insert(addr);
3298 }
3299}
3300
3301fn apply_fee_distribution<DB: Database>(
3303 state: &mut State<DB>,
3304 overlay: &mut StateOverlay,
3305 dist: &EndTxFeeDistribution,
3306 l1_pricing: Option<&l1_pricing::L1PricingState<DB>>,
3307) {
3308 if !dist.network_fee_amount.is_zero() {
3311 let _ = arb_util::mint_balance(
3312 &dist.network_fee_account,
3313 dist.network_fee_amount,
3314 |f, t, a| apply_balance_op(state, overlay, f, t, a),
3315 );
3316 }
3317 let _ = arb_util::mint_balance(&dist.infra_fee_account, dist.infra_fee_amount, |f, t, a| {
3318 apply_balance_op(state, overlay, f, t, a)
3319 });
3320 let _ = arb_util::mint_balance(
3321 &dist.poster_fee_destination,
3322 dist.poster_fee_amount,
3323 |f, t, a| apply_balance_op(state, overlay, f, t, a),
3324 );
3325
3326 if !dist.l1_fees_to_add.is_zero()
3327 && let Some(l1_state) = l1_pricing
3328 {
3329 let _ = l1_state.add_to_l1_fees_available(state, dist.l1_fees_to_add);
3330 }
3331
3332 tracing::trace!(
3333 target: "arb::executor",
3334 network_fee = %dist.network_fee_amount,
3335 infra_fee = %dist.infra_fee_amount,
3336 poster_fee = %dist.poster_fee_amount,
3337 poster_dest = %dist.poster_fee_destination,
3338 l1_fees_added = %dist.l1_fees_to_add,
3339 backlog_gas = dist.compute_gas_for_backlog,
3340 "applied fee distribution"
3341 );
3342}
3343
3344fn estimate_intrinsic_gas(tx: &impl Transaction, spec: revm::primitives::hardfork::SpecId) -> u64 {
3350 const TX_GAS: u64 = 21_000;
3351 const TX_CREATE_GAS: u64 = 32_000;
3352 const TX_DATA_ZERO_GAS: u64 = 4;
3353 const TX_DATA_NON_ZERO_GAS: u64 = 16;
3354 const TX_ACCESS_LIST_ADDRESS_GAS: u64 = 2400;
3355 const TX_ACCESS_LIST_STORAGE_KEY_GAS: u64 = 1900;
3356 const INIT_CODE_WORD_GAS: u64 = 2;
3357
3358 let is_create = tx.to().is_none();
3359
3360 let mut gas = TX_GAS;
3361 if is_create {
3362 gas += TX_CREATE_GAS;
3363 }
3364
3365 let data = tx.input();
3366
3367 let data_gas: u64 = data
3369 .iter()
3370 .map(|&b| {
3371 if b == 0 {
3372 TX_DATA_ZERO_GAS
3373 } else {
3374 TX_DATA_NON_ZERO_GAS
3375 }
3376 })
3377 .sum();
3378 gas = gas.saturating_add(data_gas);
3379
3380 if let Some(access_list) = tx.access_list() {
3382 for item in access_list.iter() {
3383 gas = gas.saturating_add(TX_ACCESS_LIST_ADDRESS_GAS);
3384 gas = gas.saturating_add(
3385 (item.storage_keys.len() as u64).saturating_mul(TX_ACCESS_LIST_STORAGE_KEY_GAS),
3386 );
3387 }
3388 }
3389
3390 if spec.is_enabled_in(revm::primitives::hardfork::SpecId::SHANGHAI)
3392 && is_create
3393 && !data.is_empty()
3394 {
3395 let words = (data.len() as u64).div_ceil(32);
3396 gas = gas.saturating_add(words.saturating_mul(INIT_CODE_WORD_GAS));
3397 }
3398
3399 gas
3400}
3401
3402fn tx_intrinsic_multi_gas(
3407 tx: &impl Transaction,
3408 spec: revm::primitives::hardfork::SpecId,
3409) -> MultiGas {
3410 let is_create = tx.to().is_none();
3411 let data = tx.input();
3412 let zero_bytes = data.iter().filter(|&&b| b == 0).count() as u64;
3413 let nonzero_bytes = data.len() as u64 - zero_bytes;
3414 let (access_list_addresses, access_list_keys) = tx.access_list().map_or((0, 0), |al| {
3415 let mut addrs = 0u64;
3416 let mut keys = 0u64;
3417 for item in al.iter() {
3418 addrs += 1;
3419 keys += item.storage_keys.len() as u64;
3420 }
3421 (addrs, keys)
3422 });
3423 let init_code_words = if is_create
3424 && spec.is_enabled_in(revm::primitives::hardfork::SpecId::SHANGHAI)
3425 && !data.is_empty()
3426 {
3427 (data.len() as u64).div_ceil(32)
3428 } else {
3429 0
3430 };
3431 crate::multi_gas::intrinsic_multigas(crate::multi_gas::IntrinsicInput {
3432 is_create,
3433 zero_bytes,
3434 nonzero_bytes,
3435 init_code_words,
3436 access_list_addresses,
3437 access_list_keys,
3438 auth_list_len: tx.authorization_list().map_or(0, |l| l.len()) as u64,
3439 })
3440}
3441
3442fn tx_floor_data_gas(tx: &impl Transaction) -> u64 {
3446 const TX_GAS: u64 = 21_000;
3447 const TX_TOKEN_PER_NONZERO_BYTE: u64 = 4;
3448 const TX_COST_FLOOR_PER_TOKEN: u64 = 10;
3449 let data = tx.input();
3450 let zero = data.iter().filter(|&&b| b == 0).count() as u64;
3451 let nonzero = (data.len() as u64).saturating_sub(zero);
3452 let tokens = nonzero
3453 .saturating_mul(TX_TOKEN_PER_NONZERO_BYTE)
3454 .saturating_add(zero);
3455 TX_GAS.saturating_add(tokens.saturating_mul(TX_COST_FLOOR_PER_TOKEN))
3456}
3457
3458fn decode_extra_fields(extra_bytes: &[u8]) -> (u64, u64) {
3461 let delayed = if extra_bytes.len() >= 40 {
3462 let mut buf = [0u8; 8];
3463 buf.copy_from_slice(&extra_bytes[32..40]);
3464 u64::from_be_bytes(buf)
3465 } else {
3466 0
3467 };
3468 let l2_block = if extra_bytes.len() >= 48 {
3469 let mut buf = [0u8; 8];
3470 buf.copy_from_slice(&extra_bytes[40..48]);
3471 u64::from_be_bytes(buf)
3472 } else {
3473 0
3474 };
3475 (delayed, l2_block)
3476}
3477
3478fn process_parent_block_hash<DB: Database>(
3482 state: &mut State<DB>,
3483 l2_block_number: u64,
3484 prev_hash: B256,
3485) {
3486 use arb_primitives::arbos_versions::HISTORY_STORAGE_ADDRESS;
3487
3488 const HISTORY_SERVE_WINDOW: u64 = 393168;
3490
3491 if l2_block_number == 0 {
3492 return;
3493 }
3494
3495 let slot = U256::from((l2_block_number - 1) % HISTORY_SERVE_WINDOW);
3496 let value = U256::from_be_slice(prev_hash.as_slice());
3497
3498 arb_storage::write_storage_at(state, HISTORY_STORAGE_ADDRESS, slot, value)
3499 .expect("HISTORY_STORAGE write must succeed: in-memory state writes are infallible");
3500}
3501
3502fn decode_retry_tx_gas(encoded: &[u8]) -> Option<u64> {
3506 if encoded.is_empty() {
3507 return None;
3508 }
3509 if encoded[0] != ArbTxType::ArbitrumRetryTx.as_u8() {
3510 tracing::warn!(
3511 target: "arb::executor",
3512 tx_type = encoded[0],
3513 "unexpected scheduled tx type"
3514 );
3515 return None;
3516 }
3517 let rlp_data = &encoded[1..];
3518 let retry =
3519 <arb_alloy_consensus::tx::ArbRetryTx as alloy_rlp::Decodable>::decode(&mut &rlp_data[..])
3520 .ok()?;
3521 Some(retry.gas)
3522}
3523
3524#[cfg(test)]
3525mod tests {
3526 use std::collections::HashMap;
3527
3528 use alloy_primitives::{B256, U256};
3529 use arb_context::BlockCtx;
3530
3531 use super::populate_l2_block_hash_window;
3532
3533 #[test]
3534 fn fills_window_from_parent_and_lookup() {
3535 let block = BlockCtx::new(60, 0, 0, 1000, false);
3536 let parent_hash = B256::repeat_byte(0xfe);
3537 let mut db: HashMap<u64, B256> = (744..=998)
3538 .map(|n| (n, B256::from(U256::from(n))))
3539 .collect();
3540 populate_l2_block_hash_window(&block, 1000, parent_hash, |n| db.remove(&n));
3541
3542 assert_eq!(block.cached_l2_block_hash(999), Some(parent_hash));
3543 assert_eq!(
3544 block.cached_l2_block_hash(998),
3545 Some(B256::from(U256::from(998u64)))
3546 );
3547 assert_eq!(
3548 block.cached_l2_block_hash(744),
3549 Some(B256::from(U256::from(744u64)))
3550 );
3551 assert_eq!(block.cached_l2_block_hash(743), None);
3552 }
3553
3554 #[test]
3555 fn keeps_chunk_internal_and_stops_at_gap() {
3556 let block = BlockCtx::new(60, 0, 0, 1000, false);
3557 populate_l2_block_hash_window(&block, 1000, B256::repeat_byte(0xfe), |n| {
3558 (n == 997).then(|| B256::repeat_byte(0x97))
3559 });
3560 assert_eq!(
3561 block.cached_l2_block_hash(999),
3562 Some(B256::repeat_byte(0xfe))
3563 );
3564 assert_eq!(block.cached_l2_block_hash(998), None);
3565 assert_eq!(block.cached_l2_block_hash(997), None);
3566 }
3567}