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,
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::TransactionEnv;
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: TransactionEnv {
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<&'a mut State<DB>, I>,
205 ctx: Self::ExecutionCtx<'a>,
206 ) -> impl BlockExecutorFor<'a, Self, DB, I>
207 where
208 DB: Database + 'a,
209 I: Inspector<EvmF::Context<&'a mut State<DB>>> + 'a,
210 {
211 let extra_bytes = ctx.extra_data.as_ref();
212 let (delayed_messages_read, l2_block_number) = decode_extra_fields(extra_bytes);
213 let arb_ctx = ArbBlockExecutionCtx {
214 parent_hash: ctx.parent_hash,
215 parent_beacon_block_root: ctx.parent_beacon_block_root,
216 extra_data: extra_bytes[..core::cmp::min(extra_bytes.len(), 32)].to_vec(),
217 delayed_messages_read,
218 l2_block_number,
219 ..Default::default()
220 };
221 ArbBlockExecutor {
222 inner: EthBlockExecutor::new(evm, ctx, &self.spec, &self.receipt_builder),
223 arb_hooks: None,
224 arb_ctx,
225 precompile_ctx: <EvmF as crate::evm::ArbEvmFactoryStaged>::staged_precompile_ctx(
229 &self.evm_factory,
230 )
231 .unwrap_or_default(),
232 pending_tx: None,
233 block_gas_left: 0,
234 user_txs_processed: 0,
235 gas_used_for_l1: Vec::new(),
236 multi_gas_used: Vec::new(),
237 expected_balance_delta: 0,
238 zombie_accounts: rustc_hash::FxHashSet::default(),
239 finalise_deleted: rustc_hash::FxHashSet::default(),
240 touched_accounts: rustc_hash::FxHashSet::default(),
241 multi_gas_current_fees: std::sync::OnceLock::new(),
242 state_overlay: StateOverlay::new(),
243 multi_gas_sink: crate::multi_gas::MultiGasSink::default(),
244 }
245 }
246}
247
248struct PendingArbTx {
254 sender: Address,
255 tx_gas_limit: u64,
256 arb_tx_type: Option<ArbTxType>,
257 poster_gas: u64,
258 evm_gas_used: u64,
260 charged_multi_gas: MultiGas,
261 gas_price_positive: bool,
262 stylus_data_fee: U256,
263 retry_context: Option<PendingRetryContext>,
264 coinbase_tip_per_gas: u128,
265 capped_gas_price: bool,
269 actual_gas_price: U256,
273}
274
275struct PendingRetryContext {
277 ticket_id: alloy_primitives::B256,
278 refund_to: Address,
279 max_refund: U256,
280 submission_fee_refund: U256,
281 call_value: U256,
283}
284
285pub struct ArbBlockExecutor<'a, Evm, Spec, R: ReceiptBuilder> {
293 pub inner: EthBlockExecutor<'a, Evm, Spec, R>,
295 pub arb_hooks: Option<DefaultArbOsHooks>,
297 pub arb_ctx: ArbBlockExecutionCtx,
299 pub precompile_ctx: std::sync::Arc<arb_context::ArbPrecompileCtx>,
301 pending_tx: Option<PendingArbTx>,
303 pub block_gas_left: u64,
306 user_txs_processed: u64,
309 pub gas_used_for_l1: Vec<u64>,
312 pub multi_gas_used: Vec<MultiGas>,
314 expected_balance_delta: i128,
317 zombie_accounts: rustc_hash::FxHashSet<Address>,
320 finalise_deleted: rustc_hash::FxHashSet<Address>,
323 touched_accounts: rustc_hash::FxHashSet<Address>,
326 multi_gas_current_fees: std::sync::OnceLock<[U256; NUM_RESOURCE_KIND]>,
333 state_overlay: StateOverlay,
337 multi_gas_sink: crate::multi_gas::MultiGasSink,
341}
342
343impl<'a, Evm, Spec, R: ReceiptBuilder> ArbBlockExecutor<'a, Evm, Spec, R> {
344 pub fn with_hooks(mut self, hooks: DefaultArbOsHooks) -> Self {
346 self.arb_hooks = Some(hooks);
347 self
348 }
349
350 pub fn with_arb_ctx(mut self, ctx: ArbBlockExecutionCtx) -> Self {
352 self.arb_ctx = ctx;
353 self
354 }
355
356 pub fn set_multi_gas_sink(&mut self, sink: crate::multi_gas::MultiGasSink) {
359 self.multi_gas_sink = sink;
360 }
361
362 pub fn zombie_accounts(&self) -> rustc_hash::FxHashSet<Address> {
368 self.zombie_accounts.clone()
369 }
370
371 pub fn finalise_deleted(&self) -> &rustc_hash::FxHashSet<Address> {
374 &self.finalise_deleted
375 }
376
377 pub fn deduct_failed_tx_gas(&mut self, is_user_tx: bool) {
382 const TX_GAS: u64 = 21_000;
383 self.block_gas_left = self.block_gas_left.saturating_sub(TX_GAS);
384 if is_user_tx {
385 self.user_txs_processed += 1;
386 }
387 }
388
389 pub fn drain_scheduled_txs(&mut self) -> Vec<Vec<u8>> {
393 self.arb_hooks
394 .as_mut()
395 .map(|hooks| std::mem::take(&mut hooks.tx_proc.scheduled_txs))
396 .unwrap_or_default()
397 }
398}
399
400fn load_state_params<D: Database>(
405 arb_ctx: &mut ArbBlockExecutionCtx,
406 precompile_ctx: &mut std::sync::Arc<arb_context::ArbPrecompileCtx>,
407 arb_hooks: &mut Option<DefaultArbOsHooks>,
408 state: &mut revm::database::State<D>,
409 arb_state: &ArbosState<D, impl arbos::burn::Burner>,
410) {
411 let arbos_version = arb_state.arbos_version();
412 arb_ctx.arbos_version = arbos_version;
413
414 precompile_ctx.reset_tx();
421 precompile_ctx.reset_caller_stack();
422 precompile_ctx
423 .block
424 .cache_l1_block_number(arb_ctx.l2_block_number, arb_ctx.l1_block_number);
425
426 if arbos_version >= arb_chainspec::arbos_version::ARBOS_VERSION_60 {
427 let cap = arb_state
428 .programs
429 .params(state)
430 .map(|p| p.block_cache_size as usize)
431 .unwrap_or(0);
432 precompile_ctx.block.reset_recent_wasms(cap);
433 } else {
434 precompile_ctx.block.reset_recent_wasms(0);
435 }
436
437 if let Ok(backlog) = arb_state.l2_pricing_state.gas_backlog(state) {
438 precompile_ctx.block.set_current_gas_backlog(backlog);
439 }
440
441 if let Ok(addr) = arb_state.network_fee_account(state) {
442 arb_ctx.network_fee_account = addr;
443 }
444 if let Ok(addr) = arb_state.infra_fee_account(state) {
445 arb_ctx.infra_fee_account = addr;
446 }
447 if let Ok(level) = arb_state.brotli_compression_level(state) {
448 arb_ctx.brotli_compression_level = level;
449 }
450 if let Ok(price) = arb_state.l1_pricing_state.price_per_unit(state) {
451 arb_ctx.l1_price_per_unit = price;
452 }
453 if let Ok(min_fee) = arb_state.l2_pricing_state.min_base_fee_wei(state) {
454 arb_ctx.min_base_fee = min_fee;
455 }
456
457 let per_block_gas_limit = arb_state
458 .l2_pricing_state
459 .per_block_gas_limit(state)
460 .unwrap_or(0);
461 let per_tx_gas_limit = arb_state
462 .l2_pricing_state
463 .per_tx_gas_limit(state)
464 .unwrap_or(0);
465
466 let calldata_pricing_increase_enabled = arbos_version
467 >= arb_chainspec::arbos_version::ARBOS_VERSION_40
468 && arb_state
469 .features
470 .is_increased_calldata_price_enabled(state)
471 .unwrap_or(false);
472
473 let collect_tips_enabled = arb_state.collect_tips(state).unwrap_or(false);
474
475 let hooks = DefaultArbOsHooks::new(
476 arb_ctx.coinbase,
477 arbos_version,
478 arb_ctx.network_fee_account,
479 arb_ctx.infra_fee_account,
480 arb_ctx.min_base_fee,
481 per_block_gas_limit,
482 per_tx_gas_limit,
483 false,
484 arb_ctx.l1_base_fee,
485 calldata_pricing_increase_enabled,
486 collect_tips_enabled,
487 );
488 *arb_hooks = Some(hooks);
489}
490
491fn populate_l2_block_hash_window(
495 block: &arb_context::BlockCtx,
496 current_l2: u64,
497 parent_hash: B256,
498 mut lookup: impl FnMut(u64) -> Option<B256>,
499) {
500 let Some(parent) = current_l2.checked_sub(1) else {
501 return;
502 };
503 block.cache_l2_block_hash(parent, parent_hash);
504 for offset in 2..=256u64 {
505 let Some(n) = current_l2.checked_sub(offset) else {
506 break;
507 };
508 if block.cached_l2_block_hash(n).is_some() {
509 continue;
510 }
511 match lookup(n) {
512 Some(hash) => block.cache_l2_block_hash(n, hash),
513 None => break,
514 }
515 }
516}
517
518impl<'db, DB, E, Spec, R> ArbBlockExecutor<'_, E, Spec, R>
519where
520 DB: Database + 'db,
521 E: Evm<
522 DB = &'db mut State<DB>,
523 Tx: FromRecoveredTx<R::Transaction>
524 + FromTxWithEncoded<R::Transaction>
525 + ArbTransactionEnv,
526 >,
527 Spec: EthExecutorSpec,
528 R: ReceiptBuilder<
529 Transaction: Transaction + Encodable2718 + ArbTransactionExt,
530 Receipt: TxReceipt<Log = Log>,
531 >,
532 R::Transaction: TransactionEnvelope,
533{
534 #[cold]
538 #[inline(never)]
539 fn refresh_state_params(&mut self) {
540 let arbos_version = self.arb_ctx.arbos_version;
541 let mut calldata_pricing_increase_enabled = false;
542 let mut collect_tips_enabled = self
543 .arb_hooks
544 .as_ref()
545 .map(|h| h.collect_tips_enabled)
546 .unwrap_or(false);
547 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
548 if let Ok(arb_state) = ArbosState::open(db, SystemBurner::new(None, false)) {
549 let state_ref = unsafe { arb_state.backing_storage.state_mut() };
551 if let Ok(net) = arb_state.network_fee_account(state_ref) {
552 self.arb_ctx.network_fee_account = net;
553 }
554 if let Ok(infra) = arb_state.infra_fee_account(state_ref) {
555 self.arb_ctx.infra_fee_account = infra;
556 }
557 if let Ok(min_fee) = arb_state.l2_pricing_state.min_base_fee_wei(state_ref) {
558 self.arb_ctx.min_base_fee = min_fee;
559 }
560 if let Ok(level) = arb_state.brotli_compression_level(state_ref) {
561 self.arb_ctx.brotli_compression_level = level;
562 }
563 calldata_pricing_increase_enabled = arbos_version
564 >= arb_chainspec::arbos_version::ARBOS_VERSION_40
565 && arb_state
566 .features
567 .is_increased_calldata_price_enabled(state_ref)
568 .unwrap_or(false);
569 collect_tips_enabled = arb_state
570 .collect_tips(state_ref)
571 .unwrap_or(collect_tips_enabled);
572 }
573 if let Some(hooks) = self.arb_hooks.as_mut() {
574 hooks.network_fee_account = self.arb_ctx.network_fee_account;
575 hooks.infra_fee_account = self.arb_ctx.infra_fee_account;
576 hooks.min_base_fee = self.arb_ctx.min_base_fee;
577 hooks.calldata_pricing_increase_enabled = calldata_pricing_increase_enabled;
578 hooks.collect_tips_enabled = collect_tips_enabled;
579 }
580 }
581
582 fn execute_submit_retryable(
586 &mut self,
587 ticket_id: alloy_primitives::B256,
588 tx_type: <R::Transaction as TransactionEnvelope>::TxType,
589 mut info: arb_primitives::SubmitRetryableInfo,
590 ) -> Result<
591 EthTxResult<E::HaltReason, <R::Transaction as TransactionEnvelope>::TxType>,
592 BlockExecutionError,
593 > {
594 let sender = info.from;
595
596 let is_filtered = {
601 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
602 let arb_state = ArbosState::open(db, SystemBurner::new(None, false))
603 .map_err(BlockExecutionError::other)?;
604 if arb_state.filtered_transactions.is_filtered_free(ticket_id) {
605 let state_ref = unsafe { arb_state.backing_storage.state_mut() };
609 let recipient = arb_state
610 .filtered_funds_recipient_or_default(state_ref)
611 .map_err(BlockExecutionError::other)?;
612 info.fee_refund_addr = recipient;
613 info.beneficiary = recipient;
614 true
615 } else {
616 false
617 }
618 };
619
620 let block = self.inner.evm().block();
622 let current_time = revm::context::Block::timestamp(block).to::<u64>();
623 let effective_base_fee = self.arb_ctx.basefee;
624
625 let overlay = &mut self.state_overlay;
626 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
627
628 let _ = arb_util::mint_balance(&sender, info.deposit_value, |f, t, a| {
630 apply_balance_op(db, overlay, f, t, a)
631 });
632 self.touched_accounts.insert(sender);
633
634 let dep_i128: i128 = info.deposit_value.try_into().unwrap_or(i128::MAX);
636 self.expected_balance_delta = self.expected_balance_delta.saturating_add(dep_i128);
637
638 let _ = db.load_cache_account(sender);
640 let balance_after_mint = db
641 .cache
642 .accounts
643 .get(&sender)
644 .and_then(|a| a.account.as_ref())
645 .map(|a| a.info.balance)
646 .unwrap_or(U256::ZERO);
647
648 let params = SubmitRetryableParams {
649 ticket_id,
650 from: sender,
651 fee_refund_addr: info.fee_refund_addr,
652 deposit_value: info.deposit_value,
653 retry_value: info.retry_value,
654 gas_fee_cap: info.gas_fee_cap,
655 gas: info.gas,
656 max_submission_fee: info.max_submission_fee,
657 retry_data_len: info.retry_data.len(),
658 l1_base_fee: info.l1_base_fee,
659 effective_base_fee,
660 current_time,
661 balance_after_mint,
662 infra_fee_account: self.arb_ctx.infra_fee_account,
663 min_base_fee: self.arb_ctx.min_base_fee,
664 arbos_version: self.arb_ctx.arbos_version,
665 };
666
667 let fees = compute_submit_retryable_fees(¶ms);
668
669 let user_gas = info.gas;
670
671 if let Some(ref err) = fees.error {
675 tracing::warn!(
676 target: "arb::executor",
677 ticket_id = %ticket_id,
678 error = %err,
679 "submit retryable fee validation failed"
680 );
681
682 self.pending_tx = Some(PendingArbTx {
683 sender,
684 tx_gas_limit: user_gas,
685 arb_tx_type: Some(ArbTxType::ArbitrumSubmitRetryableTx),
686 poster_gas: 0,
687 evm_gas_used: 0,
688
689 charged_multi_gas: MultiGas::default(),
690 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
691 stylus_data_fee: U256::ZERO,
692 retry_context: None,
693 coinbase_tip_per_gas: 0,
694 capped_gas_price: false,
695 actual_gas_price: self.arb_ctx.basefee,
696 });
697
698 return Ok(EthTxResult {
699 result: revm::context::result::ResultAndState {
700 result: ExecutionResult::Revert {
701 gas_used: 0,
702 output: alloy_primitives::Bytes::new(),
703 },
704 state: Default::default(),
705 },
706 blob_gas_used: 0,
707 tx_type,
708 });
709 }
710
711 let overlay = &mut self.state_overlay;
712 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
713
714 if !fees.submission_fee.is_zero() {
716 let _ = arb_util::transfer_balance(
720 Some(&sender),
721 Some(&self.arb_ctx.network_fee_account),
722 fees.submission_fee,
723 |f, t, a| apply_balance_op(db, overlay, f, t, a),
724 );
725 self.touched_accounts.insert(sender);
726 self.touched_accounts
727 .insert(self.arb_ctx.network_fee_account);
728 }
729
730 let _ = arb_util::transfer_balance(
732 Some(&sender),
733 Some(&info.fee_refund_addr),
734 fees.submission_fee_refund,
735 |f, t, a| apply_balance_op(db, overlay, f, t, a),
736 );
737 self.touched_accounts.insert(sender);
738 self.touched_accounts.insert(info.fee_refund_addr);
739
740 let escrow_outcome = arb_util::transfer_balance(
744 Some(&sender),
745 Some(&fees.escrow),
746 info.retry_value,
747 |f, t, a| apply_balance_op(db, overlay, f, t, a),
748 );
749 if matches!(
750 escrow_outcome,
751 Err(BalanceError::InsufficientBalance { .. })
752 ) {
753 self.touched_accounts.insert(sender);
754 self.touched_accounts.insert(fees.escrow);
755 let _ = arb_util::transfer_balance(
757 Some(&self.arb_ctx.network_fee_account),
758 Some(&sender),
759 fees.submission_fee,
760 |f, t, a| apply_balance_op(db, overlay, f, t, a),
761 );
762 self.touched_accounts
763 .insert(self.arb_ctx.network_fee_account);
764 let _ = arb_util::transfer_balance(
766 Some(&sender),
767 Some(&info.fee_refund_addr),
768 fees.withheld_submission_fee,
769 |f, t, a| apply_balance_op(db, overlay, f, t, a),
770 );
771 self.touched_accounts.insert(info.fee_refund_addr);
772
773 self.pending_tx = Some(PendingArbTx {
774 sender,
775 tx_gas_limit: user_gas,
776 arb_tx_type: Some(ArbTxType::ArbitrumSubmitRetryableTx),
777 poster_gas: 0,
778 evm_gas_used: 0,
779
780 charged_multi_gas: MultiGas::default(),
781 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
782 stylus_data_fee: U256::ZERO,
783 retry_context: None,
784 coinbase_tip_per_gas: 0,
785 capped_gas_price: false,
786 actual_gas_price: self.arb_ctx.basefee,
787 });
788
789 return Ok(EthTxResult {
790 result: revm::context::result::ResultAndState {
791 result: ExecutionResult::Revert {
792 gas_used: 0,
793 output: alloy_primitives::Bytes::new(),
794 },
795 state: Default::default(),
796 },
797 blob_gas_used: 0,
798 tx_type,
799 });
800 }
801 self.touched_accounts.insert(sender);
802 self.touched_accounts.insert(fees.escrow);
803
804 if info.retry_value.is_zero() {
808 materialise_empty(db, overlay, fees.escrow, &mut self.touched_accounts);
809 }
810
811 let arb_state = ArbosState::open(db, SystemBurner::new(None, false))
813 .map_err(BlockExecutionError::other)?;
814 let state_ref = unsafe { arb_state.backing_storage.state_mut() };
816 let _ = arb_state.retryable_state.create_retryable(
817 state_ref,
818 ticket_id,
819 fees.timeout,
820 sender,
821 info.retry_to,
822 info.retry_value,
823 info.beneficiary,
824 &info.retry_data,
825 );
826
827 let mut receipt_logs: Vec<Log> = Vec::new();
829 receipt_logs.push(Log {
830 address: arb_precompiles::ARBRETRYABLETX_ADDRESS,
831 data: alloy_primitives::LogData::new_unchecked(
832 vec![arb_precompiles::ticket_created_topic(), ticket_id],
833 alloy_primitives::Bytes::new(),
834 ),
835 });
836
837 let overlay = &mut self.state_overlay;
838 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
839
840 if fees.can_pay_for_gas {
842 if self.arb_ctx.infra_fee_account != Address::ZERO {
844 let _ = arb_util::transfer_balance(
845 Some(&sender),
846 Some(&self.arb_ctx.infra_fee_account),
847 fees.infra_cost,
848 |f, t, a| apply_balance_op(db, overlay, f, t, a),
849 );
850 self.touched_accounts.insert(sender);
851 self.touched_accounts.insert(self.arb_ctx.infra_fee_account);
852 }
853 if !fees.network_cost.is_zero() {
855 let _ = arb_util::transfer_balance(
856 Some(&sender),
857 Some(&self.arb_ctx.network_fee_account),
858 fees.network_cost,
859 |f, t, a| apply_balance_op(db, overlay, f, t, a),
860 );
861 self.touched_accounts.insert(sender);
862 self.touched_accounts
863 .insert(self.arb_ctx.network_fee_account);
864 }
865 let _ = arb_util::transfer_balance(
867 Some(&sender),
868 Some(&info.fee_refund_addr),
869 fees.gas_price_refund,
870 |f, t, a| apply_balance_op(db, overlay, f, t, a),
871 );
872 self.touched_accounts.insert(sender);
873 self.touched_accounts.insert(info.fee_refund_addr);
874
875 if !is_filtered {
877 let arb_state = ArbosState::open(db, SystemBurner::new(None, false))
880 .map_err(BlockExecutionError::other)?;
881 let state_ref = unsafe { arb_state.backing_storage.state_mut() };
883 match arb_state
884 .retryable_state
885 .open_retryable(state_ref, ticket_id, 0)
886 {
887 Ok(Some(retryable)) => {
888 let _ = retryable.increment_num_tries(state_ref);
889
890 match retryable.make_tx(
891 state_ref,
892 U256::from(self.arb_ctx.chain_id),
893 0, effective_base_fee,
895 user_gas,
896 ticket_id,
897 info.fee_refund_addr,
898 fees.available_refund,
899 fees.submission_fee,
900 ) {
901 Ok(retry_tx) => {
902 let retry_tx_hash = {
904 let mut enc = Vec::new();
905 enc.push(ArbTxType::ArbitrumRetryTx.as_u8());
906 alloy_rlp::Encodable::encode(&retry_tx, &mut enc);
907 keccak256(&enc)
908 };
909
910 let mut event_data = Vec::with_capacity(128);
912 event_data.extend_from_slice(
913 &B256::left_padding_from(&user_gas.to_be_bytes()).0,
914 );
915 event_data.extend_from_slice(
916 &B256::left_padding_from(info.fee_refund_addr.as_slice()).0,
917 );
918 event_data
919 .extend_from_slice(&fees.available_refund.to_be_bytes::<32>());
920 event_data
921 .extend_from_slice(&fees.submission_fee.to_be_bytes::<32>());
922
923 receipt_logs.push(Log {
924 address: arb_precompiles::ARBRETRYABLETX_ADDRESS,
925 data: alloy_primitives::LogData::new_unchecked(
926 vec![
927 arb_precompiles::redeem_scheduled_topic(),
928 ticket_id,
929 retry_tx_hash,
930 B256::left_padding_from(&0u64.to_be_bytes()),
931 ],
932 event_data.into(),
933 ),
934 });
935
936 if let Some(hooks) = self.arb_hooks.as_mut() {
937 let mut encoded = Vec::new();
938 encoded.push(ArbTxType::ArbitrumRetryTx.as_u8());
939 alloy_rlp::Encodable::encode(&retry_tx, &mut encoded);
940 hooks.tx_proc.scheduled_txs.push(encoded);
941 } else {
942 tracing::warn!(
943 target: "arb::executor",
944 "Cannot schedule auto-redeem: arb_hooks is None"
945 );
946 }
947 }
948 Err(_) => {
949 tracing::warn!(
950 target: "arb::executor",
951 "Auto-redeem make_tx failed"
952 );
953 }
954 }
955 }
956 Ok(None) => {
957 tracing::warn!(
958 target: "arb::executor",
959 %ticket_id,
960 "open_retryable returned None after create"
961 );
962 }
963 Err(_) => {
964 tracing::warn!(
965 target: "arb::executor",
966 "open_retryable failed"
967 );
968 }
969 }
970 }
971 } else if !fees.gas_cost_refund.is_zero() {
972 let _ = arb_util::transfer_balance(
974 Some(&sender),
975 Some(&info.fee_refund_addr),
976 fees.gas_cost_refund,
977 |f, t, a| apply_balance_op(db, overlay, f, t, a),
978 );
979 self.touched_accounts.insert(sender);
980 self.touched_accounts.insert(info.fee_refund_addr);
981 }
982
983 let gas_used = if fees.can_pay_for_gas { user_gas } else { 0 };
989 self.pending_tx = Some(PendingArbTx {
990 sender,
991 tx_gas_limit: user_gas,
992 arb_tx_type: Some(ArbTxType::ArbitrumSubmitRetryableTx),
993 poster_gas: 0,
994 evm_gas_used: gas_used,
995 charged_multi_gas: if fees.can_pay_for_gas {
996 MultiGas::single_dim_gas(user_gas)
997 } else {
998 MultiGas::default()
999 },
1000 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
1001 stylus_data_fee: U256::ZERO,
1002 retry_context: None,
1003 coinbase_tip_per_gas: 0,
1004 capped_gas_price: false,
1005 actual_gas_price: self.arb_ctx.basefee,
1006 });
1007
1008 let ticket_bytes = alloy_primitives::Bytes::copy_from_slice(ticket_id.as_slice());
1012
1013 if is_filtered {
1014 Ok(EthTxResult {
1015 result: revm::context::result::ResultAndState {
1016 result: ExecutionResult::Revert {
1017 gas_used,
1018 output: ticket_bytes,
1019 },
1020 state: Default::default(),
1021 },
1022 blob_gas_used: 0,
1023 tx_type,
1024 })
1025 } else {
1026 Ok(EthTxResult {
1027 result: revm::context::result::ResultAndState {
1028 result: ExecutionResult::Success {
1029 reason: revm::context::result::SuccessReason::Return,
1030 gas_used,
1031 gas_refunded: 0,
1032 output: revm::context::result::Output::Call(ticket_bytes),
1033 logs: receipt_logs,
1034 },
1035 state: Default::default(),
1036 },
1037 blob_gas_used: 0,
1038 tx_type,
1039 })
1040 }
1041 }
1042}
1043
1044impl<'db, DB, E, Spec, R> BlockExecutor for ArbBlockExecutor<'_, E, Spec, R>
1045where
1046 DB: Database + 'db,
1047 E: Evm<
1048 DB = &'db mut State<DB>,
1049 Tx: FromRecoveredTx<R::Transaction>
1050 + FromTxWithEncoded<R::Transaction>
1051 + ArbTransactionEnv,
1052 >,
1053 Spec: EthExecutorSpec,
1054 R: ReceiptBuilder<
1055 Transaction: Transaction + Encodable2718 + ArbTransactionExt,
1056 Receipt: TxReceipt<Log = Log> + arb_primitives::SetArbReceiptFields,
1057 >,
1058 R::Transaction: TransactionEnvelope,
1059{
1060 type Transaction = R::Transaction;
1061 type Receipt = R::Receipt;
1062 type Evm = E;
1063 type Result = EthTxResult<E::HaltReason, <R::Transaction as TransactionEnvelope>::TxType>;
1064
1065 fn apply_pre_execution_changes(&mut self) -> Result<(), BlockExecutionError> {
1066 self.inner.apply_pre_execution_changes()?;
1067
1068 {
1070 let block = self.inner.evm().block();
1071 let timestamp = revm::context::Block::timestamp(block).to::<u64>();
1072 if self.arb_ctx.block_timestamp == 0 {
1073 self.arb_ctx.block_timestamp = timestamp;
1074 }
1075 self.arb_ctx.coinbase = revm::context::Block::beneficiary(block);
1076 self.arb_ctx.basefee = U256::from(revm::context::Block::basefee(block));
1077 if self.arb_ctx.chain_id == 0 {
1082 self.arb_ctx.chain_id = self.inner.evm().chain_id();
1083 }
1084 if let Some(prevrandao) = revm::context::Block::prevrandao(block)
1085 && self.arb_ctx.l1_block_number == 0
1086 {
1087 self.arb_ctx.l1_block_number =
1088 crate::config::l1_block_number_from_mix_hash(&prevrandao);
1089 }
1090 }
1091
1092 if self.arb_ctx.l2_block_number > 0 {
1097 self.precompile_ctx
1098 .block
1099 .cache_l1_block_number(self.arb_ctx.l2_block_number, self.arb_ctx.l1_block_number);
1100 }
1101
1102 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1106 let arb_state = ArbosState::open(db, SystemBurner::new(None, false))
1107 .map_err(BlockExecutionError::other)?;
1108 let state_ref = unsafe { arb_state.backing_storage.state_mut() };
1111
1112 let _ = arb_state.l2_pricing_state.commit_multi_gas_fees(state_ref);
1113
1114 if let Ok(base_fee) = arb_state.l2_pricing_state.base_fee_wei(state_ref) {
1115 self.arb_ctx.basefee = base_fee;
1116 }
1117
1118 load_state_params(
1119 &mut self.arb_ctx,
1120 &mut self.precompile_ctx,
1121 &mut self.arb_hooks,
1122 state_ref,
1123 &arb_state,
1124 );
1125
1126 self.block_gas_left = arb_state
1127 .l2_pricing_state
1128 .per_block_gas_limit(state_ref)
1129 .unwrap_or(0);
1130
1131 if let Ok(l1_block_number) = arb_state.blockhashes.l1_block_number(state_ref) {
1132 let lower = l1_block_number.saturating_sub(256);
1133 for n in lower..l1_block_number {
1134 if let Ok(Some(hash)) = arb_state.blockhashes.block_hash(state_ref, n) {
1137 state_ref.block_hashes.insert(n, hash);
1138 }
1139 }
1140 }
1141
1142 {
1146 let parent_hash = self.arb_ctx.parent_hash;
1147 let current_l2 = self.arb_ctx.l2_block_number;
1148 let block = std::sync::Arc::clone(&self.precompile_ctx.block);
1149 populate_l2_block_hash_window(&block, current_l2, parent_hash, |n| {
1150 match state_ref.database.block_hash(n) {
1151 Ok(hash) if hash != B256::ZERO => Some(hash),
1152 _ => None,
1153 }
1154 });
1155 }
1156
1157 tracing::trace!(
1158 target: "arb::executor",
1159 l1_block = self.arb_ctx.l1_block_number,
1160 delayed_msgs = self.arb_ctx.delayed_messages_read,
1161 chain_id = self.arb_ctx.chain_id,
1162 basefee = %self.arb_ctx.basefee,
1163 arbos_version = self.arb_ctx.arbos_version,
1164 has_hooks = self.arb_hooks.is_some(),
1165 "starting block execution"
1166 );
1167
1168 Ok(())
1169 }
1170
1171 fn execute_transaction_without_commit(
1172 &mut self,
1173 tx: impl ExecutableTx<Self>,
1174 ) -> Result<Self::Result, BlockExecutionError> {
1175 let (tx_env, recovered) = tx.into_parts();
1177 let sender = *recovered.signer();
1178 let tx_type_raw = recovered.tx().ty();
1179 let tx_gas_limit = recovered.tx().gas_limit();
1180 let tx_value = recovered.tx().value();
1181 let envelope_tx_type = recovered.tx().tx_type();
1182 let intrinsic_multi_gas = tx_intrinsic_multi_gas(
1183 recovered.tx(),
1184 arb_chainspec::spec_id_by_arbos_version(self.arb_ctx.arbos_version),
1185 );
1186 let calldata_floor_gas = if self
1189 .arb_hooks
1190 .as_ref()
1191 .map(|h| h.is_calldata_pricing_increase_enabled())
1192 .unwrap_or(false)
1193 {
1194 tx_floor_data_gas(recovered.tx())
1195 } else {
1196 0
1197 };
1198
1199 let arb_tx_type = ArbTxType::from_u8(tx_type_raw).ok();
1201 let is_arb_internal = arb_tx_type == Some(ArbTxType::ArbitrumInternalTx);
1202 let is_arb_deposit = arb_tx_type == Some(ArbTxType::ArbitrumDepositTx);
1203 let is_submit_retryable = arb_tx_type == Some(ArbTxType::ArbitrumSubmitRetryableTx);
1204 let is_retry_tx = arb_tx_type == Some(ArbTxType::ArbitrumRetryTx);
1205 let is_contract_tx = arb_tx_type == Some(ArbTxType::ArbitrumContractTx);
1206 let has_poster_costs = tx_type_has_poster_costs(tx_type_raw);
1207
1208 let is_user_tx =
1212 !is_arb_internal && !is_arb_deposit && !is_submit_retryable && !is_retry_tx;
1213 const TX_GAS_MIN: u64 = 21_000;
1214 if is_user_tx && self.block_gas_left < TX_GAS_MIN {
1215 return Err(BlockExecutionError::msg("block gas limit reached"));
1216 }
1217
1218 crate::evm::reset_stylus_pages(&self.precompile_ctx);
1220 crate::evm::clear_poster_balance_correction();
1221 self.precompile_ctx.reset_tx();
1222 self.precompile_ctx.reset_caller_stack();
1223 self.state_overlay.reset_tx();
1224 if let Some(hooks) = self.arb_hooks.as_mut() {
1225 hooks.tx_proc.poster_fee = U256::ZERO;
1226 hooks.tx_proc.poster_gas = 0;
1227 hooks.tx_proc.compute_hold_gas = 0;
1228 hooks.tx_proc.current_retryable = None;
1229 hooks.tx_proc.current_refund_to = None;
1230 hooks.tx_proc.scheduled_txs.clear();
1231 }
1232
1233 let actual_gas_price: U256 = {
1238 let base_fee = self.arb_ctx.basefee;
1239 let base_fee_u128: u128 = base_fee.try_into().unwrap_or(u128::MAX);
1240 let max_fee: u128 = revm::context_interface::Transaction::gas_price(&tx_env);
1241 let effective: u128 =
1242 match revm::context_interface::Transaction::max_priority_fee_per_gas(&tx_env) {
1243 Some(max_priority) => {
1244 std::cmp::min(max_fee, base_fee_u128.saturating_add(max_priority))
1245 }
1246 None => max_fee,
1247 };
1248 let drop = self
1249 .arb_hooks
1250 .as_ref()
1251 .map(|h| h.drop_tip())
1252 .unwrap_or(false);
1253 if drop || effective == 0 {
1254 base_fee
1255 } else {
1256 U256::from(effective)
1257 }
1258 };
1259
1260 if is_arb_internal {
1264 use arbos::tx_processor::ARBOS_ADDRESS;
1265
1266 if sender != ARBOS_ADDRESS {
1267 return Err(BlockExecutionError::msg(
1268 "internal tx not from ArbOS address",
1269 ));
1270 }
1271
1272 let tx_data = recovered.tx().input().to_vec();
1273 let tx_type = recovered.tx().tx_type();
1274 let mut tx_err = None;
1275
1276 if let Some(selector) = tx_data.first_chunk::<4>() {
1277 let is_start_block = *selector == internal_tx::INTERNAL_TX_START_BLOCK_METHOD_ID;
1278
1279 if is_start_block
1280 && let Ok(start_data) = internal_tx::decode_start_block_data(&tx_data)
1281 {
1282 self.arb_ctx.l1_base_fee = start_data.l1_base_fee;
1283 self.arb_ctx.time_passed = start_data.time_passed;
1284 }
1285
1286 let (block_number, current_time) = {
1287 let block = self.inner.evm().block();
1288 (
1289 revm::context::Block::number(block).to::<u64>(),
1290 revm::context::Block::timestamp(block).to::<u64>(),
1291 )
1292 };
1293 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1294 let mut arb_state = ArbosState::open(db, SystemBurner::new(None, false))
1295 .map_err(BlockExecutionError::other)?;
1296 let closure_storage = arb_state.backing_storage.clone();
1303 let ctx = InternalTxContext {
1304 block_number,
1305 current_time,
1306 prev_hash: self.arb_ctx.parent_hash,
1307 };
1308
1309 if is_start_block
1311 && arb_state.arbos_version() >= arb_chainspec::arbos_version::ARBOS_VERSION_40
1312 {
1313 process_parent_block_hash(
1315 unsafe { closure_storage.state_mut() },
1316 self.arb_ctx.l2_block_number,
1317 ctx.prev_hash,
1318 );
1319 }
1320
1321 let touched_ptr = &mut self.touched_accounts as *mut rustc_hash::FxHashSet<Address>;
1322 let zombie_ptr = &mut self.zombie_accounts as *mut rustc_hash::FxHashSet<Address>;
1323 let finalise_ptr = &self.finalise_deleted as *const rustc_hash::FxHashSet<Address>;
1324 let overlay_ptr = &mut self.state_overlay as *mut StateOverlay;
1325 let arbos_ver = self.arb_ctx.arbos_version;
1326 let transfer_storage = closure_storage.clone();
1327 let balance_storage = closure_storage.clone();
1328 let mut do_transfer = move |from: Address, to: Address, amount: U256| {
1329 unsafe {
1331 let state = transfer_storage.state_mut();
1332 if amount.is_zero()
1333 && arbos_ver < arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS
1334 {
1335 create_zombie_if_deleted(
1336 state,
1337 &mut *overlay_ptr,
1338 from,
1339 &*finalise_ptr,
1340 &mut *zombie_ptr,
1341 &mut *touched_ptr,
1342 );
1343 }
1344 let _ = apply_balance_op(
1351 state,
1352 &mut *overlay_ptr,
1353 Some(&from),
1354 Some(&to),
1355 amount,
1356 );
1357 if !amount.is_zero() {
1358 (*zombie_ptr).remove(&from);
1359 }
1360 (*zombie_ptr).remove(&to);
1361 (*touched_ptr).insert(from);
1362 (*touched_ptr).insert(to);
1363 }
1364 Ok(())
1365 };
1366 let mut do_balance = move |addr: Address| -> U256 {
1367 unsafe { get_balance(balance_storage.state_mut(), addr) }
1369 };
1370 if let Err(e) = internal_tx::apply_internal_tx_update(
1376 unsafe { closure_storage.state_mut() },
1377 &tx_data,
1378 &mut arb_state,
1379 &ctx,
1380 &mut do_transfer,
1381 &mut do_balance,
1382 ) {
1383 tracing::warn!(
1384 target: "arb::executor",
1385 error = %e,
1386 "internal tx processing failed"
1387 );
1388 tx_err = Some(e);
1389 }
1390
1391 if is_start_block {
1392 let state_ref = unsafe { arb_state.backing_storage.state_mut() };
1394 if let Ok(l1_block_number) = arb_state.blockhashes.l1_block_number(state_ref) {
1395 self.arb_ctx.l1_block_number = l1_block_number;
1396 }
1397
1398 load_state_params(
1399 &mut self.arb_ctx,
1400 &mut self.precompile_ctx,
1401 &mut self.arb_hooks,
1402 state_ref,
1403 &arb_state,
1404 );
1405
1406 if let Ok(l1_block_number) = arb_state.blockhashes.l1_block_number(state_ref) {
1407 let lower = l1_block_number.saturating_sub(256);
1408 for n in lower..l1_block_number {
1409 if let Ok(Some(hash)) = arb_state.blockhashes.block_hash(state_ref, n) {
1410 state_ref.block_hashes.insert(n, hash);
1411 }
1412 }
1413 }
1414 }
1415 }
1416
1417 self.pending_tx = Some(PendingArbTx {
1419 sender,
1420 tx_gas_limit: 0,
1421 arb_tx_type: Some(ArbTxType::ArbitrumInternalTx),
1422 poster_gas: 0,
1423 evm_gas_used: 0,
1424
1425 charged_multi_gas: MultiGas::default(),
1426 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
1427 stylus_data_fee: U256::ZERO,
1428 retry_context: None,
1429 coinbase_tip_per_gas: 0,
1430 capped_gas_price: false,
1431 actual_gas_price: self.arb_ctx.basefee,
1432 });
1433
1434 if let Some(err) = tx_err {
1436 return Err(BlockExecutionError::other(err));
1437 }
1438
1439 return Ok(EthTxResult {
1440 result: revm::context::result::ResultAndState {
1441 result: ExecutionResult::Success {
1442 reason: revm::context::result::SuccessReason::Return,
1443 gas_used: 0,
1444 gas_refunded: 0,
1445 output: revm::context::result::Output::Call(alloy_primitives::Bytes::new()),
1446 logs: Vec::new(),
1447 },
1448 state: Default::default(),
1449 },
1450 blob_gas_used: 0,
1451 tx_type,
1452 });
1453 }
1454
1455 if is_arb_deposit {
1458 let value = recovered.tx().value();
1459 let mut to = match recovered.tx().kind() {
1460 TxKind::Call(addr) => addr,
1461 TxKind::Create => {
1462 return Err(BlockExecutionError::msg("deposit tx has no To address"));
1463 }
1464 };
1465 let tx_type = recovered.tx().tx_type();
1466 let tx_hash = recovered.tx().trie_hash();
1467
1468 let mut is_filtered = false;
1472 {
1473 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1474 let arb_state = ArbosState::open(db, SystemBurner::new(None, false))
1475 .map_err(BlockExecutionError::other)?;
1476 if arb_state.filtered_transactions.is_filtered_free(tx_hash) {
1477 let state_ref = unsafe { arb_state.backing_storage.state_mut() };
1479 to = arb_state
1480 .filtered_funds_recipient_or_default(state_ref)
1481 .map_err(BlockExecutionError::other)?;
1482 is_filtered = true;
1483 }
1484 }
1485
1486 let overlay = &mut self.state_overlay;
1487 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1488 let _ = arb_util::mint_balance(&sender, value, |f, t, a| {
1489 apply_balance_op(db, overlay, f, t, a)
1490 });
1491 let _ = arb_util::transfer_balance(Some(&sender), Some(&to), value, |f, t, a| {
1492 apply_balance_op(db, overlay, f, t, a)
1493 });
1494 self.touched_accounts.insert(sender);
1495 self.touched_accounts.insert(to);
1496
1497 let value_i128: i128 = value.try_into().unwrap_or(i128::MAX);
1499 self.expected_balance_delta = self.expected_balance_delta.saturating_add(value_i128);
1500
1501 self.pending_tx = Some(PendingArbTx {
1502 sender,
1503 tx_gas_limit: 0,
1504 arb_tx_type: Some(ArbTxType::ArbitrumDepositTx),
1505 poster_gas: 0,
1506 evm_gas_used: 0,
1507
1508 charged_multi_gas: MultiGas::default(),
1509 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
1510 stylus_data_fee: U256::ZERO,
1511 retry_context: None,
1512 coinbase_tip_per_gas: 0,
1513 capped_gas_price: false,
1514 actual_gas_price: self.arb_ctx.basefee,
1515 });
1516
1517 let result = if is_filtered {
1521 ExecutionResult::Revert {
1522 gas_used: 0,
1523 output: alloy_primitives::Bytes::from("filtered transaction"),
1524 }
1525 } else {
1526 ExecutionResult::Success {
1527 reason: revm::context::result::SuccessReason::Return,
1528 gas_used: 0,
1529 gas_refunded: 0,
1530 output: revm::context::result::Output::Call(alloy_primitives::Bytes::new()),
1531 logs: Vec::new(),
1532 }
1533 };
1534
1535 return Ok(EthTxResult {
1536 result: revm::context::result::ResultAndState {
1537 result,
1538 state: Default::default(),
1539 },
1540 blob_gas_used: 0,
1541 tx_type,
1542 });
1543 }
1544
1545 if is_submit_retryable && let Some(info) = recovered.tx().submit_retryable_info() {
1547 let ticket_id = recovered.tx().trie_hash();
1548 let tx_type = recovered.tx().tx_type();
1549 return self.execute_submit_retryable(ticket_id, tx_type, info);
1550 }
1551
1552 let mut retry_pre_exec_undo: Option<(Address, U256, Address, U256)> = None;
1556 let mut retry_context = None;
1557 if is_retry_tx && let Some(info) = recovered.tx().retry_tx_info() {
1558 let current_time = {
1559 let block = self.inner.evm().block();
1560 revm::context::Block::timestamp(block).to::<u64>()
1561 };
1562 let overlay = &mut self.state_overlay;
1563 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1564
1565 let retryable = {
1568 let arb_state = ArbosState::open(db, SystemBurner::new(None, false))
1569 .map_err(BlockExecutionError::other)?;
1570 let state_ref = unsafe { arb_state.backing_storage.state_mut() };
1572 arb_state
1573 .retryable_state
1574 .open_retryable(state_ref, info.ticket_id, current_time)
1575 .map(|opt| opt.map(|_| ()))
1576 };
1577
1578 match retryable {
1579 Ok(Some(_)) => {
1580 let escrow = retryables::retryable_escrow_address(info.ticket_id);
1582 let value = recovered.tx().value();
1583
1584 if value.is_zero()
1587 && self.arb_ctx.arbos_version
1588 < arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS
1589 {
1590 create_zombie_if_deleted(
1591 db,
1592 overlay,
1593 escrow,
1594 &self.finalise_deleted,
1595 &mut self.zombie_accounts,
1596 &mut self.touched_accounts,
1597 );
1598 }
1599
1600 let escrow_outcome = arb_util::transfer_balance(
1601 Some(&escrow),
1602 Some(&sender),
1603 value,
1604 |f, t, a| apply_balance_op(db, overlay, f, t, a),
1605 );
1606 if matches!(
1607 escrow_outcome,
1608 Err(BalanceError::InsufficientBalance { .. })
1609 ) {
1610 let tx_type = recovered.tx().tx_type();
1612 self.pending_tx = Some(PendingArbTx {
1613 sender,
1614 tx_gas_limit: 0,
1615 arb_tx_type: Some(ArbTxType::ArbitrumRetryTx),
1616 poster_gas: 0,
1617 evm_gas_used: 0,
1618
1619 charged_multi_gas: MultiGas::default(),
1620 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
1621 stylus_data_fee: U256::ZERO,
1622 retry_context: None,
1623 coinbase_tip_per_gas: 0,
1624 capped_gas_price: false,
1625 actual_gas_price: self.arb_ctx.basefee,
1626 });
1627 return Ok(EthTxResult {
1628 result: revm::context::result::ResultAndState {
1629 result: ExecutionResult::Revert {
1630 gas_used: 0,
1631 output: alloy_primitives::Bytes::new(),
1632 },
1633 state: Default::default(),
1634 },
1635 blob_gas_used: 0,
1636 tx_type,
1637 });
1638 }
1639
1640 if !value.is_zero() {
1642 self.zombie_accounts.remove(&escrow);
1643 }
1644 self.zombie_accounts.remove(&sender);
1645 self.touched_accounts.insert(escrow);
1646 self.touched_accounts.insert(sender);
1647
1648 let prepaid = self
1650 .arb_ctx
1651 .basefee
1652 .saturating_mul(U256::from(tx_gas_limit));
1653 let _ = arb_util::mint_balance(&sender, prepaid, |f, t, a| {
1654 apply_balance_op(db, overlay, f, t, a)
1655 });
1656 retry_pre_exec_undo = Some((sender, prepaid, escrow, value));
1657
1658 overlay.drain_and_apply(db, &self.zombie_accounts);
1664
1665 if let Some(hooks) = self.arb_hooks.as_mut() {
1667 hooks
1668 .tx_proc
1669 .prepare_retry_tx(info.ticket_id, info.refund_to);
1670 }
1671
1672 retry_context = Some(PendingRetryContext {
1673 ticket_id: info.ticket_id,
1674 refund_to: info.refund_to,
1675 max_refund: info.max_refund,
1676 submission_fee_refund: info.submission_fee_refund,
1677 call_value: recovered.tx().value(),
1678 });
1679 }
1680 Ok(None) => {
1681 let tx_type = recovered.tx().tx_type();
1683 self.pending_tx = Some(PendingArbTx {
1684 sender,
1685 tx_gas_limit: 0,
1686 arb_tx_type: Some(ArbTxType::ArbitrumRetryTx),
1687 poster_gas: 0,
1688 evm_gas_used: 0,
1689
1690 charged_multi_gas: MultiGas::default(),
1691 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
1692 stylus_data_fee: U256::ZERO,
1693 retry_context: None,
1694 coinbase_tip_per_gas: 0,
1695 capped_gas_price: false,
1696 actual_gas_price: self.arb_ctx.basefee,
1697 });
1698 let err_msg = format!("retryable ticket {} not found", info.ticket_id,);
1699 return Ok(EthTxResult {
1700 result: revm::context::result::ResultAndState {
1701 result: ExecutionResult::Revert {
1702 gas_used: 0,
1703 output: alloy_primitives::Bytes::from(err_msg.into_bytes()),
1704 },
1705 state: Default::default(),
1706 },
1707 blob_gas_used: 0,
1708 tx_type,
1709 });
1710 }
1711 Err(_) => {
1712 let tx_type = recovered.tx().tx_type();
1714 self.pending_tx = Some(PendingArbTx {
1715 sender,
1716 tx_gas_limit: 0,
1717 arb_tx_type: Some(ArbTxType::ArbitrumRetryTx),
1718 poster_gas: 0,
1719 evm_gas_used: 0,
1720
1721 charged_multi_gas: MultiGas::default(),
1722 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
1723 stylus_data_fee: U256::ZERO,
1724 retry_context: None,
1725 coinbase_tip_per_gas: 0,
1726 capped_gas_price: false,
1727 actual_gas_price: self.arb_ctx.basefee,
1728 });
1729 return Ok(EthTxResult {
1730 result: revm::context::result::ResultAndState {
1731 result: ExecutionResult::Revert {
1732 gas_used: 0,
1733 output: alloy_primitives::Bytes::from(
1734 format!("error opening retryable {}", info.ticket_id,)
1735 .into_bytes(),
1736 ),
1737 },
1738 state: Default::default(),
1739 },
1740 blob_gas_used: 0,
1741 tx_type,
1742 });
1743 }
1744 }
1745 }
1746
1747 let mut poster_gas = 0u64;
1750 let mut compute_hold_gas = 0u64;
1751 let calldata_units: u64 = if has_poster_costs {
1752 let level = self.arb_ctx.brotli_compression_level;
1753 let coinbase = self.arb_ctx.coinbase;
1754 let tx_ref = recovered.tx();
1755 let units = if coinbase == l1_pricing::BATCH_POSTER_ADDRESS {
1756 let tx_bytes_ref = tx_ref;
1757 tx_ref.poster_units_for(level, &mut || {
1758 l1_pricing::poster_units_from_bytes(&tx_bytes_ref.encoded_2718(), level)
1759 })
1760 } else {
1761 0
1762 };
1763 let poster_cost = self
1764 .arb_ctx
1765 .l1_price_per_unit
1766 .saturating_mul(U256::from(units));
1767
1768 if let Some(hooks) = self.arb_hooks.as_mut() {
1769 hooks.tx_proc.poster_gas = compute_poster_gas(
1770 poster_cost,
1771 actual_gas_price,
1772 false,
1773 self.arb_ctx.min_base_fee,
1774 );
1775 hooks.tx_proc.poster_fee =
1776 actual_gas_price.saturating_mul(U256::from(hooks.tx_proc.poster_gas));
1777 poster_gas = hooks.tx_proc.poster_gas;
1778 }
1779
1780 units
1781 } else {
1782 0
1783 };
1784
1785 if let Some(hooks) = self.arb_hooks.as_mut()
1790 && !hooks.is_eth_call
1791 {
1792 let spec = arb_chainspec::spec_id_by_arbos_version(self.arb_ctx.arbos_version);
1793 let intrinsic_estimate = estimate_intrinsic_gas(recovered.tx(), spec);
1794 let gas_after_intrinsic = tx_gas_limit.saturating_sub(intrinsic_estimate);
1795 let gas_after_poster = gas_after_intrinsic.saturating_sub(poster_gas);
1796
1797 let max_compute =
1798 if hooks.arbos_version < arb_chainspec::arbos_version::ARBOS_VERSION_50 {
1799 hooks.per_block_gas_limit
1800 } else {
1801 hooks.per_tx_gas_limit.saturating_sub(intrinsic_estimate)
1802 };
1803
1804 if max_compute > 0 && gas_after_poster > max_compute {
1805 compute_hold_gas = gas_after_poster - max_compute;
1806 hooks.tx_proc.compute_hold_gas = compute_hold_gas;
1807 }
1808 }
1809
1810 if is_user_tx
1815 && self.arb_ctx.arbos_version < arb_chainspec::arbos_version::ARBOS_VERSION_50
1816 && self.user_txs_processed > 0
1817 {
1818 const TX_GAS: u64 = 21_000;
1819 let compute_gas = tx_gas_limit.saturating_sub(poster_gas).max(TX_GAS);
1820 if compute_gas > self.block_gas_left {
1821 return Err(BlockExecutionError::msg("block gas limit reached"));
1822 }
1823 }
1824
1825 let tx_hash_for_filter = recovered.tx().trie_hash();
1829 let is_filtered = {
1830 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1831 let arb_state = ArbosState::open(db, SystemBurner::new(None, false))
1832 .map_err(BlockExecutionError::other)?;
1833 if calldata_units > 0 {
1834 let state_ref = unsafe { arb_state.backing_storage.state_mut() };
1836 let _ = arb_state
1837 .l1_pricing_state
1838 .add_to_units_since_update(state_ref, calldata_units);
1839 }
1840 arb_state
1841 .filtered_transactions
1842 .is_filtered_free(tx_hash_for_filter)
1843 };
1844
1845 let mut tx_env = tx_env;
1852 let gas_deduction = poster_gas.saturating_add(compute_hold_gas);
1853 if gas_deduction > 0 {
1854 let evm_gas_limit_before = revm::context_interface::Transaction::gas_limit(&tx_env);
1855 tx_env.set_gas_limit(evm_gas_limit_before.saturating_sub(gas_deduction));
1856 }
1857
1858 {
1863 let correction = actual_gas_price
1864 .saturating_mul(U256::from(poster_gas.saturating_add(compute_hold_gas)));
1865 let correction_u128 = correction.try_into().unwrap_or(u128::MAX);
1866 self.precompile_ctx
1867 .set_poster_balance_correction(correction_u128);
1868 crate::evm::set_poster_balance_correction(correction_u128);
1873 self.precompile_ctx.set_sender(sender);
1874 }
1875
1876 {
1879 use arbos::tx_processor::RevertedTxAction;
1880
1881 let tx_hash = tx_hash_for_filter;
1882
1883 if let Some(hooks) = self.arb_hooks.as_ref() {
1884 let action = hooks.tx_proc.reverted_tx_hook(
1885 Some(tx_hash),
1886 None, is_filtered,
1888 );
1889
1890 match action {
1891 RevertedTxAction::PreRecordedRevert { gas_to_consume } => {
1892 let overlay = &mut self.state_overlay;
1893 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1894 increment_nonce(db, overlay, sender);
1895 self.touched_accounts.insert(sender);
1896 let spec =
1900 arb_chainspec::spec_id_by_arbos_version(self.arb_ctx.arbos_version);
1901 let intrinsic = estimate_intrinsic_gas(recovered.tx(), spec);
1902 let gas_used = intrinsic
1903 .saturating_add(gas_to_consume)
1904 .saturating_add(poster_gas);
1905 let charged_multi_gas = MultiGas::single_dim_gas(poster_gas)
1906 .saturating_add(MultiGas::computation_gas(gas_to_consume));
1907 self.pending_tx = Some(PendingArbTx {
1908 sender,
1909 tx_gas_limit,
1910 arb_tx_type,
1911 poster_gas,
1912 evm_gas_used: 0,
1913 charged_multi_gas,
1914 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
1915 stylus_data_fee: U256::ZERO,
1916 retry_context,
1917 coinbase_tip_per_gas: 0,
1918 capped_gas_price: false,
1919 actual_gas_price: self.arb_ctx.basefee,
1920 });
1921 return Ok(EthTxResult {
1922 result: revm::context::result::ResultAndState {
1923 result: ExecutionResult::Revert {
1924 gas_used,
1925 output: alloy_primitives::Bytes::new(),
1926 },
1927 state: Default::default(),
1928 },
1929 blob_gas_used: 0,
1930 tx_type: envelope_tx_type,
1931 });
1932 }
1933 RevertedTxAction::FilteredTx => {
1934 let overlay = &mut self.state_overlay;
1935 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1936 increment_nonce(db, overlay, sender);
1937 self.touched_accounts.insert(sender);
1938 let gas_remaining = tx_gas_limit
1940 .saturating_sub(poster_gas)
1941 .saturating_sub(compute_hold_gas);
1942 let gas_used = tx_gas_limit;
1943 let charged_multi_gas = MultiGas::single_dim_gas(poster_gas)
1944 .saturating_add(MultiGas::computation_gas(gas_remaining));
1945 self.pending_tx = Some(PendingArbTx {
1946 sender,
1947 tx_gas_limit,
1948 arb_tx_type,
1949 poster_gas,
1950 evm_gas_used: 0,
1951 charged_multi_gas,
1952 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
1953 stylus_data_fee: U256::ZERO,
1954 retry_context,
1955 coinbase_tip_per_gas: 0,
1956 capped_gas_price: false,
1957 actual_gas_price: self.arb_ctx.basefee,
1958 });
1959 return Ok(EthTxResult {
1960 result: revm::context::result::ResultAndState {
1961 result: ExecutionResult::Revert {
1962 gas_used,
1963 output: alloy_primitives::Bytes::from(
1964 "filtered transaction".as_bytes(),
1965 ),
1966 },
1967 state: Default::default(),
1968 },
1969 blob_gas_used: 0,
1970 tx_type: envelope_tx_type,
1971 });
1972 }
1973 RevertedTxAction::None => {}
1974 }
1975 }
1976 }
1977
1978 let upfront_gas_price: u128 = revm::context_interface::Transaction::gas_price(&tx_env);
1984
1985 {
1988 let base_fee_u128: u128 = self.arb_ctx.basefee.try_into().unwrap_or(u128::MAX);
1989 let effective: u128 =
1990 match revm::context_interface::Transaction::max_priority_fee_per_gas(&tx_env) {
1991 Some(max_priority) => {
1992 upfront_gas_price.min(base_fee_u128.saturating_add(max_priority))
1993 }
1994 None => upfront_gas_price,
1995 };
1996 self.precompile_ctx.set_effective_gas_price(effective);
1997 }
1998
1999 let effective_tip_per_gas: u128 = {
2003 let bf: u128 = self.arb_ctx.basefee.try_into().unwrap_or(u128::MAX);
2004 let max_fee: u128 = upfront_gas_price; let max_minus_bf = max_fee.saturating_sub(bf);
2006 match revm::context_interface::Transaction::max_priority_fee_per_gas(&tx_env) {
2007 Some(max_priority) => max_priority.min(max_minus_bf),
2008 None => max_minus_bf,
2009 }
2010 };
2011
2012 let should_drop_tip = self
2020 .arb_hooks
2021 .as_ref()
2022 .map(|h| h.drop_tip())
2023 .unwrap_or(false);
2024 if should_drop_tip {
2025 let base_fee: u128 = self.arb_ctx.basefee.try_into().unwrap_or(u128::MAX);
2026 if upfront_gas_price > base_fee {
2027 tx_env.set_gas_price(base_fee);
2028 tx_env.set_gas_priority_fee(Some(0));
2029 }
2030 }
2031
2032 self.precompile_ctx
2033 .set_tx_is_aliased(arbos::util::does_tx_type_alias(tx_type_raw));
2034
2035 {
2036 let poster_fee_val = self
2037 .arb_hooks
2038 .as_ref()
2039 .map(|h| h.tx_proc.poster_fee)
2040 .unwrap_or(U256::ZERO);
2041 self.precompile_ctx
2042 .set_poster_fee(poster_fee_val.try_into().unwrap_or(u128::MAX));
2043 let retryable_id = retry_context
2044 .as_ref()
2045 .map(|ctx| ctx.ticket_id)
2046 .unwrap_or(B256::ZERO);
2047 self.precompile_ctx.set_retryable_id(retryable_id);
2048 let redeemer = retry_context
2049 .as_ref()
2050 .map(|ctx| ctx.refund_to)
2051 .unwrap_or(Address::ZERO);
2052 self.precompile_ctx.set_redeemer(redeemer);
2053 }
2054
2055 let retry_undo = retry_pre_exec_undo;
2056 let rollback_pre_exec_state =
2057 |this: &mut Self, units: u64| -> Result<(), BlockExecutionError> {
2058 this.precompile_ctx.reset_tx();
2059 let overlay = &mut this.state_overlay;
2060 let db: &mut State<DB> = this.inner.evm_mut().db_mut();
2061 if units > 0 {
2062 let arb_state = ArbosState::open(db, SystemBurner::new(None, false))
2063 .map_err(BlockExecutionError::other)?;
2064 let state_ref = unsafe { arb_state.backing_storage.state_mut() };
2066 let _ = arb_state
2067 .l1_pricing_state
2068 .subtract_from_units_since_update(state_ref, units);
2069 }
2070 if let Some((retry_sender, prepaid, escrow, escrow_value)) = retry_undo {
2071 if !prepaid.is_zero() {
2072 let _ = arb_util::burn_balance(&retry_sender, prepaid, |f, t, a| {
2073 apply_balance_op(db, overlay, f, t, a)
2074 });
2075 }
2076 if !escrow_value.is_zero() {
2077 let _ = arb_util::transfer_balance(
2082 Some(&retry_sender),
2083 Some(&escrow),
2084 escrow_value,
2085 |f, t, a| apply_balance_op(db, overlay, f, t, a),
2086 );
2087 }
2088 }
2089 Ok(())
2090 };
2091
2092 if is_user_tx {
2095 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
2096 let account = db
2097 .load_cache_account(sender)
2098 .ok()
2099 .and_then(|a| a.account_info());
2100 let sender_balance = account.as_ref().map(|a| a.balance).unwrap_or(U256::ZERO);
2101 let sender_nonce = account.as_ref().map(|a| a.nonce).unwrap_or(0);
2102
2103 if !is_contract_tx {
2105 let tx_nonce = revm::context_interface::Transaction::nonce(&tx_env);
2106 if tx_nonce != sender_nonce {
2107 rollback_pre_exec_state(self, calldata_units)?;
2108 return Err(BlockExecutionError::msg(format!(
2109 "nonce mismatch: address {sender} tx nonce {tx_nonce} != state nonce {sender_nonce}"
2110 )));
2111 }
2112 }
2113
2114 let base_fee = self.arb_ctx.basefee;
2125 if U256::from(upfront_gas_price) < base_fee {
2126 rollback_pre_exec_state(self, calldata_units)?;
2127 return Err(BlockExecutionError::msg(format!(
2128 "max fee per gas less than block base fee: address {sender}, maxFeePerGas: {upfront_gas_price}, baseFee: {base_fee}"
2129 )));
2130 }
2131
2132 let gas_cost = U256::from(tx_gas_limit) * U256::from(upfront_gas_price);
2133 let tx_value = revm::context_interface::Transaction::value(&tx_env);
2134 let total_cost = gas_cost.saturating_add(tx_value);
2135 if sender_balance < total_cost {
2136 rollback_pre_exec_state(self, calldata_units)?;
2137 return Err(BlockExecutionError::msg(format!(
2138 "insufficient funds: address {sender} have {sender_balance} want {total_cost}"
2139 )));
2140 }
2141
2142 if calldata_floor_gas > tx_gas_limit {
2143 rollback_pre_exec_state(self, calldata_units)?;
2144 return Err(BlockExecutionError::msg(format!(
2145 "insufficient gas for floor data gas: address {sender} gas limit {tx_gas_limit} floor {calldata_floor_gas}"
2146 )));
2147 }
2148 }
2149
2150 if is_retry_tx || is_contract_tx {
2156 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
2157 let sender_nonce = db
2158 .load_cache_account(sender)
2159 .map(|a| a.account_info().map(|i| i.nonce).unwrap_or(0))
2160 .unwrap_or(0);
2161 tx_env.set_nonce(sender_nonce);
2162 }
2163
2164 {
2165 let to_addr = match recovered.tx().kind() {
2166 TxKind::Call(a) => Some(a),
2167 _ => None,
2168 };
2169 if to_addr == Some(arb_precompiles::ARBWASM_ADDRESS) {
2170 self.precompile_ctx.set_stylus_call_value(tx_value);
2171 if tx_value > U256::ZERO {
2172 tx_env.set_value(U256::ZERO);
2173 }
2174 } else {
2175 self.precompile_ctx.set_stylus_call_value(U256::ZERO);
2176 }
2177 }
2178
2179 let mut output = match self
2180 .inner
2181 .execute_transaction_without_commit((tx_env, recovered))
2182 {
2183 Ok(o) => o,
2184 Err(e) => {
2185 rollback_pre_exec_state(self, calldata_units)?;
2186 return Err(e);
2187 }
2188 };
2189
2190 let evm_gas_used = output.result.result.gas_used();
2193 let gas_refunded = match &output.result.result {
2199 ExecutionResult::Success { gas_refunded, .. } => *gas_refunded,
2200 _ => 0,
2201 };
2202
2203 if poster_gas > 0 {
2210 adjust_result_gas_used(&mut output.result.result, poster_gas);
2211 }
2212
2213 let mut total_donated_gas = 0u64;
2220 let mut retry_tx_hash_fixes: Vec<(usize, B256)> = Vec::new();
2222 if let ExecutionResult::Success { ref logs, .. } = output.result.result {
2223 let redeem_topic = arb_precompiles::redeem_scheduled_topic();
2224 let precompile_addr = arb_precompiles::ARBRETRYABLETX_ADDRESS;
2225
2226 for (log_idx, log) in logs.iter().enumerate() {
2227 if log.address != precompile_addr {
2228 continue;
2229 }
2230 if log.topics().is_empty() || log.topics()[0] != redeem_topic {
2231 continue;
2232 }
2233 if log.topics().len() < 4 || log.data.data.len() < 128 {
2234 continue;
2235 }
2236
2237 let ticket_id = log.topics()[1];
2238 let seq_num_bytes = log.topics()[3];
2239 let nonce =
2240 u64::from_be_bytes(seq_num_bytes.0[24..32].try_into().unwrap_or([0u8; 8]));
2241 let data = &log.data.data;
2242 let donated_gas = U256::from_be_slice(&data[0..32]).to::<u64>();
2243 total_donated_gas = total_donated_gas.saturating_add(donated_gas);
2244 let gas_donor = Address::from_slice(&data[44..64]);
2245 let max_refund = U256::from_be_slice(&data[64..96]);
2246 let submission_fee_refund = U256::from_be_slice(&data[96..128]);
2247
2248 let (encoded_retry_tx, latest_backlog) = {
2252 let current_time = {
2253 let block = self.inner.evm().block();
2254 revm::context::Block::timestamp(block).to::<u64>()
2255 };
2256 let chain_id = self.arb_ctx.chain_id;
2257 let basefee = self.arb_ctx.basefee;
2258 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
2259 let arb_state = ArbosState::open(db, SystemBurner::new(None, false))
2260 .map_err(BlockExecutionError::other)?;
2261 let state_ref = unsafe { arb_state.backing_storage.state_mut() };
2263
2264 let mut encoded_retry_tx = None;
2265 if let Ok(Some(retryable)) =
2266 arb_state
2267 .retryable_state
2268 .open_retryable(state_ref, ticket_id, current_time)
2269 {
2270 let _ = retryable.increment_num_tries(state_ref);
2271
2272 if let Ok(retry_tx) = retryable.make_tx(
2273 state_ref,
2274 U256::from(chain_id),
2275 nonce,
2276 basefee,
2277 donated_gas,
2278 ticket_id,
2279 gas_donor,
2280 max_refund,
2281 submission_fee_refund,
2282 ) {
2283 let mut encoded = Vec::new();
2284 encoded.push(ArbTxType::ArbitrumRetryTx.as_u8());
2285 alloy_rlp::Encodable::encode(&retry_tx, &mut encoded);
2286 let correct_hash = keccak256(&encoded);
2287 retry_tx_hash_fixes.push((log_idx, correct_hash));
2288 encoded_retry_tx = Some(encoded);
2289 }
2290 }
2291
2292 let _ = arb_state.l2_pricing_state.shrink_backlog(
2293 state_ref,
2294 donated_gas,
2295 MultiGas::default(),
2296 );
2297 let backlog = arb_state.l2_pricing_state.gas_backlog(state_ref).ok();
2298 (encoded_retry_tx, backlog)
2299 };
2300
2301 if let Some(encoded) = encoded_retry_tx
2302 && let Some(hooks) = self.arb_hooks.as_mut()
2303 {
2304 hooks.tx_proc.scheduled_txs.push(encoded);
2305 }
2306 if let Some(b) = latest_backlog {
2307 self.precompile_ctx.block.set_current_gas_backlog(b);
2308 }
2309 }
2310 }
2311
2312 if !retry_tx_hash_fixes.is_empty()
2316 && let ExecutionResult::Success { ref mut logs, .. } = output.result.result
2317 {
2318 for (log_idx, correct_hash) in &retry_tx_hash_fixes {
2319 if let Some(log) = logs.get_mut(*log_idx)
2320 && log.data.topics().len() > 2
2321 {
2322 let topics = log.data.topics_mut_unchecked();
2323 topics[2] = *correct_hash;
2324 }
2325 }
2326 }
2327
2328 let stylus_data_fee = if self.precompile_ctx.take_stylus_activation_addr().is_some()
2333 || self.precompile_ctx.take_stylus_keepalive_hash().is_some()
2334 {
2335 self.precompile_ctx.take_stylus_activation_data_fee()
2336 } else {
2337 U256::ZERO
2338 };
2339
2340 let stylus_multi_gas = self.precompile_ctx.stylus_multi_gas();
2346 let precompile_multi_gas = self.precompile_ctx.precompile_multi_gas();
2347 let dimensioned = stylus_multi_gas.saturating_add(precompile_multi_gas);
2348 let raw_gas_used = evm_gas_used.saturating_add(gas_refunded);
2355 let stylus_upfront_oog_gas = self.precompile_ctx.stylus_upfront_oog_gas();
2359 let dimensionable_gas = raw_gas_used.saturating_sub(stylus_upfront_oog_gas);
2360 let execution_multi_gas = match self.multi_gas_sink.lock().take() {
2361 Some(opcode_gas) => {
2362 let observed = intrinsic_multi_gas
2363 .saturating_add(opcode_gas)
2364 .saturating_add(dimensioned);
2365 let remainder = dimensionable_gas.saturating_sub(observed.single_gas());
2366 observed.saturating_add(MultiGas::computation_gas(remainder))
2367 }
2368 None => {
2369 let remainder = dimensionable_gas.saturating_sub(dimensioned.single_gas());
2370 dimensioned.saturating_add(MultiGas::computation_gas(remainder))
2371 }
2372 };
2373 debug_assert_eq!(
2377 execution_multi_gas.single_gas(),
2378 dimensionable_gas,
2379 "multi-gas split must total the dimensionable gas",
2380 );
2381 let mut charged_multi_gas =
2382 MultiGas::single_dim_gas(poster_gas).saturating_add(execution_multi_gas);
2383
2384 let gas_before_floor = output.result.result.gas_used();
2389 if calldata_floor_gas > gas_before_floor {
2390 let receipt_top_up = calldata_floor_gas - gas_before_floor;
2391 adjust_result_gas_used(&mut output.result.result, receipt_top_up);
2392 let dim_single = charged_multi_gas.single_gas();
2393 if calldata_floor_gas > dim_single {
2394 let dim_top_up = calldata_floor_gas - dim_single;
2395 charged_multi_gas =
2396 charged_multi_gas.saturating_add(MultiGas::l2_calldata_gas(dim_top_up));
2397 }
2398 }
2399
2400 let coinbase_tip_per_gas: u128 = effective_tip_per_gas;
2404 let capped_gas_price = should_drop_tip;
2405
2406 self.pending_tx = Some(PendingArbTx {
2407 sender,
2408 tx_gas_limit,
2409 arb_tx_type,
2410 poster_gas,
2411 evm_gas_used,
2412 charged_multi_gas,
2413 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
2414 stylus_data_fee,
2415 retry_context,
2416 coinbase_tip_per_gas,
2417 capped_gas_price,
2418 actual_gas_price,
2419 });
2420
2421 Ok(output)
2422 }
2423
2424 fn commit_transaction(&mut self, output: Self::Result) -> Result<u64, BlockExecutionError> {
2425 let pending = self.pending_tx.take();
2427 let gas_used_total = output.result.result.gas_used();
2428 let success = matches!(&output.result.result, ExecutionResult::Success { .. });
2429
2430 let mut withdrawal_value = U256::ZERO;
2434 if let ExecutionResult::Success { ref logs, .. } = output.result.result {
2435 let arbsys_addr = arb_precompiles::ARBSYS_ADDRESS;
2436 let l2_to_l1_tx_topic = keccak256(
2437 b"L2ToL1Tx(address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes)",
2438 );
2439 for log in logs {
2440 if log.address == arbsys_addr
2441 && !log.data.topics().is_empty()
2442 && log.data.topics()[0] == l2_to_l1_tx_topic
2443 {
2444 if log.data.data.len() >= 160 {
2447 let callvalue = U256::from_be_slice(&log.data.data[128..160]);
2448 withdrawal_value = withdrawal_value.saturating_add(callvalue);
2449 let val_i128: i128 = callvalue.try_into().unwrap_or(i128::MAX);
2450 self.expected_balance_delta =
2451 self.expected_balance_delta.saturating_sub(val_i128);
2452 }
2453 }
2454 }
2455 }
2456
2457 for addr in output.result.state.keys() {
2458 self.touched_accounts.insert(*addr);
2459 }
2460
2461 let gas_used = self.inner.commit_transaction(output)?;
2462
2463 if self
2467 .precompile_ctx
2468 .block
2469 .state_params_dirty
2470 .swap(false, std::sync::atomic::Ordering::Relaxed)
2471 {
2472 self.refresh_state_params();
2473 }
2474
2475 if let Some(ref p) = pending
2480 && !p.capped_gas_price
2481 && p.coinbase_tip_per_gas > 0
2482 && gas_used > 0
2483 {
2484 let coinbase = self.arb_ctx.coinbase;
2485 let net_acct = self.arb_ctx.network_fee_account;
2486 let compute_gas = gas_used.saturating_sub(p.poster_gas);
2487 let tip_to_network =
2488 U256::from(p.coinbase_tip_per_gas).saturating_mul(U256::from(compute_gas));
2489 if coinbase != net_acct && !tip_to_network.is_zero() {
2490 let overlay = &mut self.state_overlay;
2491 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
2492 if get_balance(db, coinbase) >= tip_to_network {
2493 let _ = arb_util::transfer_balance(
2494 Some(&coinbase),
2495 Some(&net_acct),
2496 tip_to_network,
2497 |f, t, a| apply_balance_op(db, overlay, f, t, a),
2498 );
2499 self.touched_accounts.insert(coinbase);
2500 self.touched_accounts.insert(net_acct);
2501 }
2502 }
2503 }
2504
2505 if let Some(ref p) = pending
2508 && !p.stylus_data_fee.is_zero()
2509 {
2510 let overlay = &mut self.state_overlay;
2511 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
2512 let _ = arb_util::burn_balance(&p.sender, p.stylus_data_fee, |f, t, a| {
2513 apply_balance_op(db, overlay, f, t, a)
2514 });
2515 let _ = arb_util::mint_balance(
2516 &self.arb_ctx.network_fee_account,
2517 p.stylus_data_fee,
2518 |f, t, a| apply_balance_op(db, overlay, f, t, a),
2519 );
2520 self.touched_accounts.insert(p.sender);
2521 self.touched_accounts
2522 .insert(self.arb_ctx.network_fee_account);
2523 }
2524
2525 if let Some((escrow, beneficiary)) = self.precompile_ctx.take_cancel_escrow_sweep() {
2529 let arbos_ver = self.arb_ctx.arbos_version;
2530 let touched_ptr = &mut self.touched_accounts as *mut rustc_hash::FxHashSet<Address>;
2531 let zombie_ptr = &mut self.zombie_accounts as *mut rustc_hash::FxHashSet<Address>;
2532 let finalise_ptr = &self.finalise_deleted as *const rustc_hash::FxHashSet<Address>;
2533 let overlay_ptr = &mut self.state_overlay as *mut StateOverlay;
2534 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
2535 let amount = get_balance(db, escrow);
2536
2537 unsafe {
2542 if amount.is_zero()
2543 && arbos_ver < arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS
2544 {
2545 create_zombie_if_deleted(
2546 db,
2547 &mut *overlay_ptr,
2548 escrow,
2549 &*finalise_ptr,
2550 &mut *zombie_ptr,
2551 &mut *touched_ptr,
2552 );
2553 }
2554 let _ = apply_balance_op(
2555 db,
2556 &mut *overlay_ptr,
2557 Some(&escrow),
2558 Some(&beneficiary),
2559 amount,
2560 );
2561 if !amount.is_zero() {
2562 (*zombie_ptr).remove(&escrow);
2563 }
2564 (*zombie_ptr).remove(&beneficiary);
2565 (*touched_ptr).insert(escrow);
2566 (*touched_ptr).insert(beneficiary);
2567 }
2568 }
2569
2570 if !withdrawal_value.is_zero() {
2572 let overlay = &mut self.state_overlay;
2573 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
2574 let _ = arb_util::burn_balance(
2575 &arb_precompiles::ARBSYS_ADDRESS,
2576 withdrawal_value,
2577 |f, t, a| apply_balance_op(db, overlay, f, t, a),
2578 );
2579 self.touched_accounts
2580 .insert(arb_precompiles::ARBSYS_ADDRESS);
2581 }
2582
2583 let poster_gas_for_receipt = pending.as_ref().map_or(0, |p| p.poster_gas);
2585 self.gas_used_for_l1.push(poster_gas_for_receipt);
2586 let multi_gas_for_receipt = pending
2587 .as_ref()
2588 .map_or(MultiGas::zero(), |p| p.charged_multi_gas);
2589 self.multi_gas_used.push(multi_gas_for_receipt);
2590
2591 if let Some(pending) = pending {
2593 let is_retry = pending.retry_context.is_some();
2594
2595 debug_assert!(
2597 gas_used_total <= pending.tx_gas_limit,
2598 "gas_used ({gas_used_total}) exceeds gas_limit ({})",
2599 pending.tx_gas_limit
2600 );
2601
2602 let sender_extra_gas = gas_used_total.saturating_sub(pending.evm_gas_used);
2607 if sender_extra_gas > 0 {
2608 let extra_cost = pending
2609 .actual_gas_price
2610 .saturating_mul(U256::from(sender_extra_gas));
2611 let overlay = &mut self.state_overlay;
2612 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
2613 let _ = arb_util::burn_balance(&pending.sender, extra_cost, |f, t, a| {
2614 apply_balance_op(db, overlay, f, t, a)
2615 });
2616 self.touched_accounts.insert(pending.sender);
2617 }
2618
2619 if let Some(retry_ctx) = pending.retry_context {
2620 let gas_left = pending.tx_gas_limit.saturating_sub(gas_used_total);
2622
2623 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
2624 let touched_ptr = &mut self.touched_accounts as *mut rustc_hash::FxHashSet<Address>;
2625 let zombie_ptr = &mut self.zombie_accounts as *mut rustc_hash::FxHashSet<Address>;
2626 let finalise_ptr = &self.finalise_deleted as *const rustc_hash::FxHashSet<Address>;
2627 let overlay_ptr = &mut self.state_overlay as *mut StateOverlay;
2628 let arbos_ver = self.arb_ctx.arbos_version;
2629
2630 let arb_state_retry = ArbosState::open(db, SystemBurner::new(None, false))
2631 .map_err(BlockExecutionError::other)?;
2632 let burn_storage = arb_state_retry.backing_storage.clone();
2636 let transfer_storage = arb_state_retry.backing_storage.clone();
2637 let delete_transfer_storage = arb_state_retry.backing_storage.clone();
2638 let delete_balance_storage = arb_state_retry.backing_storage.clone();
2639 let escrow_storage = arb_state_retry.backing_storage.clone();
2640
2641 let multi_dimensional_cost = if self.arb_ctx.arbos_version
2643 >= arb_chainspec::arbos_version::ARBOS_VERSION_MULTI_GAS_CONSTRAINTS
2644 {
2645 let cached = self.multi_gas_current_fees.get_or_init(|| {
2646 let state_ref = unsafe { arb_state_retry.backing_storage.state_mut() };
2648 arb_state_retry
2649 .l2_pricing_state
2650 .get_current_multi_gas_fees(state_ref)
2651 .unwrap_or([U256::ZERO; NUM_RESOURCE_KIND])
2652 });
2653 let state_ref = unsafe { arb_state_retry.backing_storage.state_mut() };
2655 arb_state_retry
2656 .l2_pricing_state
2657 .multi_dimensional_price_for_refund_with_fees(
2658 state_ref,
2659 pending.charged_multi_gas,
2660 cached,
2661 )
2662 .ok()
2663 } else {
2664 None
2665 };
2666
2667 let result = self.arb_hooks.as_ref().map(|hooks| {
2668 hooks.tx_proc.end_tx_retryable(
2669 &EndTxRetryableParams {
2670 gas_left,
2671 gas_used: gas_used_total,
2672 effective_base_fee: self.arb_ctx.basefee,
2673 from: pending.sender,
2674 refund_to: retry_ctx.refund_to,
2675 max_refund: retry_ctx.max_refund,
2676 submission_fee_refund: retry_ctx.submission_fee_refund,
2677 ticket_id: retry_ctx.ticket_id,
2678 value: U256::ZERO, success,
2680 network_fee_account: self.arb_ctx.network_fee_account,
2681 infra_fee_account: self.arb_ctx.infra_fee_account,
2682 min_base_fee: self.arb_ctx.min_base_fee,
2683 arbos_version: self.arb_ctx.arbos_version,
2684 multi_dimensional_cost,
2685 block_base_fee: self.arb_ctx.basefee,
2686 },
2687 |addr, amount| {
2688 unsafe {
2690 apply_burn_to_state(
2691 burn_storage.state_mut(),
2692 &mut *overlay_ptr,
2693 addr,
2694 amount,
2695 );
2696 (*touched_ptr).insert(addr);
2697 }
2698 },
2699 |from, to, amount| {
2700 unsafe {
2702 let state = transfer_storage.state_mut();
2703 if amount.is_zero()
2704 && arbos_ver
2705 < arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS
2706 {
2707 create_zombie_if_deleted(
2708 state,
2709 &mut *overlay_ptr,
2710 from,
2711 &*finalise_ptr,
2712 &mut *zombie_ptr,
2713 &mut *touched_ptr,
2714 );
2715 }
2716 let _ = apply_balance_op(
2721 state,
2722 &mut *overlay_ptr,
2723 Some(&from),
2724 Some(&to),
2725 amount,
2726 );
2727 if !amount.is_zero() {
2730 (*zombie_ptr).remove(&from);
2731 }
2732 (*zombie_ptr).remove(&to);
2734 (*touched_ptr).insert(from);
2735 (*touched_ptr).insert(to);
2736 }
2737 Ok(())
2738 },
2739 )
2740 });
2741
2742 if let Some(ref result) = result {
2743 if result.should_delete_retryable {
2744 let state_ref = unsafe { arb_state_retry.backing_storage.state_mut() };
2746 let _ = arb_state_retry.retryable_state.delete_retryable(
2747 state_ref,
2748 retry_ctx.ticket_id,
2749 |from, to, amount| {
2750 unsafe {
2752 let state = delete_transfer_storage.state_mut();
2753 if amount.is_zero()
2754 && arbos_ver
2755 < arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS
2756 {
2757 create_zombie_if_deleted(
2758 state,
2759 &mut *overlay_ptr,
2760 from,
2761 &*finalise_ptr,
2762 &mut *zombie_ptr,
2763 &mut *touched_ptr,
2764 );
2765 }
2766 let _ = apply_balance_op(
2773 state,
2774 &mut *overlay_ptr,
2775 Some(&from),
2776 Some(&to),
2777 amount,
2778 );
2779 if !amount.is_zero() {
2780 (*zombie_ptr).remove(&from);
2781 }
2782 (*zombie_ptr).remove(&to);
2783 (*touched_ptr).insert(from);
2784 (*touched_ptr).insert(to);
2785 }
2786 Ok(())
2787 },
2788 |addr| {
2789 unsafe { get_balance(delete_balance_storage.state_mut(), addr) }
2791 },
2792 );
2793 } else if result.should_return_value_to_escrow {
2794 unsafe {
2797 let state = escrow_storage.state_mut();
2798 if retry_ctx.call_value.is_zero()
2799 && arbos_ver < arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS
2800 {
2801 create_zombie_if_deleted(
2802 state,
2803 &mut *overlay_ptr,
2804 pending.sender,
2805 &*finalise_ptr,
2806 &mut *zombie_ptr,
2807 &mut *touched_ptr,
2808 );
2809 }
2810 let _ = arb_util::transfer_balance(
2811 Some(&pending.sender),
2812 Some(&result.escrow_address),
2813 retry_ctx.call_value,
2814 |f, t, a| {
2815 apply_balance_op(
2816 escrow_storage.state_mut(),
2817 &mut *overlay_ptr,
2818 f,
2819 t,
2820 a,
2821 )
2822 },
2823 );
2824 if !retry_ctx.call_value.is_zero() {
2826 (*zombie_ptr).remove(&pending.sender);
2827 }
2828 (*zombie_ptr).remove(&result.escrow_address);
2830 (*touched_ptr).insert(pending.sender);
2831 (*touched_ptr).insert(result.escrow_address);
2832 }
2833 }
2834
2835 let state_ref = unsafe { arb_state_retry.backing_storage.state_mut() };
2837 let _ = arb_state_retry.l2_pricing_state.grow_backlog(
2838 state_ref,
2839 result.compute_gas_for_backlog,
2840 pending.charged_multi_gas,
2841 );
2842 if let Ok(b) = arb_state_retry.l2_pricing_state.gas_backlog(state_ref) {
2843 self.precompile_ctx.block.set_current_gas_backlog(b);
2844 }
2845 }
2846 } else if matches!(
2847 pending.arb_tx_type,
2848 None | Some(ArbTxType::ArbitrumLegacyTx)
2849 | Some(ArbTxType::ArbitrumUnsignedTx)
2850 | Some(ArbTxType::ArbitrumContractTx)
2851 ) {
2852 let gas_left = pending.tx_gas_limit.saturating_sub(gas_used_total);
2856
2857 let fee_dist = self.arb_hooks.as_ref().map(|hooks| {
2858 hooks.compute_end_tx_fees(&EndTxContext {
2859 sender: pending.sender,
2860 gas_left,
2861 gas_used: gas_used_total,
2862 gas_price: self.arb_ctx.basefee,
2863 base_fee: self.arb_ctx.basefee,
2864 tx_type: pending.arb_tx_type.unwrap_or(ArbTxType::ArbitrumLegacyTx),
2865 success,
2866 refund_to: pending.sender,
2867 })
2868 });
2869
2870 if let Some(ref dist) = fee_dist {
2871 {
2872 let overlay = &mut self.state_overlay;
2873 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
2874 apply_fee_distribution(db, overlay, dist, None);
2875 }
2876 if !dist.network_fee_amount.is_zero() {
2879 self.touched_accounts.insert(dist.network_fee_account);
2880 }
2881 self.touched_accounts.insert(dist.infra_fee_account);
2882 self.touched_accounts.insert(dist.poster_fee_destination);
2883
2884 let arbos_version_active = self.arb_ctx.arbos_version;
2885 let basefee_active = self.arb_ctx.basefee;
2886 let charged_multi_gas = pending.charged_multi_gas;
2887 let poster_gas_active = pending.poster_gas;
2888 let gas_price_positive_active = pending.gas_price_positive;
2889
2890 let (refund_done, new_backlog) = {
2891 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
2892 let arb_state_post = ArbosState::open(db, SystemBurner::new(None, false))
2893 .map_err(BlockExecutionError::other)?;
2894 let refund_storage = arb_state_post.backing_storage.clone();
2899 let overlay_ptr = &mut self.state_overlay as *mut StateOverlay;
2900
2901 let mut refund_done = false;
2902 if arbos_version_active
2903 >= arb_chainspec::arbos_version::ARBOS_VERSION_MULTI_GAS_CONSTRAINTS
2904 {
2905 let total_cost =
2906 basefee_active.saturating_mul(U256::from(gas_used_total));
2907 let cached = self.multi_gas_current_fees.get_or_init(|| {
2908 let state_ref =
2910 unsafe { arb_state_post.backing_storage.state_mut() };
2911 arb_state_post
2912 .l2_pricing_state
2913 .get_current_multi_gas_fees(state_ref)
2914 .unwrap_or([U256::ZERO; NUM_RESOURCE_KIND])
2915 });
2916 let state_ref = unsafe { arb_state_post.backing_storage.state_mut() };
2918 let multi_cost = arb_state_post
2919 .l2_pricing_state
2920 .multi_dimensional_price_for_refund_with_fees(
2921 state_ref,
2922 charged_multi_gas,
2923 cached,
2924 )
2925 .unwrap_or(total_cost);
2926 if total_cost > multi_cost {
2927 let refund_amount = total_cost.saturating_sub(multi_cost);
2928 let _ = arb_util::transfer_balance(
2929 Some(&dist.network_fee_account),
2930 Some(&pending.sender),
2931 refund_amount,
2932 |f, t, a| {
2933 unsafe {
2935 apply_balance_op(
2936 refund_storage.state_mut(),
2937 &mut *overlay_ptr,
2938 f,
2939 t,
2940 a,
2941 )
2942 }
2943 },
2944 );
2945 refund_done = true;
2946 }
2947 }
2948
2949 let used_multi_gas = charged_multi_gas
2954 .saturating_sub(MultiGas::single_dim_gas(poster_gas_active));
2955
2956 let mut new_backlog: Option<u64> = None;
2957 if gas_price_positive_active {
2958 let state_ref = unsafe { arb_state_post.backing_storage.state_mut() };
2960 let _ = arb_state_post.l2_pricing_state.grow_backlog(
2961 state_ref,
2962 dist.compute_gas_for_backlog,
2963 used_multi_gas,
2964 );
2965 new_backlog =
2966 arb_state_post.l2_pricing_state.gas_backlog(state_ref).ok();
2967 }
2968 if !dist.l1_fees_to_add.is_zero() {
2969 let state_ref = unsafe { arb_state_post.backing_storage.state_mut() };
2971 let _ = arb_state_post
2972 .l1_pricing_state
2973 .add_to_l1_fees_available(state_ref, dist.l1_fees_to_add);
2974 }
2975
2976 (refund_done, new_backlog)
2977 };
2978
2979 if refund_done {
2980 self.touched_accounts.insert(dist.network_fee_account);
2981 self.touched_accounts.insert(pending.sender);
2982 }
2983 if let Some(b) = new_backlog {
2984 self.precompile_ctx.block.set_current_gas_backlog(b);
2985 }
2986 }
2987 }
2988
2989 let mut adjusted_gas_used = gas_used_total;
2993 if self.arb_ctx.arbos_version
2994 >= arb_chainspec::arbos_version::ARBOS_VERSION_FIX_REDEEM_GAS
2995 && let Some(hooks) = self.arb_hooks.as_ref()
2996 {
2997 for scheduled in &hooks.tx_proc.scheduled_txs {
2998 if let Some(retry_gas) = decode_retry_tx_gas(scheduled) {
2999 adjusted_gas_used = adjusted_gas_used.saturating_sub(retry_gas);
3000 }
3001 }
3002 }
3003
3004 const TX_GAS: u64 = 21_000;
3006 let data_gas = pending.poster_gas;
3007 let compute_used = if adjusted_gas_used < data_gas {
3008 TX_GAS
3009 } else {
3010 let compute = adjusted_gas_used - data_gas;
3011 if compute < TX_GAS { TX_GAS } else { compute }
3012 };
3013 self.block_gas_left = self.block_gas_left.saturating_sub(compute_used);
3014
3015 let is_user_tx = !matches!(
3017 pending.arb_tx_type,
3018 Some(ArbTxType::ArbitrumInternalTx)
3019 | Some(ArbTxType::ArbitrumDepositTx)
3020 | Some(ArbTxType::ArbitrumSubmitRetryableTx)
3021 | Some(ArbTxType::ArbitrumRetryTx)
3022 );
3023 if is_user_tx {
3024 self.user_txs_processed += 1;
3025 }
3026
3027 let _ = is_retry; }
3029
3030 self.precompile_ctx.reset_tx();
3031
3032 {
3042 let keccak_empty = alloy_primitives::B256::from(alloy_primitives::keccak256([]));
3043 let overlay = &mut self.state_overlay;
3044 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
3045 let to_remove: Vec<Address> = self
3046 .touched_accounts
3047 .drain()
3048 .filter(|addr| {
3049 if self.zombie_accounts.contains(addr) {
3051 return false;
3052 }
3053 if let Some(cached) = db.cache.accounts.get(addr)
3054 && let Some(ref acct) = cached.account
3055 {
3056 let is_empty = acct.info.nonce == 0
3057 && acct.info.balance.is_zero()
3058 && acct.info.code_hash == keccak_empty;
3059 return is_empty;
3060 }
3061 false
3062 })
3063 .collect();
3064
3065 for addr in &to_remove {
3073 overlay.record_pre_touch(db, *addr);
3074 if let Some(cached) = db.cache.accounts.get_mut(addr) {
3075 cached.account = None;
3076 cached.status = revm_database::AccountStatus::LoadedNotExisting;
3077 }
3078 }
3079 self.finalise_deleted.extend(to_remove);
3080 }
3081
3082 {
3083 let overlay = &mut self.state_overlay;
3084 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
3085 overlay.drain_and_apply(db, &self.zombie_accounts);
3086 }
3087
3088 Ok(gas_used)
3089 }
3090
3091 fn finish(self) -> Result<(Self::Evm, BlockExecutionResult<R::Receipt>), BlockExecutionError> {
3092 if self.expected_balance_delta != 0 {
3094 tracing::trace!(
3095 target: "arb::executor",
3096 delta = self.expected_balance_delta,
3097 "expected balance delta from deposits/withdrawals"
3098 );
3099 }
3100 let mut result = BlockExecutionResult {
3104 receipts: self.inner.receipts,
3105 requests: Default::default(),
3106 gas_used: self.inner.gas_used,
3107 blob_gas_used: self.inner.blob_gas_used,
3108 };
3109 for (i, receipt) in result.receipts.iter_mut().enumerate() {
3111 if let Some(&l1_gas) = self.gas_used_for_l1.get(i) {
3112 arb_primitives::SetArbReceiptFields::set_gas_used_for_l1(receipt, l1_gas);
3113 }
3114 if let Some(&multi_gas) = self.multi_gas_used.get(i) {
3115 arb_primitives::SetArbReceiptFields::set_multi_gas_used(receipt, multi_gas);
3116 }
3117 }
3118 Ok((self.inner.evm, result))
3119 }
3120
3121 fn set_state_hook(&mut self, hook: Option<Box<dyn OnStateHook>>) {
3122 self.inner.set_state_hook(hook);
3123 }
3124
3125 fn evm_mut(&mut self) -> &mut Self::Evm {
3126 self.inner.evm_mut()
3127 }
3128
3129 fn evm(&self) -> &Self::Evm {
3130 self.inner.evm()
3131 }
3132
3133 fn receipts(&self) -> &[Self::Receipt] {
3134 self.inner.receipts()
3135 }
3136}
3137
3138fn adjust_result_gas_used<H>(result: &mut ExecutionResult<H>, extra_gas: u64) {
3147 match result {
3148 ExecutionResult::Success { gas_used, .. } => *gas_used = gas_used.saturating_add(extra_gas),
3149 ExecutionResult::Revert { gas_used, .. } => *gas_used = gas_used.saturating_add(extra_gas),
3150 ExecutionResult::Halt { gas_used, .. } => *gas_used = gas_used.saturating_add(extra_gas),
3151 }
3152}
3153
3154fn apply_mint_to_state<DB: Database>(
3156 state: &mut State<DB>,
3157 overlay: &mut StateOverlay,
3158 address: Address,
3159 amount: U256,
3160) {
3161 if amount.is_zero() {
3162 return;
3163 }
3164 overlay.record_pre_touch(state, address);
3165 if let Some(cache_acct) = state.cache.accounts.get_mut(&address) {
3166 if let Some(ref mut acct) = cache_acct.account {
3167 acct.info.balance = acct.info.balance.saturating_add(amount);
3168 } else {
3169 cache_acct.account = Some(revm_database::states::plain_account::PlainAccount {
3170 info: revm_state::AccountInfo {
3171 balance: amount,
3172 ..Default::default()
3173 },
3174 storage: Default::default(),
3175 });
3176 }
3177 }
3178}
3179
3180fn materialise_empty<DB: Database>(
3185 state: &mut State<DB>,
3186 overlay: &mut StateOverlay,
3187 addr: Address,
3188 touched: &mut rustc_hash::FxHashSet<Address>,
3189) {
3190 overlay.record_pre_touch(state, addr);
3191 let _ = state.load_cache_account(addr);
3192 if let Some(cached) = state.cache.accounts.get_mut(&addr)
3193 && cached.account.is_none()
3194 {
3195 cached.account = Some(revm_database::states::plain_account::PlainAccount {
3196 info: revm_state::AccountInfo::default(),
3197 storage: Default::default(),
3198 });
3199 cached.status = revm_database::AccountStatus::InMemoryChange;
3200 }
3201 touched.insert(addr);
3202}
3203
3204fn apply_burn_to_state<DB: Database>(
3206 state: &mut State<DB>,
3207 overlay: &mut StateOverlay,
3208 address: Address,
3209 amount: U256,
3210) {
3211 if amount.is_zero() {
3212 return;
3213 }
3214 overlay.record_pre_touch(state, address);
3215 if let Some(cache_acct) = state.cache.accounts.get_mut(&address)
3216 && let Some(ref mut acct) = cache_acct.account
3217 {
3218 acct.info.balance = acct.info.balance.saturating_sub(amount);
3219 }
3220}
3221
3222fn apply_balance_op<DB: Database>(
3230 state: &mut State<DB>,
3231 overlay: &mut StateOverlay,
3232 from: Option<&Address>,
3233 to: Option<&Address>,
3234 amount: U256,
3235) -> Result<(), BalanceError> {
3236 if amount.is_zero() {
3237 return Ok(());
3238 }
3239 match (from, to) {
3240 (Some(from_addr), Some(to_addr)) => {
3241 let available = get_balance(state, *from_addr);
3242 if available < amount {
3243 return Err(BalanceError::InsufficientBalance {
3244 account: *from_addr,
3245 available,
3246 requested: amount,
3247 });
3248 }
3249 apply_burn_to_state(state, overlay, *from_addr, amount);
3250 apply_mint_to_state(state, overlay, *to_addr, amount);
3251 }
3252 (Some(from_addr), None) => {
3253 apply_burn_to_state(state, overlay, *from_addr, amount);
3254 }
3255 (None, Some(to_addr)) => {
3256 apply_mint_to_state(state, overlay, *to_addr, amount);
3257 }
3258 (None, None) => {}
3259 }
3260 Ok(())
3261}
3262
3263fn increment_nonce<DB: Database>(
3265 state: &mut State<DB>,
3266 overlay: &mut StateOverlay,
3267 address: Address,
3268) {
3269 overlay.record_pre_touch(state, address);
3270 if let Some(cache_acct) = state.cache.accounts.get_mut(&address)
3271 && let Some(ref mut acct) = cache_acct.account
3272 {
3273 acct.info.nonce += 1;
3274 }
3275}
3276
3277fn get_balance<DB: Database>(state: &mut State<DB>, address: Address) -> U256 {
3279 match revm::Database::basic(state, address) {
3280 Ok(Some(info)) => info.balance,
3281 _ => U256::ZERO,
3282 }
3283}
3284
3285fn create_zombie_if_deleted<DB: Database>(
3291 state: &mut State<DB>,
3292 overlay: &mut StateOverlay,
3293 addr: Address,
3294 finalise_deleted: &rustc_hash::FxHashSet<Address>,
3295 zombie_accounts: &mut rustc_hash::FxHashSet<Address>,
3296 touched_accounts: &mut rustc_hash::FxHashSet<Address>,
3297) {
3298 overlay.record_pre_touch(state, addr);
3299 let account_missing = state
3300 .cache
3301 .accounts
3302 .get(&addr)
3303 .is_none_or(|c| c.account.is_none());
3304 if account_missing && finalise_deleted.contains(&addr) {
3305 if let Some(cached) = state.cache.accounts.get_mut(&addr) {
3306 cached.account = Some(revm_database::states::plain_account::PlainAccount {
3307 info: revm_state::AccountInfo::default(),
3308 storage: Default::default(),
3309 });
3310 cached.status = revm_database::AccountStatus::InMemoryChange;
3311 }
3312 zombie_accounts.insert(addr);
3313 touched_accounts.insert(addr);
3314 }
3315}
3316
3317fn apply_fee_distribution<DB: Database>(
3319 state: &mut State<DB>,
3320 overlay: &mut StateOverlay,
3321 dist: &EndTxFeeDistribution,
3322 l1_pricing: Option<&l1_pricing::L1PricingState<DB>>,
3323) {
3324 if !dist.network_fee_amount.is_zero() {
3327 let _ = arb_util::mint_balance(
3328 &dist.network_fee_account,
3329 dist.network_fee_amount,
3330 |f, t, a| apply_balance_op(state, overlay, f, t, a),
3331 );
3332 }
3333 let _ = arb_util::mint_balance(&dist.infra_fee_account, dist.infra_fee_amount, |f, t, a| {
3334 apply_balance_op(state, overlay, f, t, a)
3335 });
3336 let _ = arb_util::mint_balance(
3337 &dist.poster_fee_destination,
3338 dist.poster_fee_amount,
3339 |f, t, a| apply_balance_op(state, overlay, f, t, a),
3340 );
3341
3342 if !dist.l1_fees_to_add.is_zero()
3343 && let Some(l1_state) = l1_pricing
3344 {
3345 let _ = l1_state.add_to_l1_fees_available(state, dist.l1_fees_to_add);
3346 }
3347
3348 tracing::trace!(
3349 target: "arb::executor",
3350 network_fee = %dist.network_fee_amount,
3351 infra_fee = %dist.infra_fee_amount,
3352 poster_fee = %dist.poster_fee_amount,
3353 poster_dest = %dist.poster_fee_destination,
3354 l1_fees_added = %dist.l1_fees_to_add,
3355 backlog_gas = dist.compute_gas_for_backlog,
3356 "applied fee distribution"
3357 );
3358}
3359
3360fn estimate_intrinsic_gas(tx: &impl Transaction, spec: revm::primitives::hardfork::SpecId) -> u64 {
3366 const TX_GAS: u64 = 21_000;
3367 const TX_CREATE_GAS: u64 = 32_000;
3368 const TX_DATA_ZERO_GAS: u64 = 4;
3369 const TX_DATA_NON_ZERO_GAS: u64 = 16;
3370 const TX_ACCESS_LIST_ADDRESS_GAS: u64 = 2400;
3371 const TX_ACCESS_LIST_STORAGE_KEY_GAS: u64 = 1900;
3372 const INIT_CODE_WORD_GAS: u64 = 2;
3373
3374 let is_create = tx.to().is_none();
3375
3376 let mut gas = TX_GAS;
3377 if is_create {
3378 gas += TX_CREATE_GAS;
3379 }
3380
3381 let data = tx.input();
3382
3383 let data_gas: u64 = data
3385 .iter()
3386 .map(|&b| {
3387 if b == 0 {
3388 TX_DATA_ZERO_GAS
3389 } else {
3390 TX_DATA_NON_ZERO_GAS
3391 }
3392 })
3393 .sum();
3394 gas = gas.saturating_add(data_gas);
3395
3396 if let Some(access_list) = tx.access_list() {
3398 for item in access_list.iter() {
3399 gas = gas.saturating_add(TX_ACCESS_LIST_ADDRESS_GAS);
3400 gas = gas.saturating_add(
3401 (item.storage_keys.len() as u64).saturating_mul(TX_ACCESS_LIST_STORAGE_KEY_GAS),
3402 );
3403 }
3404 }
3405
3406 if spec.is_enabled_in(revm::primitives::hardfork::SpecId::SHANGHAI)
3408 && is_create
3409 && !data.is_empty()
3410 {
3411 let words = (data.len() as u64).div_ceil(32);
3412 gas = gas.saturating_add(words.saturating_mul(INIT_CODE_WORD_GAS));
3413 }
3414
3415 gas
3416}
3417
3418fn tx_intrinsic_multi_gas(
3423 tx: &impl Transaction,
3424 spec: revm::primitives::hardfork::SpecId,
3425) -> MultiGas {
3426 let is_create = tx.to().is_none();
3427 let data = tx.input();
3428 let zero_bytes = data.iter().filter(|&&b| b == 0).count() as u64;
3429 let nonzero_bytes = data.len() as u64 - zero_bytes;
3430 let (access_list_addresses, access_list_keys) = tx.access_list().map_or((0, 0), |al| {
3431 let mut addrs = 0u64;
3432 let mut keys = 0u64;
3433 for item in al.iter() {
3434 addrs += 1;
3435 keys += item.storage_keys.len() as u64;
3436 }
3437 (addrs, keys)
3438 });
3439 let init_code_words = if is_create
3440 && spec.is_enabled_in(revm::primitives::hardfork::SpecId::SHANGHAI)
3441 && !data.is_empty()
3442 {
3443 (data.len() as u64).div_ceil(32)
3444 } else {
3445 0
3446 };
3447 crate::multi_gas::intrinsic_multigas(crate::multi_gas::IntrinsicInput {
3448 is_create,
3449 zero_bytes,
3450 nonzero_bytes,
3451 init_code_words,
3452 access_list_addresses,
3453 access_list_keys,
3454 auth_list_len: tx.authorization_list().map_or(0, |l| l.len()) as u64,
3455 })
3456}
3457
3458fn tx_floor_data_gas(tx: &impl Transaction) -> u64 {
3462 const TX_GAS: u64 = 21_000;
3463 const TX_TOKEN_PER_NONZERO_BYTE: u64 = 4;
3464 const TX_COST_FLOOR_PER_TOKEN: u64 = 10;
3465 let data = tx.input();
3466 let zero = data.iter().filter(|&&b| b == 0).count() as u64;
3467 let nonzero = (data.len() as u64).saturating_sub(zero);
3468 let tokens = nonzero
3469 .saturating_mul(TX_TOKEN_PER_NONZERO_BYTE)
3470 .saturating_add(zero);
3471 TX_GAS.saturating_add(tokens.saturating_mul(TX_COST_FLOOR_PER_TOKEN))
3472}
3473
3474fn decode_extra_fields(extra_bytes: &[u8]) -> (u64, u64) {
3477 let delayed = if extra_bytes.len() >= 40 {
3478 let mut buf = [0u8; 8];
3479 buf.copy_from_slice(&extra_bytes[32..40]);
3480 u64::from_be_bytes(buf)
3481 } else {
3482 0
3483 };
3484 let l2_block = if extra_bytes.len() >= 48 {
3485 let mut buf = [0u8; 8];
3486 buf.copy_from_slice(&extra_bytes[40..48]);
3487 u64::from_be_bytes(buf)
3488 } else {
3489 0
3490 };
3491 (delayed, l2_block)
3492}
3493
3494fn process_parent_block_hash<DB: Database>(
3498 state: &mut State<DB>,
3499 l2_block_number: u64,
3500 prev_hash: B256,
3501) {
3502 use arb_primitives::arbos_versions::HISTORY_STORAGE_ADDRESS;
3503
3504 const HISTORY_SERVE_WINDOW: u64 = 393168;
3506
3507 if l2_block_number == 0 {
3508 return;
3509 }
3510
3511 let slot = U256::from((l2_block_number - 1) % HISTORY_SERVE_WINDOW);
3512 let value = U256::from_be_slice(prev_hash.as_slice());
3513
3514 arb_storage::write_storage_at(state, HISTORY_STORAGE_ADDRESS, slot, value)
3515 .expect("HISTORY_STORAGE write must succeed: in-memory state writes are infallible");
3516}
3517
3518fn decode_retry_tx_gas(encoded: &[u8]) -> Option<u64> {
3522 if encoded.is_empty() {
3523 return None;
3524 }
3525 if encoded[0] != ArbTxType::ArbitrumRetryTx.as_u8() {
3526 tracing::warn!(
3527 target: "arb::executor",
3528 tx_type = encoded[0],
3529 "unexpected scheduled tx type"
3530 );
3531 return None;
3532 }
3533 let rlp_data = &encoded[1..];
3534 let retry =
3535 <arb_alloy_consensus::tx::ArbRetryTx as alloy_rlp::Decodable>::decode(&mut &rlp_data[..])
3536 .ok()?;
3537 Some(retry.gas)
3538}
3539
3540#[cfg(test)]
3541mod tests {
3542 use std::collections::HashMap;
3543
3544 use alloy_primitives::{B256, U256};
3545 use arb_context::BlockCtx;
3546
3547 use super::populate_l2_block_hash_window;
3548
3549 #[test]
3550 fn fills_window_from_parent_and_lookup() {
3551 let block = BlockCtx::new(60, 0, 0, 1000, false);
3552 let parent_hash = B256::repeat_byte(0xfe);
3553 let mut db: HashMap<u64, B256> = (744..=998)
3554 .map(|n| (n, B256::from(U256::from(n))))
3555 .collect();
3556 populate_l2_block_hash_window(&block, 1000, parent_hash, |n| db.remove(&n));
3557
3558 assert_eq!(block.cached_l2_block_hash(999), Some(parent_hash));
3559 assert_eq!(
3560 block.cached_l2_block_hash(998),
3561 Some(B256::from(U256::from(998u64)))
3562 );
3563 assert_eq!(
3564 block.cached_l2_block_hash(744),
3565 Some(B256::from(U256::from(744u64)))
3566 );
3567 assert_eq!(block.cached_l2_block_hash(743), None);
3568 }
3569
3570 #[test]
3571 fn keeps_chunk_internal_and_stops_at_gap() {
3572 let block = BlockCtx::new(60, 0, 0, 1000, false);
3573 populate_l2_block_hash_window(&block, 1000, B256::repeat_byte(0xfe), |n| {
3574 (n == 997).then(|| B256::repeat_byte(0x97))
3575 });
3576 assert_eq!(
3577 block.cached_l2_block_hash(999),
3578 Some(B256::repeat_byte(0xfe))
3579 );
3580 assert_eq!(block.cached_l2_block_hash(998), None);
3581 assert_eq!(block.cached_l2_block_hash(997), None);
3582 }
3583}