1use alloy_consensus::{Transaction, TransactionEnvelope, TxReceipt};
2use alloy_eips::eip2718::{Encodable2718, Typed2718};
3use alloy_evm::{
4 block::{
5 BlockExecutionError, BlockExecutionResult, BlockExecutor, BlockExecutorFactory,
6 BlockExecutorFor, ExecutableTx, OnStateHook,
7 },
8 eth::{
9 receipt_builder::ReceiptBuilder, spec::EthExecutorSpec, EthBlockExecutionCtx,
10 EthBlockExecutor, EthTxResult,
11 },
12 tx::{FromRecoveredTx, FromTxWithEncoded},
13 Database, Evm, EvmFactory, RecoveredTx,
14};
15use alloy_primitives::{keccak256, Address, Log, TxKind, B256, U256};
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 compute_poster_gas, compute_submit_retryable_fees, EndTxFeeDistribution,
29 EndTxRetryableParams, SubmitRetryableParams,
30 },
31 util::tx_type_has_poster_costs,
32};
33use reth_evm::TransactionEnv;
34use revm::{
35 context::{result::ExecutionResult, TxEnv},
36 database::State,
37 inspector::Inspector,
38};
39
40use crate::{
41 context::ArbBlockExecutionCtx,
42 executor::DefaultArbOsHooks,
43 hooks::{ArbOsHooks, EndTxContext},
44};
45
46pub trait ArbTransactionEnv: TransactionEnv {
51 fn set_gas_price(&mut self, gas_price: u128);
53 fn set_gas_priority_fee(&mut self, fee: Option<u128>);
55 fn set_value(&mut self, value: U256);
57}
58
59impl ArbTransactionEnv for TxEnv {
60 fn set_gas_price(&mut self, gas_price: u128) {
61 self.gas_price = gas_price;
62 }
63 fn set_gas_priority_fee(&mut self, fee: Option<u128>) {
64 self.gas_priority_fee = fee;
65 }
66 fn set_value(&mut self, value: U256) {
67 self.value = value;
68 }
69}
70
71pub trait ArbScheduledTxDrain {
77 fn drain_scheduled_txs(&mut self) -> Vec<Vec<u8>>;
80}
81
82impl<'a, Evm, Spec, R: ReceiptBuilder> ArbScheduledTxDrain for ArbBlockExecutor<'a, Evm, Spec, R> {
83 fn drain_scheduled_txs(&mut self) -> Vec<Vec<u8>> {
84 self.arb_hooks
85 .as_mut()
86 .map(|hooks| std::mem::take(&mut hooks.tx_proc.scheduled_txs))
87 .unwrap_or_default()
88 }
89}
90
91#[derive(Debug, Clone)]
96pub struct ArbBlockExecutorFactory<R, Spec, EvmF> {
97 receipt_builder: R,
98 spec: Spec,
99 evm_factory: EvmF,
100}
101
102impl<R, Spec, EvmF> ArbBlockExecutorFactory<R, Spec, EvmF> {
103 pub fn new(receipt_builder: R, spec: Spec, evm_factory: EvmF) -> Self {
104 Self {
105 receipt_builder,
106 spec,
107 evm_factory,
108 }
109 }
110
111 pub fn create_arb_executor<'a, DB, I>(
116 &'a self,
117 evm: EvmF::Evm<&'a mut State<DB>, I>,
118 ctx: EthBlockExecutionCtx<'a>,
119 chain_id: u64,
120 ) -> ArbBlockExecutor<'a, EvmF::Evm<&'a mut State<DB>, I>, &'a Spec, &'a R>
121 where
122 DB: Database + 'a,
123 R: ReceiptBuilder,
124 Spec: EthExecutorSpec + Clone,
125 I: Inspector<EvmF::Context<&'a mut State<DB>>> + 'a,
126 EvmF: EvmFactory,
127 {
128 let extra_bytes = ctx.extra_data.as_ref();
129 let (delayed_messages_read, l2_block_number) = decode_extra_fields(extra_bytes);
130 let arb_ctx = ArbBlockExecutionCtx {
131 parent_hash: ctx.parent_hash,
132 parent_beacon_block_root: ctx.parent_beacon_block_root,
133 extra_data: extra_bytes[..core::cmp::min(extra_bytes.len(), 32)].to_vec(),
134 delayed_messages_read,
135 l2_block_number,
136 chain_id,
137 ..Default::default()
138 };
139 ArbBlockExecutor {
140 inner: EthBlockExecutor::new(evm, ctx, &self.spec, &self.receipt_builder),
141 arb_hooks: None,
142 arb_ctx,
143 pending_tx: None,
144 block_gas_left: 0,
145 user_txs_processed: 0,
146 gas_used_for_l1: Vec::new(),
147 multi_gas_used: Vec::new(),
148 expected_balance_delta: 0,
149 zombie_accounts: rustc_hash::FxHashSet::default(),
150 finalise_deleted: rustc_hash::FxHashSet::default(),
151 touched_accounts: rustc_hash::FxHashSet::default(),
152 multi_gas_current_fees: std::sync::OnceLock::new(),
153 }
154 }
155}
156
157impl<R, Spec, EvmF> BlockExecutorFactory for ArbBlockExecutorFactory<R, Spec, EvmF>
158where
159 R: ReceiptBuilder<
160 Transaction: Transaction + Encodable2718 + ArbTransactionExt,
161 Receipt: TxReceipt<Log = Log> + arb_primitives::SetArbReceiptFields,
162 > + 'static,
163 Spec: EthExecutorSpec + Clone + 'static,
164 EvmF: EvmFactory<
165 Tx: FromRecoveredTx<R::Transaction> + FromTxWithEncoded<R::Transaction> + ArbTransactionEnv,
166 >,
167 Self: 'static,
168{
169 type EvmFactory = EvmF;
170 type ExecutionCtx<'a> = EthBlockExecutionCtx<'a>;
171 type Transaction = R::Transaction;
172 type Receipt = R::Receipt;
173
174 fn evm_factory(&self) -> &Self::EvmFactory {
175 &self.evm_factory
176 }
177
178 fn create_executor<'a, DB, I>(
179 &'a self,
180 evm: EvmF::Evm<&'a mut State<DB>, I>,
181 ctx: Self::ExecutionCtx<'a>,
182 ) -> impl BlockExecutorFor<'a, Self, DB, I>
183 where
184 DB: Database + 'a,
185 I: Inspector<EvmF::Context<&'a mut State<DB>>> + 'a,
186 {
187 let extra_bytes = ctx.extra_data.as_ref();
188 let (delayed_messages_read, l2_block_number) = decode_extra_fields(extra_bytes);
189 let arb_ctx = ArbBlockExecutionCtx {
190 parent_hash: ctx.parent_hash,
191 parent_beacon_block_root: ctx.parent_beacon_block_root,
192 extra_data: extra_bytes[..core::cmp::min(extra_bytes.len(), 32)].to_vec(),
193 delayed_messages_read,
194 l2_block_number,
195 ..Default::default()
196 };
197 ArbBlockExecutor {
198 inner: EthBlockExecutor::new(evm, ctx, &self.spec, &self.receipt_builder),
199 arb_hooks: None,
200 arb_ctx,
201 pending_tx: None,
202 block_gas_left: 0, user_txs_processed: 0,
204 gas_used_for_l1: Vec::new(),
205 multi_gas_used: Vec::new(),
206 expected_balance_delta: 0,
207 zombie_accounts: rustc_hash::FxHashSet::default(),
208 finalise_deleted: rustc_hash::FxHashSet::default(),
209 touched_accounts: rustc_hash::FxHashSet::default(),
210 multi_gas_current_fees: std::sync::OnceLock::new(),
211 }
212 }
213}
214
215struct PendingArbTx {
221 sender: Address,
222 tx_gas_limit: u64,
223 arb_tx_type: Option<ArbTxType>,
224 poster_gas: u64,
225 evm_gas_used: u64,
227 charged_multi_gas: MultiGas,
228 gas_price_positive: bool,
229 stylus_data_fee: U256,
230 retry_context: Option<PendingRetryContext>,
231 coinbase_tip_per_gas: u128,
232 capped_gas_price: bool,
236 actual_gas_price: U256,
240}
241
242struct PendingRetryContext {
244 ticket_id: alloy_primitives::B256,
245 refund_to: Address,
246 #[allow(dead_code)]
247 gas_fee_cap: U256,
248 max_refund: U256,
249 submission_fee_refund: U256,
250 call_value: U256,
252}
253
254pub struct ArbBlockExecutor<'a, Evm, Spec, R: ReceiptBuilder> {
262 pub inner: EthBlockExecutor<'a, Evm, Spec, R>,
264 pub arb_hooks: Option<DefaultArbOsHooks>,
266 pub arb_ctx: ArbBlockExecutionCtx,
268 pending_tx: Option<PendingArbTx>,
270 pub block_gas_left: u64,
273 user_txs_processed: u64,
276 pub gas_used_for_l1: Vec<u64>,
279 pub multi_gas_used: Vec<MultiGas>,
281 expected_balance_delta: i128,
284 zombie_accounts: rustc_hash::FxHashSet<Address>,
287 finalise_deleted: rustc_hash::FxHashSet<Address>,
290 touched_accounts: rustc_hash::FxHashSet<Address>,
293 multi_gas_current_fees: std::sync::OnceLock<[U256; NUM_RESOURCE_KIND]>,
300}
301
302impl<'a, Evm, Spec, R: ReceiptBuilder> ArbBlockExecutor<'a, Evm, Spec, R> {
303 pub fn with_hooks(mut self, hooks: DefaultArbOsHooks) -> Self {
305 self.arb_hooks = Some(hooks);
306 self
307 }
308
309 pub fn with_arb_ctx(mut self, ctx: ArbBlockExecutionCtx) -> Self {
311 self.arb_ctx = ctx;
312 self
313 }
314
315 pub fn zombie_accounts(&self) -> rustc_hash::FxHashSet<Address> {
321 self.zombie_accounts.clone()
322 }
323
324 pub fn finalise_deleted(&self) -> &rustc_hash::FxHashSet<Address> {
327 &self.finalise_deleted
328 }
329
330 pub fn deduct_failed_tx_gas(&mut self, is_user_tx: bool) {
335 const TX_GAS: u64 = 21_000;
336 self.block_gas_left = self.block_gas_left.saturating_sub(TX_GAS);
337 if is_user_tx {
338 self.user_txs_processed += 1;
339 }
340 }
341
342 pub fn drain_scheduled_txs(&mut self) -> Vec<Vec<u8>> {
346 self.arb_hooks
347 .as_mut()
348 .map(|hooks| std::mem::take(&mut hooks.tx_proc.scheduled_txs))
349 .unwrap_or_default()
350 }
351
352 fn load_state_params<D: Database>(
355 &mut self,
356 arb_state: &ArbosState<D, impl arbos::burn::Burner>,
357 ) {
358 let arbos_version = arb_state.arbos_version();
359 self.arb_ctx.arbos_version = arbos_version;
360 arb_precompiles::set_arbos_version(arbos_version);
362 arb_precompiles::set_block_timestamp(self.arb_ctx.block_timestamp);
363 arb_precompiles::set_current_l2_block(self.arb_ctx.l2_block_number);
364 arb_precompiles::set_l1_block_number_for_evm(self.arb_ctx.l1_block_number);
365 arb_precompiles::set_cached_l1_block_number(
366 self.arb_ctx.l2_block_number,
367 self.arb_ctx.l1_block_number,
368 );
369
370 if let Ok(backlog) = arb_state.l2_pricing_state.gas_backlog() {
372 arb_precompiles::set_current_gas_backlog(backlog);
373 }
374
375 if let Ok(addr) = arb_state.network_fee_account() {
376 self.arb_ctx.network_fee_account = addr;
377 }
378 if let Ok(addr) = arb_state.infra_fee_account() {
379 self.arb_ctx.infra_fee_account = addr;
380 }
381 if let Ok(level) = arb_state.brotli_compression_level() {
382 self.arb_ctx.brotli_compression_level = level;
383 }
384 if let Ok(price) = arb_state.l1_pricing_state.price_per_unit() {
385 self.arb_ctx.l1_price_per_unit = price;
386 }
387 if let Ok(min_fee) = arb_state.l2_pricing_state.min_base_fee_wei() {
388 self.arb_ctx.min_base_fee = min_fee;
389 }
390
391 let per_block_gas_limit = arb_state
392 .l2_pricing_state
393 .per_block_gas_limit()
394 .unwrap_or(0);
395 let per_tx_gas_limit = arb_state.l2_pricing_state.per_tx_gas_limit().unwrap_or(0);
396
397 let calldata_pricing_increase_enabled = arbos_version
399 >= arb_chainspec::arbos_version::ARBOS_VERSION_40
400 && arb_state
401 .features
402 .is_increased_calldata_price_enabled()
403 .unwrap_or(false);
404
405 let collect_tips_enabled = arb_state.collect_tips().unwrap_or(false);
407
408 let hooks = DefaultArbOsHooks::new(
409 self.arb_ctx.coinbase,
410 arbos_version,
411 self.arb_ctx.network_fee_account,
412 self.arb_ctx.infra_fee_account,
413 self.arb_ctx.min_base_fee,
414 per_block_gas_limit,
415 per_tx_gas_limit,
416 false,
417 self.arb_ctx.l1_base_fee,
418 calldata_pricing_increase_enabled,
419 collect_tips_enabled,
420 );
421 self.arb_hooks = Some(hooks);
422 }
423}
424
425impl<'db, DB, E, Spec, R> ArbBlockExecutor<'_, E, Spec, R>
426where
427 DB: Database + 'db,
428 E: Evm<
429 DB = &'db mut State<DB>,
430 Tx: FromRecoveredTx<R::Transaction> + FromTxWithEncoded<R::Transaction> + ArbTransactionEnv,
431 >,
432 Spec: EthExecutorSpec,
433 R: ReceiptBuilder<
434 Transaction: Transaction + Encodable2718 + ArbTransactionExt,
435 Receipt: TxReceipt<Log = Log>,
436 >,
437 R::Transaction: TransactionEnvelope,
438{
439 fn execute_submit_retryable(
443 &mut self,
444 ticket_id: alloy_primitives::B256,
445 tx_type: <R::Transaction as TransactionEnvelope>::TxType,
446 mut info: arb_primitives::SubmitRetryableInfo,
447 ) -> Result<
448 EthTxResult<E::HaltReason, <R::Transaction as TransactionEnvelope>::TxType>,
449 BlockExecutionError,
450 > {
451 let sender = info.from;
452
453 let is_filtered = {
458 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
459 let state_ptr: *mut State<DB> = db as *mut State<DB>;
460 if let Ok(arb_state) = ArbosState::open(state_ptr, SystemBurner::new(None, false)) {
461 if arb_state.filtered_transactions.is_filtered_free(ticket_id) {
462 if let Ok(recipient) = arb_state.filtered_funds_recipient_or_default() {
463 info.fee_refund_addr = recipient;
464 info.beneficiary = recipient;
465 }
466 true
467 } else {
468 false
469 }
470 } else {
471 false
472 }
473 };
474
475 let block = self.inner.evm().block();
477 let current_time = revm::context::Block::timestamp(block).to::<u64>();
478 let effective_base_fee = self.arb_ctx.basefee;
479
480 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
481
482 mint_balance(db, sender, info.deposit_value);
484 self.touched_accounts.insert(sender);
485
486 let dep_i128: i128 = info.deposit_value.try_into().unwrap_or(i128::MAX);
488 self.expected_balance_delta = self.expected_balance_delta.saturating_add(dep_i128);
489
490 let _ = db.load_cache_account(sender);
492 let balance_after_mint = db
493 .cache
494 .accounts
495 .get(&sender)
496 .and_then(|a| a.account.as_ref())
497 .map(|a| a.info.balance)
498 .unwrap_or(U256::ZERO);
499
500 let params = SubmitRetryableParams {
501 ticket_id,
502 from: sender,
503 fee_refund_addr: info.fee_refund_addr,
504 deposit_value: info.deposit_value,
505 retry_value: info.retry_value,
506 gas_fee_cap: info.gas_fee_cap,
507 gas: info.gas,
508 max_submission_fee: info.max_submission_fee,
509 retry_data_len: info.retry_data.len(),
510 l1_base_fee: info.l1_base_fee,
511 effective_base_fee,
512 current_time,
513 balance_after_mint,
514 infra_fee_account: self.arb_ctx.infra_fee_account,
515 min_base_fee: self.arb_ctx.min_base_fee,
516 arbos_version: self.arb_ctx.arbos_version,
517 };
518
519 let fees = compute_submit_retryable_fees(¶ms);
520
521 let user_gas = info.gas;
522
523 if let Some(ref err) = fees.error {
527 tracing::warn!(
528 target: "arb::executor",
529 ticket_id = %ticket_id,
530 error = %err,
531 "submit retryable fee validation failed"
532 );
533
534 self.pending_tx = Some(PendingArbTx {
535 sender,
536 tx_gas_limit: user_gas,
537 arb_tx_type: Some(ArbTxType::ArbitrumSubmitRetryableTx),
538 poster_gas: 0,
539 evm_gas_used: 0,
540
541 charged_multi_gas: MultiGas::default(),
542 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
543 stylus_data_fee: U256::ZERO,
544 retry_context: None,
545 coinbase_tip_per_gas: 0,
546 capped_gas_price: false,
547 actual_gas_price: self.arb_ctx.basefee,
548 });
549
550 return Ok(EthTxResult {
551 result: revm::context::result::ResultAndState {
552 result: ExecutionResult::Revert {
553 gas_used: 0,
554 output: alloy_primitives::Bytes::new(),
555 },
556 state: Default::default(),
557 },
558 blob_gas_used: 0,
559 tx_type,
560 });
561 }
562
563 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
564
565 if !fees.submission_fee.is_zero() {
567 transfer_balance(
568 db,
569 sender,
570 self.arb_ctx.network_fee_account,
571 fees.submission_fee,
572 );
573 self.touched_accounts.insert(sender);
574 self.touched_accounts
575 .insert(self.arb_ctx.network_fee_account);
576 }
577
578 transfer_balance(db, sender, info.fee_refund_addr, fees.submission_fee_refund);
580 self.touched_accounts.insert(sender);
581 self.touched_accounts.insert(info.fee_refund_addr);
582
583 if !try_transfer_balance(db, sender, fees.escrow, info.retry_value) {
587 self.touched_accounts.insert(sender);
588 self.touched_accounts.insert(fees.escrow);
589 transfer_balance(
591 db,
592 self.arb_ctx.network_fee_account,
593 sender,
594 fees.submission_fee,
595 );
596 self.touched_accounts
597 .insert(self.arb_ctx.network_fee_account);
598 transfer_balance(
600 db,
601 sender,
602 info.fee_refund_addr,
603 fees.withheld_submission_fee,
604 );
605 self.touched_accounts.insert(info.fee_refund_addr);
606
607 self.pending_tx = Some(PendingArbTx {
608 sender,
609 tx_gas_limit: user_gas,
610 arb_tx_type: Some(ArbTxType::ArbitrumSubmitRetryableTx),
611 poster_gas: 0,
612 evm_gas_used: 0,
613
614 charged_multi_gas: MultiGas::default(),
615 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
616 stylus_data_fee: U256::ZERO,
617 retry_context: None,
618 coinbase_tip_per_gas: 0,
619 capped_gas_price: false,
620 actual_gas_price: self.arb_ctx.basefee,
621 });
622
623 return Ok(EthTxResult {
624 result: revm::context::result::ResultAndState {
625 result: ExecutionResult::Revert {
626 gas_used: 0,
627 output: alloy_primitives::Bytes::new(),
628 },
629 state: Default::default(),
630 },
631 blob_gas_used: 0,
632 tx_type,
633 });
634 }
635 self.touched_accounts.insert(sender);
636 self.touched_accounts.insert(fees.escrow);
637
638 let state_ptr: *mut State<DB> = db as *mut State<DB>;
640 if let Ok(arb_state) = ArbosState::open(state_ptr, SystemBurner::new(None, false)) {
641 let _ = arb_state.retryable_state.create_retryable(
642 ticket_id,
643 fees.timeout,
644 sender,
645 info.retry_to,
646 info.retry_value,
647 info.beneficiary,
648 &info.retry_data,
649 );
650 }
651
652 let mut receipt_logs: Vec<Log> = Vec::new();
654 receipt_logs.push(Log {
655 address: arb_precompiles::ARBRETRYABLETX_ADDRESS,
656 data: alloy_primitives::LogData::new_unchecked(
657 vec![arb_precompiles::ticket_created_topic(), ticket_id],
658 alloy_primitives::Bytes::new(),
659 ),
660 });
661
662 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
663
664 if fees.can_pay_for_gas {
666 if self.arb_ctx.infra_fee_account != Address::ZERO {
668 transfer_balance(db, sender, self.arb_ctx.infra_fee_account, fees.infra_cost);
669 self.touched_accounts.insert(sender);
670 self.touched_accounts.insert(self.arb_ctx.infra_fee_account);
671 }
672 if !fees.network_cost.is_zero() {
674 transfer_balance(
675 db,
676 sender,
677 self.arb_ctx.network_fee_account,
678 fees.network_cost,
679 );
680 self.touched_accounts.insert(sender);
681 self.touched_accounts
682 .insert(self.arb_ctx.network_fee_account);
683 }
684 transfer_balance(db, sender, info.fee_refund_addr, fees.gas_price_refund);
686 self.touched_accounts.insert(sender);
687 self.touched_accounts.insert(info.fee_refund_addr);
688
689 if !is_filtered {
691 let state_ptr2: *mut State<DB> = db as *mut State<DB>;
694 match ArbosState::open(state_ptr2, SystemBurner::new(None, false)) {
695 Ok(arb_state) => {
696 match arb_state.retryable_state.open_retryable(
697 ticket_id, 0, ) {
699 Ok(Some(retryable)) => {
700 let _ = retryable.increment_num_tries();
701
702 match retryable.make_tx(
703 U256::from(self.arb_ctx.chain_id),
704 0, effective_base_fee,
706 user_gas,
707 ticket_id,
708 info.fee_refund_addr,
709 fees.available_refund,
710 fees.submission_fee,
711 ) {
712 Ok(retry_tx) => {
713 let retry_tx_hash = {
715 let mut enc = Vec::new();
716 enc.push(ArbTxType::ArbitrumRetryTx.as_u8());
717 alloy_rlp::Encodable::encode(&retry_tx, &mut enc);
718 keccak256(&enc)
719 };
720
721 let mut event_data = Vec::with_capacity(128);
723 event_data.extend_from_slice(
724 &B256::left_padding_from(&user_gas.to_be_bytes()).0,
725 );
726 event_data.extend_from_slice(
727 &B256::left_padding_from(
728 info.fee_refund_addr.as_slice(),
729 )
730 .0,
731 );
732 event_data.extend_from_slice(
733 &fees.available_refund.to_be_bytes::<32>(),
734 );
735 event_data.extend_from_slice(
736 &fees.submission_fee.to_be_bytes::<32>(),
737 );
738
739 receipt_logs.push(Log {
740 address: arb_precompiles::ARBRETRYABLETX_ADDRESS,
741 data: alloy_primitives::LogData::new_unchecked(
742 vec![
743 arb_precompiles::redeem_scheduled_topic(),
744 ticket_id,
745 retry_tx_hash,
746 B256::left_padding_from(&0u64.to_be_bytes()),
747 ],
748 event_data.into(),
749 ),
750 });
751
752 if let Some(hooks) = self.arb_hooks.as_mut() {
753 let mut encoded = Vec::new();
754 encoded.push(ArbTxType::ArbitrumRetryTx.as_u8());
755 alloy_rlp::Encodable::encode(&retry_tx, &mut encoded);
756 hooks.tx_proc.scheduled_txs.push(encoded);
757 } else {
758 tracing::warn!(
759 target: "arb::executor",
760 "Cannot schedule auto-redeem: arb_hooks is None"
761 );
762 }
763 }
764 Err(_) => {
765 tracing::warn!(
766 target: "arb::executor",
767 "Auto-redeem make_tx failed"
768 );
769 }
770 }
771 }
772 Ok(None) => {
773 tracing::warn!(
774 target: "arb::executor",
775 %ticket_id,
776 "open_retryable returned None after create"
777 );
778 }
779 Err(_) => {
780 tracing::warn!(
781 target: "arb::executor",
782 "open_retryable failed"
783 );
784 }
785 }
786 }
787 Err(_) => {
788 tracing::warn!(
789 target: "arb::executor",
790 "ArbosState::open failed for auto-redeem"
791 );
792 }
793 }
794 }
795 } else if !fees.gas_cost_refund.is_zero() {
796 transfer_balance(db, sender, info.fee_refund_addr, fees.gas_cost_refund);
798 self.touched_accounts.insert(sender);
799 self.touched_accounts.insert(info.fee_refund_addr);
800 }
801
802 let gas_used = if fees.can_pay_for_gas { user_gas } else { 0 };
808 self.pending_tx = Some(PendingArbTx {
809 sender,
810 tx_gas_limit: user_gas,
811 arb_tx_type: Some(ArbTxType::ArbitrumSubmitRetryableTx),
812 poster_gas: 0,
813 evm_gas_used: gas_used,
814 charged_multi_gas: if fees.can_pay_for_gas {
815 MultiGas::l2_calldata_gas(user_gas)
816 } else {
817 MultiGas::default()
818 },
819 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
820 stylus_data_fee: U256::ZERO,
821 retry_context: None,
822 coinbase_tip_per_gas: 0,
823 capped_gas_price: false,
824 actual_gas_price: self.arb_ctx.basefee,
825 });
826
827 let ticket_bytes = alloy_primitives::Bytes::copy_from_slice(ticket_id.as_slice());
831
832 if is_filtered {
833 Ok(EthTxResult {
834 result: revm::context::result::ResultAndState {
835 result: ExecutionResult::Revert {
836 gas_used,
837 output: ticket_bytes,
838 },
839 state: Default::default(),
840 },
841 blob_gas_used: 0,
842 tx_type,
843 })
844 } else {
845 Ok(EthTxResult {
846 result: revm::context::result::ResultAndState {
847 result: ExecutionResult::Success {
848 reason: revm::context::result::SuccessReason::Return,
849 gas_used,
850 gas_refunded: 0,
851 output: revm::context::result::Output::Call(ticket_bytes),
852 logs: receipt_logs,
853 },
854 state: Default::default(),
855 },
856 blob_gas_used: 0,
857 tx_type,
858 })
859 }
860 }
861}
862
863impl<'db, DB, E, Spec, R> BlockExecutor for ArbBlockExecutor<'_, E, Spec, R>
864where
865 DB: Database + 'db,
866 E: Evm<
867 DB = &'db mut State<DB>,
868 Tx: FromRecoveredTx<R::Transaction> + FromTxWithEncoded<R::Transaction> + ArbTransactionEnv,
869 >,
870 Spec: EthExecutorSpec,
871 R: ReceiptBuilder<
872 Transaction: Transaction + Encodable2718 + ArbTransactionExt,
873 Receipt: TxReceipt<Log = Log> + arb_primitives::SetArbReceiptFields,
874 >,
875 R::Transaction: TransactionEnvelope,
876{
877 type Transaction = R::Transaction;
878 type Receipt = R::Receipt;
879 type Evm = E;
880 type Result = EthTxResult<E::HaltReason, <R::Transaction as TransactionEnvelope>::TxType>;
881
882 fn apply_pre_execution_changes(&mut self) -> Result<(), BlockExecutionError> {
883 self.inner.apply_pre_execution_changes()?;
884
885 {
887 let block = self.inner.evm().block();
888 let timestamp = revm::context::Block::timestamp(block).to::<u64>();
889 if self.arb_ctx.block_timestamp == 0 {
890 self.arb_ctx.block_timestamp = timestamp;
891 }
892 self.arb_ctx.coinbase = revm::context::Block::beneficiary(block);
893 self.arb_ctx.basefee = U256::from(revm::context::Block::basefee(block));
894 if let Some(prevrandao) = revm::context::Block::prevrandao(block) {
895 if self.arb_ctx.l1_block_number == 0 {
896 self.arb_ctx.l1_block_number =
897 crate::config::l1_block_number_from_mix_hash(&prevrandao);
898 }
899 }
900 }
901
902 if self.arb_ctx.l2_block_number > 0 {
907 arb_precompiles::set_current_l2_block(self.arb_ctx.l2_block_number);
908 arb_precompiles::set_cached_l1_block_number(
909 self.arb_ctx.l2_block_number,
910 self.arb_ctx.l1_block_number,
911 );
912 }
913
914 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
918 let state_ptr: *mut State<DB> = db as *mut State<DB>;
919
920 if let Ok(arb_state) = ArbosState::open(state_ptr, SystemBurner::new(None, false)) {
921 let _ = arb_state.l2_pricing_state.commit_multi_gas_fees();
923
924 if let Ok(base_fee) = arb_state.l2_pricing_state.base_fee_wei() {
927 self.arb_ctx.basefee = base_fee;
928 }
929
930 self.load_state_params(&arb_state);
932
933 self.block_gas_left = arb_state
935 .l2_pricing_state
936 .per_block_gas_limit()
937 .unwrap_or(0);
938
939 if let Ok(l1_block_number) = arb_state.blockhashes.l1_block_number() {
945 let lower = l1_block_number.saturating_sub(256);
946 let state_ref = unsafe { &mut *state_ptr };
948 for n in lower..l1_block_number {
949 if let Ok(Some(hash)) = arb_state.blockhashes.block_hash(n) {
950 state_ref.block_hashes.insert(n, hash);
951 }
952 }
953 }
954 }
955
956 tracing::trace!(
960 target: "arb::executor",
961 l1_block = self.arb_ctx.l1_block_number,
962 delayed_msgs = self.arb_ctx.delayed_messages_read,
963 chain_id = self.arb_ctx.chain_id,
964 basefee = %self.arb_ctx.basefee,
965 arbos_version = self.arb_ctx.arbos_version,
966 has_hooks = self.arb_hooks.is_some(),
967 "starting block execution"
968 );
969
970 Ok(())
971 }
972
973 fn execute_transaction_without_commit(
974 &mut self,
975 tx: impl ExecutableTx<Self>,
976 ) -> Result<Self::Result, BlockExecutionError> {
977 let (tx_env, recovered) = tx.into_parts();
979 let sender = *recovered.signer();
980 let tx_type_raw = recovered.tx().ty();
981 let tx_gas_limit = recovered.tx().gas_limit();
982 let tx_value = recovered.tx().value();
983 let envelope_tx_type = recovered.tx().tx_type();
984
985 let arb_tx_type = ArbTxType::from_u8(tx_type_raw).ok();
987 let is_arb_internal = arb_tx_type == Some(ArbTxType::ArbitrumInternalTx);
988 let is_arb_deposit = arb_tx_type == Some(ArbTxType::ArbitrumDepositTx);
989 let is_submit_retryable = arb_tx_type == Some(ArbTxType::ArbitrumSubmitRetryableTx);
990 let is_retry_tx = arb_tx_type == Some(ArbTxType::ArbitrumRetryTx);
991 let is_contract_tx = arb_tx_type == Some(ArbTxType::ArbitrumContractTx);
992 let has_poster_costs = tx_type_has_poster_costs(tx_type_raw);
993
994 let is_user_tx =
998 !is_arb_internal && !is_arb_deposit && !is_submit_retryable && !is_retry_tx;
999 const TX_GAS_MIN: u64 = 21_000;
1000 if is_user_tx && self.block_gas_left < TX_GAS_MIN {
1001 return Err(BlockExecutionError::msg("block gas limit reached"));
1002 }
1003
1004 crate::evm::reset_stylus_pages();
1006 arb_precompiles::set_poster_balance_correction(U256::ZERO);
1007 arb_precompiles::set_current_tx_sender(Address::ZERO);
1008 arb_precompiles::reset_caller_stack();
1009 crate::state_overlay::reset_tx();
1010 if let Some(hooks) = self.arb_hooks.as_mut() {
1011 hooks.tx_proc.poster_fee = U256::ZERO;
1012 hooks.tx_proc.poster_gas = 0;
1013 hooks.tx_proc.compute_hold_gas = 0;
1014 hooks.tx_proc.current_retryable = None;
1015 hooks.tx_proc.current_refund_to = None;
1016 hooks.tx_proc.scheduled_txs.clear();
1017 }
1018
1019 let actual_gas_price: U256 = {
1024 let base_fee = self.arb_ctx.basefee;
1025 let base_fee_u128: u128 = base_fee.try_into().unwrap_or(u128::MAX);
1026 let max_fee: u128 = revm::context_interface::Transaction::gas_price(&tx_env);
1027 let max_priority: u128 =
1028 revm::context_interface::Transaction::max_priority_fee_per_gas(&tx_env)
1029 .unwrap_or(0);
1030 let effective: u128 =
1031 std::cmp::min(max_fee, base_fee_u128.saturating_add(max_priority));
1032 let drop = self
1033 .arb_hooks
1034 .as_ref()
1035 .map(|h| h.drop_tip())
1036 .unwrap_or(false);
1037 if drop || effective == 0 {
1038 base_fee
1039 } else {
1040 U256::from(effective)
1041 }
1042 };
1043
1044 if is_arb_internal {
1048 use arbos::tx_processor::ARBOS_ADDRESS;
1049
1050 if sender != ARBOS_ADDRESS {
1051 return Err(BlockExecutionError::msg(
1052 "internal tx not from ArbOS address",
1053 ));
1054 }
1055
1056 let tx_data = recovered.tx().input().to_vec();
1057 let tx_type = recovered.tx().tx_type();
1058 let mut tx_err = None;
1059
1060 if tx_data.len() >= 4 {
1061 let selector: [u8; 4] = tx_data[0..4].try_into().unwrap();
1062 let is_start_block = selector == internal_tx::INTERNAL_TX_START_BLOCK_METHOD_ID;
1063
1064 if is_start_block {
1065 if let Ok(start_data) = internal_tx::decode_start_block_data(&tx_data) {
1066 self.arb_ctx.l1_base_fee = start_data.l1_base_fee;
1067 self.arb_ctx.time_passed = start_data.time_passed;
1068 }
1069 }
1070
1071 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1072 let state_ptr: *mut State<DB> = db as *mut State<DB>;
1073 if let Ok(mut arb_state) =
1074 ArbosState::open(state_ptr, SystemBurner::new(None, false))
1075 {
1076 let block = self.inner.evm().block();
1077 let current_time = revm::context::Block::timestamp(block).to::<u64>();
1078 let ctx = InternalTxContext {
1079 block_number: revm::context::Block::number(block).to::<u64>(),
1080 current_time,
1081 prev_hash: self.arb_ctx.parent_hash,
1082 };
1083
1084 if is_start_block
1088 && arb_state.arbos_version()
1089 >= arb_chainspec::arbos_version::ARBOS_VERSION_40
1090 {
1091 process_parent_block_hash(
1093 unsafe { &mut *state_ptr },
1094 self.arb_ctx.l2_block_number,
1095 ctx.prev_hash,
1096 );
1097 }
1098
1099 let touched_ptr =
1100 &mut self.touched_accounts as *mut rustc_hash::FxHashSet<Address>;
1101 let zombie_ptr =
1102 &mut self.zombie_accounts as *mut rustc_hash::FxHashSet<Address>;
1103 let finalise_ptr =
1104 &self.finalise_deleted as *const rustc_hash::FxHashSet<Address>;
1105 let arbos_ver = self.arb_ctx.arbos_version;
1106 let mut do_transfer = |from: Address, to: Address, amount: U256| {
1107 unsafe {
1109 if amount.is_zero()
1110 && arbos_ver < arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS
1111 {
1112 create_zombie_if_deleted(
1113 &mut *state_ptr,
1114 from,
1115 &*finalise_ptr,
1116 &mut *zombie_ptr,
1117 &mut *touched_ptr,
1118 );
1119 }
1120 transfer_balance(&mut *state_ptr, from, to, amount);
1121 if !amount.is_zero() {
1122 (*zombie_ptr).remove(&from);
1123 }
1124 (*zombie_ptr).remove(&to);
1125 (*touched_ptr).insert(from);
1126 (*touched_ptr).insert(to);
1127 }
1128 Ok(())
1129 };
1130 let mut do_balance = |addr: Address| -> U256 {
1131 unsafe { get_balance(&mut *state_ptr, addr) }
1133 };
1134 if let Err(e) = internal_tx::apply_internal_tx_update(
1135 &tx_data,
1136 &mut arb_state,
1137 &ctx,
1138 &mut do_transfer,
1139 &mut do_balance,
1140 ) {
1141 tracing::warn!(
1142 target: "arb::executor",
1143 error = %e,
1144 "internal tx processing failed"
1145 );
1146 tx_err = Some(e);
1147 }
1148
1149 if is_start_block {
1150 self.load_state_params(&arb_state);
1151
1152 if let Ok(l1_block_number) = arb_state.blockhashes.l1_block_number() {
1156 self.arb_ctx.l1_block_number = l1_block_number;
1157 arb_precompiles::set_l1_block_number_for_evm(l1_block_number);
1158 arb_precompiles::set_cached_l1_block_number(
1159 self.arb_ctx.l2_block_number,
1160 l1_block_number,
1161 );
1162
1163 let lower = l1_block_number.saturating_sub(256);
1165 let state_ref = unsafe { &mut *state_ptr };
1166 for n in lower..l1_block_number {
1167 if let Ok(Some(hash)) = arb_state.blockhashes.block_hash(n) {
1168 state_ref.block_hashes.insert(n, hash);
1169 }
1170 }
1171 }
1172 }
1173 }
1174 }
1175
1176 self.pending_tx = Some(PendingArbTx {
1178 sender,
1179 tx_gas_limit: 0,
1180 arb_tx_type: Some(ArbTxType::ArbitrumInternalTx),
1181 poster_gas: 0,
1182 evm_gas_used: 0,
1183
1184 charged_multi_gas: MultiGas::default(),
1185 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
1186 stylus_data_fee: U256::ZERO,
1187 retry_context: None,
1188 coinbase_tip_per_gas: 0,
1189 capped_gas_price: false,
1190 actual_gas_price: self.arb_ctx.basefee,
1191 });
1192
1193 if let Some(err) = tx_err {
1195 return Err(BlockExecutionError::msg(format!(
1196 "failed to apply internal transaction: {err}"
1197 )));
1198 }
1199
1200 return Ok(EthTxResult {
1201 result: revm::context::result::ResultAndState {
1202 result: ExecutionResult::Success {
1203 reason: revm::context::result::SuccessReason::Return,
1204 gas_used: 0,
1205 gas_refunded: 0,
1206 output: revm::context::result::Output::Call(alloy_primitives::Bytes::new()),
1207 logs: Vec::new(),
1208 },
1209 state: Default::default(),
1210 },
1211 blob_gas_used: 0,
1212 tx_type,
1213 });
1214 }
1215
1216 if is_arb_deposit {
1219 let value = recovered.tx().value();
1220 let mut to = match recovered.tx().kind() {
1221 TxKind::Call(addr) => addr,
1222 TxKind::Create => {
1223 return Err(BlockExecutionError::msg("deposit tx has no To address"));
1224 }
1225 };
1226 let tx_type = recovered.tx().tx_type();
1227 let tx_hash = recovered.tx().trie_hash();
1228
1229 let mut is_filtered = false;
1233 {
1234 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1235 let state_ptr: *mut State<DB> = db as *mut State<DB>;
1236 if let Ok(arb_state) = ArbosState::open(state_ptr, SystemBurner::new(None, false)) {
1237 if arb_state.filtered_transactions.is_filtered_free(tx_hash) {
1238 if let Ok(recipient) = arb_state.filtered_funds_recipient_or_default() {
1239 to = recipient;
1240 }
1241 is_filtered = true;
1242 }
1243 }
1244 }
1245
1246 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1247 mint_balance(db, sender, value);
1249 transfer_balance(db, sender, to, value);
1250 self.touched_accounts.insert(sender);
1251 self.touched_accounts.insert(to);
1252
1253 let value_i128: i128 = value.try_into().unwrap_or(i128::MAX);
1255 self.expected_balance_delta = self.expected_balance_delta.saturating_add(value_i128);
1256
1257 self.pending_tx = Some(PendingArbTx {
1258 sender,
1259 tx_gas_limit: 0,
1260 arb_tx_type: Some(ArbTxType::ArbitrumDepositTx),
1261 poster_gas: 0,
1262 evm_gas_used: 0,
1263
1264 charged_multi_gas: MultiGas::default(),
1265 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
1266 stylus_data_fee: U256::ZERO,
1267 retry_context: None,
1268 coinbase_tip_per_gas: 0,
1269 capped_gas_price: false,
1270 actual_gas_price: self.arb_ctx.basefee,
1271 });
1272
1273 let result = if is_filtered {
1277 ExecutionResult::Revert {
1278 gas_used: 0,
1279 output: alloy_primitives::Bytes::from("filtered transaction"),
1280 }
1281 } else {
1282 ExecutionResult::Success {
1283 reason: revm::context::result::SuccessReason::Return,
1284 gas_used: 0,
1285 gas_refunded: 0,
1286 output: revm::context::result::Output::Call(alloy_primitives::Bytes::new()),
1287 logs: Vec::new(),
1288 }
1289 };
1290
1291 return Ok(EthTxResult {
1292 result: revm::context::result::ResultAndState {
1293 result,
1294 state: Default::default(),
1295 },
1296 blob_gas_used: 0,
1297 tx_type,
1298 });
1299 }
1300
1301 if is_submit_retryable {
1303 if let Some(info) = recovered.tx().submit_retryable_info() {
1304 let ticket_id = recovered.tx().trie_hash();
1305 let tx_type = recovered.tx().tx_type();
1306 return self.execute_submit_retryable(ticket_id, tx_type, info);
1307 }
1308 }
1309
1310 let mut retry_pre_exec_undo: Option<(Address, U256, Address, U256)> = None;
1314 let mut retry_context = None;
1315 if is_retry_tx {
1316 if let Some(info) = recovered.tx().retry_tx_info() {
1317 let block = self.inner.evm().block();
1318 let current_time = revm::context::Block::timestamp(block).to::<u64>();
1319 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1320 let state_ptr: *mut State<DB> = db as *mut State<DB>;
1321
1322 if let Ok(arb_state) = ArbosState::open(state_ptr, SystemBurner::new(None, false)) {
1324 let retryable = arb_state
1325 .retryable_state
1326 .open_retryable(info.ticket_id, current_time);
1327
1328 match retryable {
1329 Ok(Some(_)) => {
1330 let escrow = retryables::retryable_escrow_address(info.ticket_id);
1332 let value = recovered.tx().value();
1333
1334 if value.is_zero()
1337 && self.arb_ctx.arbos_version
1338 < arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS
1339 {
1340 create_zombie_if_deleted(
1341 db,
1342 escrow,
1343 &self.finalise_deleted,
1344 &mut self.zombie_accounts,
1345 &mut self.touched_accounts,
1346 );
1347 }
1348
1349 if !try_transfer_balance(db, escrow, sender, value) {
1350 let tx_type = recovered.tx().tx_type();
1352 self.pending_tx = Some(PendingArbTx {
1353 sender,
1354 tx_gas_limit: 0,
1355 arb_tx_type: Some(ArbTxType::ArbitrumRetryTx),
1356 poster_gas: 0,
1357 evm_gas_used: 0,
1358
1359 charged_multi_gas: MultiGas::default(),
1360 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
1361 stylus_data_fee: U256::ZERO,
1362 retry_context: None,
1363 coinbase_tip_per_gas: 0,
1364 capped_gas_price: false,
1365 actual_gas_price: self.arb_ctx.basefee,
1366 });
1367 return Ok(EthTxResult {
1368 result: revm::context::result::ResultAndState {
1369 result: ExecutionResult::Revert {
1370 gas_used: 0,
1371 output: alloy_primitives::Bytes::new(),
1372 },
1373 state: Default::default(),
1374 },
1375 blob_gas_used: 0,
1376 tx_type,
1377 });
1378 }
1379
1380 if !value.is_zero() {
1382 self.zombie_accounts.remove(&escrow);
1383 }
1384 self.zombie_accounts.remove(&sender);
1385 self.touched_accounts.insert(escrow);
1386 self.touched_accounts.insert(sender);
1387
1388 let prepaid = self
1390 .arb_ctx
1391 .basefee
1392 .saturating_mul(U256::from(tx_gas_limit));
1393 mint_balance(db, sender, prepaid);
1394 retry_pre_exec_undo = Some((sender, prepaid, escrow, value));
1395
1396 if let Some(hooks) = self.arb_hooks.as_mut() {
1398 hooks
1399 .tx_proc
1400 .prepare_retry_tx(info.ticket_id, info.refund_to);
1401 }
1402
1403 retry_context = Some(PendingRetryContext {
1404 ticket_id: info.ticket_id,
1405 refund_to: info.refund_to,
1406 gas_fee_cap: info.gas_fee_cap,
1407 max_refund: info.max_refund,
1408 submission_fee_refund: info.submission_fee_refund,
1409 call_value: recovered.tx().value(),
1410 });
1411 }
1412 Ok(None) => {
1413 let tx_type = recovered.tx().tx_type();
1415 self.pending_tx = Some(PendingArbTx {
1416 sender,
1417 tx_gas_limit: 0,
1418 arb_tx_type: Some(ArbTxType::ArbitrumRetryTx),
1419 poster_gas: 0,
1420 evm_gas_used: 0,
1421
1422 charged_multi_gas: MultiGas::default(),
1423 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
1424 stylus_data_fee: U256::ZERO,
1425 retry_context: None,
1426 coinbase_tip_per_gas: 0,
1427 capped_gas_price: false,
1428 actual_gas_price: self.arb_ctx.basefee,
1429 });
1430 let err_msg = format!("retryable ticket {} not found", info.ticket_id,);
1431 return Ok(EthTxResult {
1432 result: revm::context::result::ResultAndState {
1433 result: ExecutionResult::Revert {
1434 gas_used: 0,
1435 output: alloy_primitives::Bytes::from(err_msg.into_bytes()),
1436 },
1437 state: Default::default(),
1438 },
1439 blob_gas_used: 0,
1440 tx_type,
1441 });
1442 }
1443 Err(_) => {
1444 let tx_type = recovered.tx().tx_type();
1446 self.pending_tx = Some(PendingArbTx {
1447 sender,
1448 tx_gas_limit: 0,
1449 arb_tx_type: Some(ArbTxType::ArbitrumRetryTx),
1450 poster_gas: 0,
1451 evm_gas_used: 0,
1452
1453 charged_multi_gas: MultiGas::default(),
1454 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
1455 stylus_data_fee: U256::ZERO,
1456 retry_context: None,
1457 coinbase_tip_per_gas: 0,
1458 capped_gas_price: false,
1459 actual_gas_price: self.arb_ctx.basefee,
1460 });
1461 return Ok(EthTxResult {
1462 result: revm::context::result::ResultAndState {
1463 result: ExecutionResult::Revert {
1464 gas_used: 0,
1465 output: alloy_primitives::Bytes::from(
1466 format!("error opening retryable {}", info.ticket_id,)
1467 .into_bytes(),
1468 ),
1469 },
1470 state: Default::default(),
1471 },
1472 blob_gas_used: 0,
1473 tx_type,
1474 });
1475 }
1476 }
1477 }
1478 }
1479 }
1480
1481 let mut poster_gas = 0u64;
1484 let mut compute_hold_gas = 0u64;
1485 let calldata_units: u64 = if has_poster_costs {
1486 let level = self.arb_ctx.brotli_compression_level;
1487 let coinbase = self.arb_ctx.coinbase;
1488 let tx_ref = recovered.tx();
1489 let units = if coinbase == l1_pricing::BATCH_POSTER_ADDRESS {
1490 let tx_bytes_ref = tx_ref;
1491 tx_ref.poster_units_for(level, &mut || {
1492 l1_pricing::poster_units_from_bytes(&tx_bytes_ref.encoded_2718(), level)
1493 })
1494 } else {
1495 0
1496 };
1497 let poster_cost = self
1498 .arb_ctx
1499 .l1_price_per_unit
1500 .saturating_mul(U256::from(units));
1501
1502 if let Some(hooks) = self.arb_hooks.as_mut() {
1503 hooks.tx_proc.poster_gas = compute_poster_gas(
1504 poster_cost,
1505 actual_gas_price,
1506 false,
1507 self.arb_ctx.min_base_fee,
1508 );
1509 hooks.tx_proc.poster_fee =
1510 actual_gas_price.saturating_mul(U256::from(hooks.tx_proc.poster_gas));
1511 poster_gas = hooks.tx_proc.poster_gas;
1512 }
1513
1514 units
1515 } else {
1516 0
1517 };
1518
1519 if let Some(hooks) = self.arb_hooks.as_mut() {
1524 if !hooks.is_eth_call {
1525 let spec = arb_chainspec::spec_id_by_arbos_version(self.arb_ctx.arbos_version);
1526 let intrinsic_estimate = estimate_intrinsic_gas(recovered.tx(), spec);
1527 let gas_after_intrinsic = tx_gas_limit.saturating_sub(intrinsic_estimate);
1528 let gas_after_poster = gas_after_intrinsic.saturating_sub(poster_gas);
1529
1530 let max_compute =
1531 if hooks.arbos_version < arb_chainspec::arbos_version::ARBOS_VERSION_50 {
1532 hooks.per_block_gas_limit
1533 } else {
1534 hooks.per_tx_gas_limit.saturating_sub(intrinsic_estimate)
1535 };
1536
1537 if max_compute > 0 && gas_after_poster > max_compute {
1538 compute_hold_gas = gas_after_poster - max_compute;
1539 hooks.tx_proc.compute_hold_gas = compute_hold_gas;
1540 }
1541 }
1542 }
1543
1544 if is_user_tx
1549 && self.arb_ctx.arbos_version < arb_chainspec::arbos_version::ARBOS_VERSION_50
1550 && self.user_txs_processed > 0
1551 {
1552 const TX_GAS: u64 = 21_000;
1553 let compute_gas = tx_gas_limit.saturating_sub(poster_gas).max(TX_GAS);
1554 if compute_gas > self.block_gas_left {
1555 return Err(BlockExecutionError::msg("block gas limit reached"));
1556 }
1557 }
1558
1559 let tx_hash_for_filter = recovered.tx().trie_hash();
1563 let is_filtered = {
1564 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1565 let state_ptr: *mut State<DB> = db as *mut State<DB>;
1566 match ArbosState::open(state_ptr, SystemBurner::new(None, false)) {
1567 Ok(arb_state) => {
1568 if calldata_units > 0 {
1569 let _ = arb_state
1570 .l1_pricing_state
1571 .add_to_units_since_update(calldata_units);
1572 }
1573 arb_state
1574 .filtered_transactions
1575 .is_filtered_free(tx_hash_for_filter)
1576 }
1577 Err(_) => false,
1578 }
1579 };
1580
1581 let mut tx_env = tx_env;
1588 let gas_deduction = poster_gas.saturating_add(compute_hold_gas);
1589 if gas_deduction > 0 {
1590 let evm_gas_limit_before = revm::context_interface::Transaction::gas_limit(&tx_env);
1591 tx_env.set_gas_limit(evm_gas_limit_before.saturating_sub(gas_deduction));
1592 }
1593
1594 {
1599 let correction = self
1600 .arb_ctx
1601 .basefee
1602 .saturating_mul(U256::from(poster_gas.saturating_add(compute_hold_gas)));
1603 arb_precompiles::set_poster_balance_correction(correction);
1604 arb_precompiles::set_current_tx_sender(sender);
1605 }
1606
1607 {
1610 use arbos::tx_processor::RevertedTxAction;
1611
1612 let tx_hash = tx_hash_for_filter;
1613
1614 if let Some(hooks) = self.arb_hooks.as_ref() {
1615 let action = hooks.tx_proc.reverted_tx_hook(
1616 Some(tx_hash),
1617 None, is_filtered,
1619 );
1620
1621 match action {
1622 RevertedTxAction::PreRecordedRevert { gas_to_consume } => {
1623 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1624 increment_nonce(db, sender);
1625 self.touched_accounts.insert(sender);
1626 let gas_used = poster_gas + gas_to_consume;
1627 let charged_multi_gas = MultiGas::single_dim_gas(poster_gas)
1628 .saturating_add(MultiGas::computation_gas(gas_to_consume));
1629 self.pending_tx = Some(PendingArbTx {
1630 sender,
1631 tx_gas_limit,
1632 arb_tx_type,
1633 poster_gas,
1634 evm_gas_used: 0,
1635 charged_multi_gas,
1636 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
1637 stylus_data_fee: U256::ZERO,
1638 retry_context,
1639 coinbase_tip_per_gas: 0,
1640 capped_gas_price: false,
1641 actual_gas_price: self.arb_ctx.basefee,
1642 });
1643 return Ok(EthTxResult {
1644 result: revm::context::result::ResultAndState {
1645 result: ExecutionResult::Revert {
1646 gas_used,
1647 output: alloy_primitives::Bytes::new(),
1648 },
1649 state: Default::default(),
1650 },
1651 blob_gas_used: 0,
1652 tx_type: envelope_tx_type,
1653 });
1654 }
1655 RevertedTxAction::FilteredTx => {
1656 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1657 increment_nonce(db, sender);
1658 self.touched_accounts.insert(sender);
1659 let gas_remaining = tx_gas_limit
1661 .saturating_sub(poster_gas)
1662 .saturating_sub(compute_hold_gas);
1663 let gas_used = tx_gas_limit;
1664 let charged_multi_gas = MultiGas::single_dim_gas(poster_gas)
1665 .saturating_add(MultiGas::computation_gas(gas_remaining));
1666 self.pending_tx = Some(PendingArbTx {
1667 sender,
1668 tx_gas_limit,
1669 arb_tx_type,
1670 poster_gas,
1671 evm_gas_used: 0,
1672 charged_multi_gas,
1673 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
1674 stylus_data_fee: U256::ZERO,
1675 retry_context,
1676 coinbase_tip_per_gas: 0,
1677 capped_gas_price: false,
1678 actual_gas_price: self.arb_ctx.basefee,
1679 });
1680 return Ok(EthTxResult {
1681 result: revm::context::result::ResultAndState {
1682 result: ExecutionResult::Revert {
1683 gas_used,
1684 output: alloy_primitives::Bytes::from(
1685 "filtered transaction".as_bytes(),
1686 ),
1687 },
1688 state: Default::default(),
1689 },
1690 blob_gas_used: 0,
1691 tx_type: envelope_tx_type,
1692 });
1693 }
1694 RevertedTxAction::None => {}
1695 }
1696 }
1697 }
1698
1699 let upfront_gas_price: u128 = revm::context_interface::Transaction::gas_price(&tx_env);
1705
1706 let effective_tip_per_gas: u128 = {
1710 let bf: u128 = self.arb_ctx.basefee.try_into().unwrap_or(u128::MAX);
1711 let max_fee: u128 = upfront_gas_price; let max_priority: u128 =
1713 revm::context_interface::Transaction::max_priority_fee_per_gas(&tx_env)
1714 .unwrap_or(0);
1715 let max_minus_bf = max_fee.saturating_sub(bf);
1716 max_priority.min(max_minus_bf)
1717 };
1718
1719 let should_drop_tip = self
1727 .arb_hooks
1728 .as_ref()
1729 .map(|h| h.drop_tip())
1730 .unwrap_or(false);
1731 if should_drop_tip {
1732 let base_fee: u128 = self.arb_ctx.basefee.try_into().unwrap_or(u128::MAX);
1733 if upfront_gas_price > base_fee {
1734 tx_env.set_gas_price(base_fee);
1735 tx_env.set_gas_priority_fee(Some(0));
1736 }
1737 }
1738
1739 arb_precompiles::set_tx_is_aliased(arbos::util::does_tx_type_alias(tx_type_raw));
1743
1744 {
1745 let poster_fee_val = self
1746 .arb_hooks
1747 .as_ref()
1748 .map(|h| h.tx_proc.poster_fee)
1749 .unwrap_or(U256::ZERO);
1750 arb_precompiles::set_current_tx_poster_fee(
1751 poster_fee_val.try_into().unwrap_or(u128::MAX),
1752 );
1753 let retryable_id = retry_context
1754 .as_ref()
1755 .map(|ctx| ctx.ticket_id)
1756 .unwrap_or(B256::ZERO);
1757 arb_precompiles::set_current_retryable_id(retryable_id);
1758 let redeemer = retry_context
1759 .as_ref()
1760 .map(|ctx| ctx.refund_to)
1761 .unwrap_or(Address::ZERO);
1762 arb_precompiles::set_current_redeemer(redeemer);
1763 }
1764
1765 let retry_undo = retry_pre_exec_undo;
1766 let rollback_pre_exec_state = |this: &mut Self, units: u64| {
1767 arb_precompiles::clear_tx_scratch();
1768 let db: &mut State<DB> = this.inner.evm_mut().db_mut();
1769 if units > 0 {
1770 let state_ptr: *mut State<DB> = db as *mut State<DB>;
1771 if let Ok(arb_state) = ArbosState::open(state_ptr, SystemBurner::new(None, false)) {
1772 let _ = arb_state
1773 .l1_pricing_state
1774 .subtract_from_units_since_update(units);
1775 }
1776 }
1777 if let Some((retry_sender, prepaid, escrow, escrow_value)) = retry_undo {
1778 if !prepaid.is_zero() {
1779 burn_balance(db, retry_sender, prepaid);
1780 }
1781 if !escrow_value.is_zero() {
1782 let _ = try_transfer_balance(db, retry_sender, escrow, escrow_value);
1783 }
1784 }
1785 };
1786
1787 if is_user_tx {
1790 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1791 let account = db
1792 .load_cache_account(sender)
1793 .ok()
1794 .and_then(|a| a.account_info());
1795 let sender_balance = account.as_ref().map(|a| a.balance).unwrap_or(U256::ZERO);
1796 let sender_nonce = account.as_ref().map(|a| a.nonce).unwrap_or(0);
1797
1798 if !is_contract_tx {
1800 let tx_nonce = revm::context_interface::Transaction::nonce(&tx_env);
1801 if tx_nonce != sender_nonce {
1802 rollback_pre_exec_state(self, calldata_units);
1803 return Err(BlockExecutionError::msg(format!(
1804 "nonce mismatch: address {sender} tx nonce {tx_nonce} != state nonce {sender_nonce}"
1805 )));
1806 }
1807 }
1808
1809 let gas_cost = U256::from(tx_gas_limit) * U256::from(upfront_gas_price);
1810 let tx_value = revm::context_interface::Transaction::value(&tx_env);
1811 let total_cost = gas_cost.saturating_add(tx_value);
1812 if sender_balance < total_cost {
1813 rollback_pre_exec_state(self, calldata_units);
1814 return Err(BlockExecutionError::msg(format!(
1815 "insufficient funds: address {sender} have {sender_balance} want {total_cost}"
1816 )));
1817 }
1818 }
1819
1820 if is_retry_tx || is_contract_tx {
1826 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1827 let sender_nonce = db
1828 .load_cache_account(sender)
1829 .map(|a| a.account_info().map(|i| i.nonce).unwrap_or(0))
1830 .unwrap_or(0);
1831 tx_env.set_nonce(sender_nonce);
1832 }
1833
1834 {
1838 let to_addr = match recovered.tx().kind() {
1839 TxKind::Call(a) => Some(a),
1840 _ => None,
1841 };
1842 if to_addr == Some(arb_precompiles::ARBWASM_ADDRESS) && tx_value > U256::ZERO {
1843 tx_env.set_value(U256::ZERO);
1844 }
1845 }
1846
1847 let mut output = match self
1848 .inner
1849 .execute_transaction_without_commit((tx_env, recovered))
1850 {
1851 Ok(o) => o,
1852 Err(e) => {
1853 rollback_pre_exec_state(self, calldata_units);
1854 return Err(e);
1855 }
1856 };
1857
1858 let evm_gas_used = output.result.result.gas_used();
1861
1862 if poster_gas > 0 {
1869 adjust_result_gas_used(&mut output.result.result, poster_gas);
1870 }
1871
1872 let mut total_donated_gas = 0u64;
1879 let mut retry_tx_hash_fixes: Vec<(usize, B256)> = Vec::new();
1881 if let ExecutionResult::Success { ref logs, .. } = output.result.result {
1882 let redeem_topic = arb_precompiles::redeem_scheduled_topic();
1883 let precompile_addr = arb_precompiles::ARBRETRYABLETX_ADDRESS;
1884
1885 for (log_idx, log) in logs.iter().enumerate() {
1886 if log.address != precompile_addr {
1887 continue;
1888 }
1889 if log.topics().is_empty() || log.topics()[0] != redeem_topic {
1890 continue;
1891 }
1892 if log.topics().len() < 4 || log.data.data.len() < 128 {
1893 continue;
1894 }
1895
1896 let ticket_id = log.topics()[1];
1897 let seq_num_bytes = log.topics()[3];
1898 let nonce =
1899 u64::from_be_bytes(seq_num_bytes.0[24..32].try_into().unwrap_or([0u8; 8]));
1900 let data = &log.data.data;
1901 let donated_gas = U256::from_be_slice(&data[0..32]).to::<u64>();
1902 total_donated_gas = total_donated_gas.saturating_add(donated_gas);
1903 let gas_donor = Address::from_slice(&data[44..64]);
1904 let max_refund = U256::from_be_slice(&data[64..96]);
1905 let submission_fee_refund = U256::from_be_slice(&data[96..128]);
1906
1907 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1909 let state_ptr: *mut State<DB> = db as *mut State<DB>;
1910 let current_time = {
1911 let block = self.inner.evm().block();
1912 revm::context::Block::timestamp(block).to::<u64>()
1913 };
1914 if let Ok(arb_state) = ArbosState::open(state_ptr, SystemBurner::new(None, false)) {
1915 if let Ok(Some(retryable)) = arb_state
1916 .retryable_state
1917 .open_retryable(ticket_id, current_time)
1918 {
1919 let _ = retryable.increment_num_tries();
1920
1921 if let Ok(retry_tx) = retryable.make_tx(
1922 U256::from(self.arb_ctx.chain_id),
1923 nonce,
1924 self.arb_ctx.basefee,
1925 donated_gas,
1926 ticket_id,
1927 gas_donor,
1928 max_refund,
1929 submission_fee_refund,
1930 ) {
1931 let mut encoded = Vec::new();
1933 encoded.push(ArbTxType::ArbitrumRetryTx.as_u8());
1934 alloy_rlp::Encodable::encode(&retry_tx, &mut encoded);
1935 let correct_hash = keccak256(&encoded);
1936 retry_tx_hash_fixes.push((log_idx, correct_hash));
1937
1938 if let Some(hooks) = self.arb_hooks.as_mut() {
1939 hooks.tx_proc.scheduled_txs.push(encoded);
1940 }
1941 }
1942 }
1943
1944 let _ = arb_state
1946 .l2_pricing_state
1947 .shrink_backlog(donated_gas, MultiGas::default());
1948 if let Ok(b) = arb_state.l2_pricing_state.gas_backlog() {
1949 arb_precompiles::set_current_gas_backlog(b);
1950 }
1951 }
1952 }
1953 }
1954
1955 if !retry_tx_hash_fixes.is_empty() {
1959 if let ExecutionResult::Success { ref mut logs, .. } = output.result.result {
1960 for (log_idx, correct_hash) in &retry_tx_hash_fixes {
1961 if let Some(log) = logs.get_mut(*log_idx) {
1962 if log.data.topics().len() > 2 {
1963 let topics = log.data.topics_mut_unchecked();
1964 topics[2] = *correct_hash;
1965 }
1966 }
1967 }
1968 }
1969 }
1970
1971 let stylus_data_fee = if arb_precompiles::take_stylus_activation_request().is_some()
1976 || arb_precompiles::take_stylus_keepalive_request().is_some()
1977 {
1978 arb_precompiles::take_stylus_activation_data_fee()
1979 } else {
1980 U256::ZERO
1981 };
1982
1983 let pending_logs = arb_precompiles::take_pending_precompile_logs();
1985 if !pending_logs.is_empty() {
1986 if let ExecutionResult::Success { ref mut logs, .. } = output.result.result {
1987 for (address, topics, data) in pending_logs {
1988 logs.push(Log {
1989 address,
1990 data: alloy_primitives::LogData::new(topics, data.into())
1991 .unwrap_or_default(),
1992 });
1993 }
1994 }
1995 }
1996
1997 let charged_multi_gas = MultiGas::single_dim_gas(poster_gas)
1998 .saturating_add(MultiGas::computation_gas(evm_gas_used));
1999
2000 let coinbase_tip_per_gas: u128 = effective_tip_per_gas;
2004 let capped_gas_price = should_drop_tip;
2005
2006 self.pending_tx = Some(PendingArbTx {
2007 sender,
2008 tx_gas_limit,
2009 arb_tx_type,
2010 poster_gas,
2011 evm_gas_used,
2012 charged_multi_gas,
2013 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
2014 stylus_data_fee,
2015 retry_context,
2016 coinbase_tip_per_gas,
2017 capped_gas_price,
2018 actual_gas_price,
2019 });
2020
2021 Ok(output)
2022 }
2023
2024 fn commit_transaction(&mut self, output: Self::Result) -> Result<u64, BlockExecutionError> {
2025 let pending = self.pending_tx.take();
2027 let gas_used_total = output.result.result.gas_used();
2028 let success = matches!(&output.result.result, ExecutionResult::Success { .. });
2029
2030 let mut withdrawal_value = U256::ZERO;
2034 if let ExecutionResult::Success { ref logs, .. } = output.result.result {
2035 let arbsys_addr = arb_precompiles::ARBSYS_ADDRESS;
2036 let l2_to_l1_tx_topic = keccak256(
2037 b"L2ToL1Tx(address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes)",
2038 );
2039 for log in logs {
2040 if log.address == arbsys_addr
2041 && !log.data.topics().is_empty()
2042 && log.data.topics()[0] == l2_to_l1_tx_topic
2043 {
2044 if log.data.data.len() >= 160 {
2047 let callvalue = U256::from_be_slice(&log.data.data[128..160]);
2048 withdrawal_value = withdrawal_value.saturating_add(callvalue);
2049 let val_i128: i128 = callvalue.try_into().unwrap_or(i128::MAX);
2050 self.expected_balance_delta =
2051 self.expected_balance_delta.saturating_sub(val_i128);
2052 }
2053 }
2054 }
2055 }
2056
2057 for addr in output.result.state.keys() {
2059 self.touched_accounts.insert(*addr);
2060 }
2061
2062 let gas_used = self.inner.commit_transaction(output)?;
2064
2065 if let Some(ref p) = pending {
2070 if !p.capped_gas_price && p.coinbase_tip_per_gas > 0 && gas_used > 0 {
2071 let coinbase = self.arb_ctx.coinbase;
2072 let net_acct = self.arb_ctx.network_fee_account;
2073 let compute_gas = gas_used.saturating_sub(p.poster_gas);
2074 let tip_to_network =
2075 U256::from(p.coinbase_tip_per_gas).saturating_mul(U256::from(compute_gas));
2076 if coinbase != net_acct && !tip_to_network.is_zero() {
2077 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
2078 if get_balance(db, coinbase) >= tip_to_network {
2079 transfer_balance(db, coinbase, net_acct, tip_to_network);
2080 self.touched_accounts.insert(coinbase);
2081 self.touched_accounts.insert(net_acct);
2082 }
2083 }
2084 }
2085 }
2086
2087 if let Some(ref p) = pending {
2090 if !p.stylus_data_fee.is_zero() {
2091 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
2092 burn_balance(db, p.sender, p.stylus_data_fee);
2093 mint_balance(db, self.arb_ctx.network_fee_account, p.stylus_data_fee);
2094 self.touched_accounts.insert(p.sender);
2095 self.touched_accounts
2096 .insert(self.arb_ctx.network_fee_account);
2097 }
2098 }
2099
2100 if !withdrawal_value.is_zero() {
2102 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
2103 burn_balance(db, arb_precompiles::ARBSYS_ADDRESS, withdrawal_value);
2104 self.touched_accounts
2105 .insert(arb_precompiles::ARBSYS_ADDRESS);
2106 }
2107
2108 let poster_gas_for_receipt = pending.as_ref().map_or(0, |p| p.poster_gas);
2110 self.gas_used_for_l1.push(poster_gas_for_receipt);
2111 let multi_gas_for_receipt = pending
2112 .as_ref()
2113 .map_or(MultiGas::zero(), |p| p.charged_multi_gas);
2114 self.multi_gas_used.push(multi_gas_for_receipt);
2115
2116 if let Some(pending) = pending {
2118 let is_retry = pending.retry_context.is_some();
2119
2120 debug_assert!(
2122 gas_used_total <= pending.tx_gas_limit,
2123 "gas_used ({gas_used_total}) exceeds gas_limit ({})",
2124 pending.tx_gas_limit
2125 );
2126
2127 let sender_extra_gas = gas_used_total.saturating_sub(pending.evm_gas_used);
2132 if sender_extra_gas > 0 {
2133 let extra_cost = pending
2134 .actual_gas_price
2135 .saturating_mul(U256::from(sender_extra_gas));
2136 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
2137 burn_balance(db, pending.sender, extra_cost);
2138 self.touched_accounts.insert(pending.sender);
2139 }
2140
2141 if let Some(retry_ctx) = pending.retry_context {
2142 let gas_left = pending.tx_gas_limit.saturating_sub(gas_used_total);
2144
2145 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
2146 let state_ptr: *mut State<DB> = db as *mut State<DB>;
2147 let touched_ptr = &mut self.touched_accounts as *mut rustc_hash::FxHashSet<Address>;
2148 let zombie_ptr = &mut self.zombie_accounts as *mut rustc_hash::FxHashSet<Address>;
2149 let finalise_ptr = &self.finalise_deleted as *const rustc_hash::FxHashSet<Address>;
2150 let arbos_ver = self.arb_ctx.arbos_version;
2151
2152 let arb_state_retry =
2153 ArbosState::open(state_ptr, SystemBurner::new(None, false)).ok();
2154
2155 let multi_dimensional_cost = if self.arb_ctx.arbos_version
2157 >= arb_chainspec::arbos_version::ARBOS_VERSION_MULTI_GAS_CONSTRAINTS
2158 {
2159 arb_state_retry.as_ref().and_then(|s| {
2160 let cached = self.multi_gas_current_fees.get_or_init(|| {
2161 s.l2_pricing_state
2162 .get_current_multi_gas_fees()
2163 .unwrap_or([U256::ZERO; NUM_RESOURCE_KIND])
2164 });
2165 s.l2_pricing_state
2166 .multi_dimensional_price_for_refund_with_fees(
2167 pending.charged_multi_gas,
2168 cached,
2169 )
2170 .ok()
2171 })
2172 } else {
2173 None
2174 };
2175
2176 let result = self.arb_hooks.as_ref().map(|hooks| {
2177 hooks.tx_proc.end_tx_retryable(
2178 &EndTxRetryableParams {
2179 gas_left,
2180 gas_used: gas_used_total,
2181 effective_base_fee: self.arb_ctx.basefee,
2182 from: pending.sender,
2183 refund_to: retry_ctx.refund_to,
2184 max_refund: retry_ctx.max_refund,
2185 submission_fee_refund: retry_ctx.submission_fee_refund,
2186 ticket_id: retry_ctx.ticket_id,
2187 value: U256::ZERO, success,
2189 network_fee_account: self.arb_ctx.network_fee_account,
2190 infra_fee_account: self.arb_ctx.infra_fee_account,
2191 min_base_fee: self.arb_ctx.min_base_fee,
2192 arbos_version: self.arb_ctx.arbos_version,
2193 multi_dimensional_cost,
2194 block_base_fee: self.arb_ctx.basefee,
2195 },
2196 |addr, amount| unsafe {
2197 burn_balance(&mut *state_ptr, addr, amount);
2198 (*touched_ptr).insert(addr);
2199 },
2200 |from, to, amount| {
2201 unsafe {
2202 if amount.is_zero()
2203 && arbos_ver
2204 < arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS
2205 {
2206 create_zombie_if_deleted(
2207 &mut *state_ptr,
2208 from,
2209 &*finalise_ptr,
2210 &mut *zombie_ptr,
2211 &mut *touched_ptr,
2212 );
2213 }
2214 transfer_balance(&mut *state_ptr, from, to, amount);
2215 if !amount.is_zero() {
2218 (*zombie_ptr).remove(&from);
2219 }
2220 (*zombie_ptr).remove(&to);
2222 (*touched_ptr).insert(from);
2223 (*touched_ptr).insert(to);
2224 }
2225 Ok(())
2226 },
2227 )
2228 });
2229
2230 if let Some(ref result) = result {
2231 if result.should_delete_retryable {
2232 if let Some(arb_state) = arb_state_retry.as_ref() {
2233 let _ = arb_state.retryable_state.delete_retryable(
2234 retry_ctx.ticket_id,
2235 |from, to, amount| {
2236 unsafe {
2237 if amount.is_zero()
2238 && arbos_ver
2239 < arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS
2240 {
2241 create_zombie_if_deleted(
2242 &mut *state_ptr,
2243 from,
2244 &*finalise_ptr,
2245 &mut *zombie_ptr,
2246 &mut *touched_ptr,
2247 );
2248 }
2249 transfer_balance(&mut *state_ptr, from, to, amount);
2250 if !amount.is_zero() {
2251 (*zombie_ptr).remove(&from);
2252 }
2253 (*zombie_ptr).remove(&to);
2254 (*touched_ptr).insert(from);
2255 (*touched_ptr).insert(to);
2256 }
2257 Ok(())
2258 },
2259 |addr| unsafe { get_balance(&mut *state_ptr, addr) },
2260 );
2261 }
2262 } else if result.should_return_value_to_escrow {
2263 unsafe {
2265 if retry_ctx.call_value.is_zero()
2266 && arbos_ver < arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS
2267 {
2268 create_zombie_if_deleted(
2269 &mut *state_ptr,
2270 pending.sender,
2271 &*finalise_ptr,
2272 &mut *zombie_ptr,
2273 &mut *touched_ptr,
2274 );
2275 }
2276 transfer_balance(
2277 &mut *state_ptr,
2278 pending.sender,
2279 result.escrow_address,
2280 retry_ctx.call_value,
2281 );
2282 if !retry_ctx.call_value.is_zero() {
2284 (*zombie_ptr).remove(&pending.sender);
2285 }
2286 (*zombie_ptr).remove(&result.escrow_address);
2288 (*touched_ptr).insert(pending.sender);
2289 (*touched_ptr).insert(result.escrow_address);
2290 }
2291 }
2292
2293 if let Some(arb_state) = arb_state_retry.as_ref() {
2296 let _ = arb_state.l2_pricing_state.grow_backlog(
2297 result.compute_gas_for_backlog,
2298 pending.charged_multi_gas,
2299 );
2300 if let Ok(b) = arb_state.l2_pricing_state.gas_backlog() {
2301 arb_precompiles::set_current_gas_backlog(b);
2302 }
2303 }
2304 }
2305 } else if matches!(
2306 pending.arb_tx_type,
2307 None | Some(ArbTxType::ArbitrumLegacyTx)
2308 | Some(ArbTxType::ArbitrumUnsignedTx)
2309 | Some(ArbTxType::ArbitrumContractTx)
2310 ) {
2311 let gas_left = pending.tx_gas_limit.saturating_sub(gas_used_total);
2315
2316 let fee_dist = self.arb_hooks.as_ref().map(|hooks| {
2317 hooks.compute_end_tx_fees(&EndTxContext {
2318 sender: pending.sender,
2319 gas_left,
2320 gas_used: gas_used_total,
2321 gas_price: self.arb_ctx.basefee,
2322 base_fee: self.arb_ctx.basefee,
2323 tx_type: pending.arb_tx_type.unwrap_or(ArbTxType::ArbitrumLegacyTx),
2324 success,
2325 refund_to: pending.sender,
2326 })
2327 });
2328
2329 if let Some(ref dist) = fee_dist {
2330 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
2331 apply_fee_distribution(db, dist, None);
2332 if !dist.network_fee_amount.is_zero() {
2335 self.touched_accounts.insert(dist.network_fee_account);
2336 }
2337 self.touched_accounts.insert(dist.infra_fee_account);
2338 self.touched_accounts.insert(dist.poster_fee_destination);
2339
2340 let state_ptr: *mut State<DB> = db as *mut State<DB>;
2341 let arb_state_post =
2342 ArbosState::open(state_ptr, SystemBurner::new(None, false)).ok();
2343
2344 if self.arb_ctx.arbos_version
2347 >= arb_chainspec::arbos_version::ARBOS_VERSION_MULTI_GAS_CONSTRAINTS
2348 {
2349 if let Some(arb_state) = arb_state_post.as_ref() {
2350 let total_cost = self
2351 .arb_ctx
2352 .basefee
2353 .saturating_mul(U256::from(gas_used_total));
2354 let cached = self.multi_gas_current_fees.get_or_init(|| {
2355 arb_state
2356 .l2_pricing_state
2357 .get_current_multi_gas_fees()
2358 .unwrap_or([U256::ZERO; NUM_RESOURCE_KIND])
2359 });
2360 if let Ok(multi_cost) = arb_state
2361 .l2_pricing_state
2362 .multi_dimensional_price_for_refund_with_fees(
2363 pending.charged_multi_gas,
2364 cached,
2365 )
2366 {
2367 if total_cost > multi_cost {
2368 let refund_amount = total_cost.saturating_sub(multi_cost);
2369 transfer_balance(
2370 db,
2371 dist.network_fee_account,
2372 pending.sender,
2373 refund_amount,
2374 );
2375 self.touched_accounts.insert(dist.network_fee_account);
2376 self.touched_accounts.insert(pending.sender);
2377 }
2378 }
2379 }
2380 }
2381
2382 let used_multi_gas = pending
2386 .charged_multi_gas
2387 .saturating_sub(MultiGas::single_dim_gas(pending.poster_gas));
2388
2389 if let Some(arb_state) = arb_state_post.as_ref() {
2390 if pending.gas_price_positive {
2392 let _ = arb_state
2393 .l2_pricing_state
2394 .grow_backlog(dist.compute_gas_for_backlog, used_multi_gas);
2395 if let Ok(b) = arb_state.l2_pricing_state.gas_backlog() {
2396 arb_precompiles::set_current_gas_backlog(b);
2397 }
2398 }
2399 if !dist.l1_fees_to_add.is_zero() {
2400 let _ = arb_state
2401 .l1_pricing_state
2402 .add_to_l1_fees_available(dist.l1_fees_to_add);
2403 }
2404 } else {
2405 tracing::error!(
2406 target: "arb::backlog",
2407 "NormalTx: ArbosState::open FAILED for grow_backlog"
2408 );
2409 }
2410 }
2411 }
2412
2413 let mut adjusted_gas_used = gas_used_total;
2417 if self.arb_ctx.arbos_version
2418 >= arb_chainspec::arbos_version::ARBOS_VERSION_FIX_REDEEM_GAS
2419 {
2420 if let Some(hooks) = self.arb_hooks.as_ref() {
2421 for scheduled in &hooks.tx_proc.scheduled_txs {
2422 if let Some(retry_gas) = decode_retry_tx_gas(scheduled) {
2423 adjusted_gas_used = adjusted_gas_used.saturating_sub(retry_gas);
2424 }
2425 }
2426 }
2427 }
2428
2429 const TX_GAS: u64 = 21_000;
2431 let data_gas = pending.poster_gas;
2432 let compute_used = if adjusted_gas_used < data_gas {
2433 TX_GAS
2434 } else {
2435 let compute = adjusted_gas_used - data_gas;
2436 if compute < TX_GAS {
2437 TX_GAS
2438 } else {
2439 compute
2440 }
2441 };
2442 self.block_gas_left = self.block_gas_left.saturating_sub(compute_used);
2443
2444 let is_user_tx = !matches!(
2446 pending.arb_tx_type,
2447 Some(ArbTxType::ArbitrumInternalTx)
2448 | Some(ArbTxType::ArbitrumDepositTx)
2449 | Some(ArbTxType::ArbitrumSubmitRetryableTx)
2450 | Some(ArbTxType::ArbitrumRetryTx)
2451 );
2452 if is_user_tx {
2453 self.user_txs_processed += 1;
2454 }
2455
2456 let _ = is_retry; }
2458
2459 arb_precompiles::clear_tx_scratch();
2460
2461 {
2471 let keccak_empty = alloy_primitives::B256::from(alloy_primitives::keccak256([]));
2472 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
2473 let to_remove: Vec<Address> = self
2474 .touched_accounts
2475 .drain()
2476 .filter(|addr| {
2477 if self.zombie_accounts.contains(addr) {
2479 return false;
2480 }
2481 if let Some(cached) = db.cache.accounts.get(addr) {
2482 if let Some(ref acct) = cached.account {
2483 let is_empty = acct.info.nonce == 0
2484 && acct.info.balance.is_zero()
2485 && acct.info.code_hash == keccak_empty;
2486 return is_empty;
2487 }
2488 }
2489 false
2490 })
2491 .collect();
2492
2493 for addr in &to_remove {
2501 crate::state_overlay::record_pre_touch(db, *addr);
2502 if let Some(cached) = db.cache.accounts.get_mut(addr) {
2503 cached.account = None;
2504 }
2505 }
2506 self.finalise_deleted.extend(to_remove);
2507 }
2508
2509 {
2510 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
2511 crate::state_overlay::drain_and_apply(db);
2512 }
2513
2514 Ok(gas_used)
2515 }
2516
2517 fn finish(self) -> Result<(Self::Evm, BlockExecutionResult<R::Receipt>), BlockExecutionError> {
2518 if self.expected_balance_delta != 0 {
2520 tracing::trace!(
2521 target: "arb::executor",
2522 delta = self.expected_balance_delta,
2523 "expected balance delta from deposits/withdrawals"
2524 );
2525 }
2526 let mut result = BlockExecutionResult {
2530 receipts: self.inner.receipts,
2531 requests: Default::default(),
2532 gas_used: self.inner.gas_used,
2533 blob_gas_used: self.inner.blob_gas_used,
2534 };
2535 for (i, receipt) in result.receipts.iter_mut().enumerate() {
2537 if let Some(&l1_gas) = self.gas_used_for_l1.get(i) {
2538 arb_primitives::SetArbReceiptFields::set_gas_used_for_l1(receipt, l1_gas);
2539 }
2540 if let Some(&multi_gas) = self.multi_gas_used.get(i) {
2541 arb_primitives::SetArbReceiptFields::set_multi_gas_used(receipt, multi_gas);
2542 }
2543 }
2544 Ok((self.inner.evm, result))
2545 }
2546
2547 fn set_state_hook(&mut self, hook: Option<Box<dyn OnStateHook>>) {
2548 self.inner.set_state_hook(hook);
2549 }
2550
2551 fn evm_mut(&mut self) -> &mut Self::Evm {
2552 self.inner.evm_mut()
2553 }
2554
2555 fn evm(&self) -> &Self::Evm {
2556 self.inner.evm()
2557 }
2558
2559 fn receipts(&self) -> &[Self::Receipt] {
2560 self.inner.receipts()
2561 }
2562}
2563
2564fn adjust_result_gas_used<H>(result: &mut ExecutionResult<H>, extra_gas: u64) {
2573 match result {
2574 ExecutionResult::Success { gas_used, .. } => *gas_used = gas_used.saturating_add(extra_gas),
2575 ExecutionResult::Revert { gas_used, .. } => *gas_used = gas_used.saturating_add(extra_gas),
2576 ExecutionResult::Halt { gas_used, .. } => *gas_used = gas_used.saturating_add(extra_gas),
2577 }
2578}
2579
2580fn mint_balance<DB: Database>(state: &mut State<DB>, address: Address, amount: U256) {
2582 if amount.is_zero() {
2583 return;
2584 }
2585 crate::state_overlay::record_pre_touch(state, address);
2586 if let Some(cache_acct) = state.cache.accounts.get_mut(&address) {
2587 if let Some(ref mut acct) = cache_acct.account {
2588 acct.info.balance = acct.info.balance.saturating_add(amount);
2589 } else {
2590 cache_acct.account = Some(revm_database::states::plain_account::PlainAccount {
2591 info: revm_state::AccountInfo {
2592 balance: amount,
2593 ..Default::default()
2594 },
2595 storage: Default::default(),
2596 });
2597 }
2598 }
2599}
2600
2601fn burn_balance<DB: Database>(state: &mut State<DB>, address: Address, amount: U256) {
2603 if amount.is_zero() {
2604 return;
2605 }
2606 crate::state_overlay::record_pre_touch(state, address);
2607 if let Some(cache_acct) = state.cache.accounts.get_mut(&address) {
2608 if let Some(ref mut acct) = cache_acct.account {
2609 acct.info.balance = acct.info.balance.saturating_sub(amount);
2610 }
2611 }
2612}
2613
2614fn increment_nonce<DB: Database>(state: &mut State<DB>, address: Address) {
2616 crate::state_overlay::record_pre_touch(state, address);
2617 if let Some(cache_acct) = state.cache.accounts.get_mut(&address) {
2618 if let Some(ref mut acct) = cache_acct.account {
2619 acct.info.nonce += 1;
2620 }
2621 }
2622}
2623
2624fn get_balance<DB: Database>(state: &mut State<DB>, address: Address) -> U256 {
2626 match revm::Database::basic(state, address) {
2627 Ok(Some(info)) => info.balance,
2628 _ => U256::ZERO,
2629 }
2630}
2631
2632fn transfer_balance<DB: Database>(state: &mut State<DB>, from: Address, to: Address, amount: U256) {
2638 if amount.is_zero() {
2639 return;
2640 }
2641 let balance = get_balance(state, from);
2645 if balance < amount {
2646 tracing::warn!(
2647 target: "arb::executor",
2648 %from, %to, %amount, %balance,
2649 "transfer_balance: insufficient funds, skipping"
2650 );
2651 return;
2652 }
2653 burn_balance(state, from, amount);
2654 mint_balance(state, to, amount);
2655}
2656
2657fn create_zombie_if_deleted<DB: Database>(
2663 state: &mut State<DB>,
2664 addr: Address,
2665 finalise_deleted: &rustc_hash::FxHashSet<Address>,
2666 zombie_accounts: &mut rustc_hash::FxHashSet<Address>,
2667 touched_accounts: &mut rustc_hash::FxHashSet<Address>,
2668) {
2669 crate::state_overlay::record_pre_touch(state, addr);
2670 let account_missing = state
2671 .cache
2672 .accounts
2673 .get(&addr)
2674 .is_none_or(|c| c.account.is_none());
2675 if account_missing && finalise_deleted.contains(&addr) {
2676 if let Some(cached) = state.cache.accounts.get_mut(&addr) {
2677 cached.account = Some(revm_database::states::plain_account::PlainAccount {
2678 info: revm_state::AccountInfo::default(),
2679 storage: Default::default(),
2680 });
2681 cached.status = revm_database::AccountStatus::InMemoryChange;
2682 }
2683 zombie_accounts.insert(addr);
2684 touched_accounts.insert(addr);
2685 }
2686}
2687
2688fn try_transfer_balance<DB: Database>(
2692 state: &mut State<DB>,
2693 from: Address,
2694 to: Address,
2695 amount: U256,
2696) -> bool {
2697 if amount.is_zero() {
2698 return true;
2699 }
2700 if get_balance(state, from) < amount {
2701 return false;
2702 }
2703 burn_balance(state, from, amount);
2704 mint_balance(state, to, amount);
2705 true
2706}
2707
2708fn apply_fee_distribution<DB: Database>(
2710 state: &mut State<DB>,
2711 dist: &EndTxFeeDistribution,
2712 l1_pricing: Option<&l1_pricing::L1PricingState<DB>>,
2713) {
2714 if !dist.network_fee_amount.is_zero() {
2717 mint_balance(state, dist.network_fee_account, dist.network_fee_amount);
2718 }
2719 mint_balance(state, dist.infra_fee_account, dist.infra_fee_amount);
2720 mint_balance(state, dist.poster_fee_destination, dist.poster_fee_amount);
2721
2722 if !dist.l1_fees_to_add.is_zero() {
2723 if let Some(l1_state) = l1_pricing {
2724 let _ = l1_state.add_to_l1_fees_available(dist.l1_fees_to_add);
2725 }
2726 }
2727
2728 tracing::trace!(
2729 target: "arb::executor",
2730 network_fee = %dist.network_fee_amount,
2731 infra_fee = %dist.infra_fee_amount,
2732 poster_fee = %dist.poster_fee_amount,
2733 poster_dest = %dist.poster_fee_destination,
2734 l1_fees_added = %dist.l1_fees_to_add,
2735 backlog_gas = dist.compute_gas_for_backlog,
2736 "applied fee distribution"
2737 );
2738}
2739
2740fn estimate_intrinsic_gas(tx: &impl Transaction, spec: revm::primitives::hardfork::SpecId) -> u64 {
2746 const TX_GAS: u64 = 21_000;
2747 const TX_CREATE_GAS: u64 = 32_000;
2748 const TX_DATA_ZERO_GAS: u64 = 4;
2749 const TX_DATA_NON_ZERO_GAS: u64 = 16;
2750 const TX_ACCESS_LIST_ADDRESS_GAS: u64 = 2400;
2751 const TX_ACCESS_LIST_STORAGE_KEY_GAS: u64 = 1900;
2752 const INIT_CODE_WORD_GAS: u64 = 2;
2753
2754 let is_create = tx.to().is_none();
2755
2756 let mut gas = TX_GAS;
2757 if is_create {
2758 gas += TX_CREATE_GAS;
2759 }
2760
2761 let data = tx.input();
2762
2763 let data_gas: u64 = data
2765 .iter()
2766 .map(|&b| {
2767 if b == 0 {
2768 TX_DATA_ZERO_GAS
2769 } else {
2770 TX_DATA_NON_ZERO_GAS
2771 }
2772 })
2773 .sum();
2774 gas = gas.saturating_add(data_gas);
2775
2776 if let Some(access_list) = tx.access_list() {
2778 for item in access_list.iter() {
2779 gas = gas.saturating_add(TX_ACCESS_LIST_ADDRESS_GAS);
2780 gas = gas.saturating_add(
2781 (item.storage_keys.len() as u64).saturating_mul(TX_ACCESS_LIST_STORAGE_KEY_GAS),
2782 );
2783 }
2784 }
2785
2786 if spec.is_enabled_in(revm::primitives::hardfork::SpecId::SHANGHAI)
2788 && is_create
2789 && !data.is_empty()
2790 {
2791 let words = (data.len() as u64).div_ceil(32);
2792 gas = gas.saturating_add(words.saturating_mul(INIT_CODE_WORD_GAS));
2793 }
2794
2795 gas
2796}
2797
2798fn decode_extra_fields(extra_bytes: &[u8]) -> (u64, u64) {
2801 let delayed = if extra_bytes.len() >= 40 {
2802 let mut buf = [0u8; 8];
2803 buf.copy_from_slice(&extra_bytes[32..40]);
2804 u64::from_be_bytes(buf)
2805 } else {
2806 0
2807 };
2808 let l2_block = if extra_bytes.len() >= 48 {
2809 let mut buf = [0u8; 8];
2810 buf.copy_from_slice(&extra_bytes[40..48]);
2811 u64::from_be_bytes(buf)
2812 } else {
2813 0
2814 };
2815 (delayed, l2_block)
2816}
2817
2818fn process_parent_block_hash<DB: Database>(
2822 state: &mut State<DB>,
2823 l2_block_number: u64,
2824 prev_hash: B256,
2825) {
2826 use arb_primitives::arbos_versions::HISTORY_STORAGE_ADDRESS;
2827
2828 const HISTORY_SERVE_WINDOW: u64 = 393168;
2830
2831 if l2_block_number == 0 {
2832 return;
2833 }
2834
2835 let slot = U256::from((l2_block_number - 1) % HISTORY_SERVE_WINDOW);
2836 let value = U256::from_be_slice(prev_hash.as_slice());
2837
2838 arb_storage::write_storage_at(state, HISTORY_STORAGE_ADDRESS, slot, value);
2839}
2840
2841fn decode_retry_tx_gas(encoded: &[u8]) -> Option<u64> {
2845 if encoded.is_empty() {
2846 return None;
2847 }
2848 if encoded[0] != ArbTxType::ArbitrumRetryTx.as_u8() {
2849 tracing::warn!(
2850 target: "arb::executor",
2851 tx_type = encoded[0],
2852 "unexpected scheduled tx type"
2853 );
2854 return None;
2855 }
2856 let rlp_data = &encoded[1..];
2857 let retry =
2858 <arb_alloy_consensus::tx::ArbRetryTx as alloy_rlp::Decodable>::decode(&mut &rlp_data[..])
2859 .ok()?;
2860 Some(retry.gas)
2861}