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::{multigas::MultiGas, signed_tx::ArbTransactionExt, tx_types::ArbTxType};
18use arbos::{
19 arbos_state::ArbosState,
20 burn::SystemBurner,
21 internal_tx::{self, InternalTxContext},
22 l1_pricing, retryables,
23 tx_processor::{
24 compute_poster_gas, compute_submit_retryable_fees, EndTxFeeDistribution,
25 EndTxRetryableParams, SubmitRetryableParams,
26 },
27 util::tx_type_has_poster_costs,
28};
29use reth_evm::TransactionEnv;
30use revm::{
31 context::{result::ExecutionResult, TxEnv},
32 database::State,
33 inspector::Inspector,
34};
35
36use crate::{
37 context::ArbBlockExecutionCtx,
38 executor::DefaultArbOsHooks,
39 hooks::{ArbOsHooks, EndTxContext},
40};
41
42pub trait ArbTransactionEnv: TransactionEnv {
47 fn set_gas_price(&mut self, gas_price: u128);
49 fn set_gas_priority_fee(&mut self, fee: Option<u128>);
51}
52
53impl ArbTransactionEnv for TxEnv {
54 fn set_gas_price(&mut self, gas_price: u128) {
55 self.gas_price = gas_price;
56 }
57 fn set_gas_priority_fee(&mut self, fee: Option<u128>) {
58 self.gas_priority_fee = fee;
59 }
60}
61
62pub trait ArbScheduledTxDrain {
68 fn drain_scheduled_txs(&mut self) -> Vec<Vec<u8>>;
71}
72
73impl<'a, Evm, Spec, R: ReceiptBuilder> ArbScheduledTxDrain for ArbBlockExecutor<'a, Evm, Spec, R> {
74 fn drain_scheduled_txs(&mut self) -> Vec<Vec<u8>> {
75 self.arb_hooks
76 .as_mut()
77 .map(|hooks| std::mem::take(&mut hooks.tx_proc.scheduled_txs))
78 .unwrap_or_default()
79 }
80}
81
82#[derive(Debug, Clone)]
87pub struct ArbBlockExecutorFactory<R, Spec, EvmF> {
88 receipt_builder: R,
89 spec: Spec,
90 evm_factory: EvmF,
91}
92
93impl<R, Spec, EvmF> ArbBlockExecutorFactory<R, Spec, EvmF> {
94 pub fn new(receipt_builder: R, spec: Spec, evm_factory: EvmF) -> Self {
95 Self {
96 receipt_builder,
97 spec,
98 evm_factory,
99 }
100 }
101
102 pub fn create_arb_executor<'a, DB, I>(
107 &'a self,
108 evm: EvmF::Evm<&'a mut State<DB>, I>,
109 ctx: EthBlockExecutionCtx<'a>,
110 chain_id: u64,
111 ) -> ArbBlockExecutor<'a, EvmF::Evm<&'a mut State<DB>, I>, &'a Spec, &'a R>
112 where
113 DB: Database + 'a,
114 R: ReceiptBuilder,
115 Spec: EthExecutorSpec + Clone,
116 I: Inspector<EvmF::Context<&'a mut State<DB>>> + 'a,
117 EvmF: EvmFactory,
118 {
119 let extra_bytes = ctx.extra_data.as_ref();
120 let (delayed_messages_read, l2_block_number) = decode_extra_fields(extra_bytes);
121 let arb_ctx = ArbBlockExecutionCtx {
122 parent_hash: ctx.parent_hash,
123 parent_beacon_block_root: ctx.parent_beacon_block_root,
124 extra_data: extra_bytes[..core::cmp::min(extra_bytes.len(), 32)].to_vec(),
125 delayed_messages_read,
126 l2_block_number,
127 chain_id,
128 ..Default::default()
129 };
130 ArbBlockExecutor {
131 inner: EthBlockExecutor::new(evm, ctx, &self.spec, &self.receipt_builder),
132 arb_hooks: None,
133 arb_ctx,
134 pending_tx: None,
135 block_gas_left: 0,
136 user_txs_processed: 0,
137 gas_used_for_l1: Vec::new(),
138 multi_gas_used: Vec::new(),
139 expected_balance_delta: 0,
140 zombie_accounts: std::collections::HashSet::new(),
141 finalise_deleted: std::collections::HashSet::new(),
142 touched_accounts: std::collections::HashSet::new(),
143 }
144 }
145}
146
147impl<R, Spec, EvmF> BlockExecutorFactory for ArbBlockExecutorFactory<R, Spec, EvmF>
148where
149 R: ReceiptBuilder<
150 Transaction: Transaction + Encodable2718 + ArbTransactionExt,
151 Receipt: TxReceipt<Log = Log> + arb_primitives::SetArbReceiptFields,
152 > + 'static,
153 Spec: EthExecutorSpec + Clone + 'static,
154 EvmF: EvmFactory<
155 Tx: FromRecoveredTx<R::Transaction> + FromTxWithEncoded<R::Transaction> + ArbTransactionEnv,
156 >,
157 Self: 'static,
158{
159 type EvmFactory = EvmF;
160 type ExecutionCtx<'a> = EthBlockExecutionCtx<'a>;
161 type Transaction = R::Transaction;
162 type Receipt = R::Receipt;
163
164 fn evm_factory(&self) -> &Self::EvmFactory {
165 &self.evm_factory
166 }
167
168 fn create_executor<'a, DB, I>(
169 &'a self,
170 evm: EvmF::Evm<&'a mut State<DB>, I>,
171 ctx: Self::ExecutionCtx<'a>,
172 ) -> impl BlockExecutorFor<'a, Self, DB, I>
173 where
174 DB: Database + 'a,
175 I: Inspector<EvmF::Context<&'a mut State<DB>>> + 'a,
176 {
177 let extra_bytes = ctx.extra_data.as_ref();
178 let (delayed_messages_read, l2_block_number) = decode_extra_fields(extra_bytes);
179 let arb_ctx = ArbBlockExecutionCtx {
180 parent_hash: ctx.parent_hash,
181 parent_beacon_block_root: ctx.parent_beacon_block_root,
182 extra_data: extra_bytes[..core::cmp::min(extra_bytes.len(), 32)].to_vec(),
183 delayed_messages_read,
184 l2_block_number,
185 ..Default::default()
186 };
187 ArbBlockExecutor {
188 inner: EthBlockExecutor::new(evm, ctx, &self.spec, &self.receipt_builder),
189 arb_hooks: None,
190 arb_ctx,
191 pending_tx: None,
192 block_gas_left: 0, user_txs_processed: 0,
194 gas_used_for_l1: Vec::new(),
195 multi_gas_used: Vec::new(),
196 expected_balance_delta: 0,
197 zombie_accounts: std::collections::HashSet::new(),
198 finalise_deleted: std::collections::HashSet::new(),
199 touched_accounts: std::collections::HashSet::new(),
200 }
201 }
202}
203
204struct PendingArbTx {
210 sender: Address,
211 tx_gas_limit: u64,
212 arb_tx_type: Option<ArbTxType>,
213 has_poster_costs: bool,
214 poster_gas: u64,
215 evm_gas_used: u64,
220 charged_multi_gas: MultiGas,
222 gas_price_positive: bool,
225 retry_context: Option<PendingRetryContext>,
227}
228
229struct PendingRetryContext {
231 ticket_id: alloy_primitives::B256,
232 refund_to: Address,
233 #[allow(dead_code)]
234 gas_fee_cap: U256,
235 max_refund: U256,
236 submission_fee_refund: U256,
237 call_value: U256,
239}
240
241pub struct ArbBlockExecutor<'a, Evm, Spec, R: ReceiptBuilder> {
249 pub inner: EthBlockExecutor<'a, Evm, Spec, R>,
251 pub arb_hooks: Option<DefaultArbOsHooks>,
253 pub arb_ctx: ArbBlockExecutionCtx,
255 pending_tx: Option<PendingArbTx>,
257 pub block_gas_left: u64,
260 user_txs_processed: u64,
263 pub gas_used_for_l1: Vec<u64>,
266 pub multi_gas_used: Vec<MultiGas>,
268 expected_balance_delta: i128,
271 zombie_accounts: std::collections::HashSet<Address>,
274 finalise_deleted: std::collections::HashSet<Address>,
277 touched_accounts: std::collections::HashSet<Address>,
280}
281
282impl<'a, Evm, Spec, R: ReceiptBuilder> ArbBlockExecutor<'a, Evm, Spec, R> {
283 pub fn with_hooks(mut self, hooks: DefaultArbOsHooks) -> Self {
285 self.arb_hooks = Some(hooks);
286 self
287 }
288
289 pub fn with_arb_ctx(mut self, ctx: ArbBlockExecutionCtx) -> Self {
291 self.arb_ctx = ctx;
292 self
293 }
294
295 pub fn zombie_accounts(&self) -> std::collections::HashSet<Address> {
301 self.zombie_accounts.clone()
302 }
303
304 pub fn finalise_deleted(&self) -> &std::collections::HashSet<Address> {
307 &self.finalise_deleted
308 }
309
310 pub fn deduct_failed_tx_gas(&mut self, is_user_tx: bool) {
315 const TX_GAS: u64 = 21_000;
316 self.block_gas_left = self.block_gas_left.saturating_sub(TX_GAS);
317 if is_user_tx {
318 self.user_txs_processed += 1;
319 }
320 }
321
322 pub fn drain_scheduled_txs(&mut self) -> Vec<Vec<u8>> {
326 self.arb_hooks
327 .as_mut()
328 .map(|hooks| std::mem::take(&mut hooks.tx_proc.scheduled_txs))
329 .unwrap_or_default()
330 }
331
332 fn load_state_params<D: Database>(
335 &mut self,
336 arb_state: &ArbosState<D, impl arbos::burn::Burner>,
337 ) {
338 let arbos_version = arb_state.arbos_version();
339 self.arb_ctx.arbos_version = arbos_version;
340 arb_precompiles::set_arbos_version(arbos_version);
342 arb_precompiles::set_block_timestamp(self.arb_ctx.block_timestamp);
343 arb_precompiles::set_current_l2_block(self.arb_ctx.l2_block_number);
344 arb_precompiles::set_l1_block_number_for_evm(self.arb_ctx.l1_block_number);
345 arb_precompiles::set_cached_l1_block_number(
346 self.arb_ctx.l2_block_number,
347 self.arb_ctx.l1_block_number,
348 );
349
350 if let Ok(backlog) = arb_state.l2_pricing_state.gas_backlog() {
352 arb_precompiles::set_current_gas_backlog(backlog);
353 }
354
355 if let Ok(addr) = arb_state.network_fee_account() {
356 self.arb_ctx.network_fee_account = addr;
357 }
358 if let Ok(addr) = arb_state.infra_fee_account() {
359 self.arb_ctx.infra_fee_account = addr;
360 }
361 if let Ok(level) = arb_state.brotli_compression_level() {
362 self.arb_ctx.brotli_compression_level = level;
363 }
364 if let Ok(price) = arb_state.l1_pricing_state.price_per_unit() {
365 self.arb_ctx.l1_price_per_unit = price;
366 }
367 if let Ok(min_fee) = arb_state.l2_pricing_state.min_base_fee_wei() {
368 self.arb_ctx.min_base_fee = min_fee;
369 }
370
371 let per_block_gas_limit = arb_state
372 .l2_pricing_state
373 .per_block_gas_limit()
374 .unwrap_or(0);
375 let per_tx_gas_limit = arb_state.l2_pricing_state.per_tx_gas_limit().unwrap_or(0);
376
377 let calldata_pricing_increase_enabled = arbos_version
379 >= arb_chainspec::arbos_version::ARBOS_VERSION_40
380 && arb_state
381 .features
382 .is_increased_calldata_price_enabled()
383 .unwrap_or(false);
384
385 let hooks = DefaultArbOsHooks::new(
386 self.arb_ctx.coinbase,
387 arbos_version,
388 self.arb_ctx.network_fee_account,
389 self.arb_ctx.infra_fee_account,
390 self.arb_ctx.min_base_fee,
391 per_block_gas_limit,
392 per_tx_gas_limit,
393 false,
394 self.arb_ctx.l1_base_fee,
395 calldata_pricing_increase_enabled,
396 );
397 self.arb_hooks = Some(hooks);
398 }
399}
400
401impl<'db, DB, E, Spec, R> ArbBlockExecutor<'_, E, Spec, R>
402where
403 DB: Database + 'db,
404 E: Evm<
405 DB = &'db mut State<DB>,
406 Tx: FromRecoveredTx<R::Transaction> + FromTxWithEncoded<R::Transaction> + ArbTransactionEnv,
407 >,
408 Spec: EthExecutorSpec,
409 R: ReceiptBuilder<
410 Transaction: Transaction + Encodable2718 + ArbTransactionExt,
411 Receipt: TxReceipt<Log = Log>,
412 >,
413 R::Transaction: TransactionEnvelope,
414{
415 fn execute_submit_retryable(
419 &mut self,
420 ticket_id: alloy_primitives::B256,
421 tx_type: <R::Transaction as TransactionEnvelope>::TxType,
422 mut info: arb_primitives::SubmitRetryableInfo,
423 ) -> Result<
424 EthTxResult<E::HaltReason, <R::Transaction as TransactionEnvelope>::TxType>,
425 BlockExecutionError,
426 > {
427 let sender = info.from;
428
429 let is_filtered = {
434 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
435 let state_ptr: *mut State<DB> = db as *mut State<DB>;
436 if let Ok(arb_state) = ArbosState::open(state_ptr, SystemBurner::new(None, false)) {
437 if arb_state.filtered_transactions.is_filtered_free(ticket_id) {
438 if let Ok(recipient) = arb_state.filtered_funds_recipient_or_default() {
439 info.fee_refund_addr = recipient;
440 info.beneficiary = recipient;
441 }
442 true
443 } else {
444 false
445 }
446 } else {
447 false
448 }
449 };
450
451 let block = self.inner.evm().block();
453 let current_time = revm::context::Block::timestamp(block).to::<u64>();
454 let effective_base_fee = self.arb_ctx.basefee;
455
456 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
457
458 mint_balance(db, sender, info.deposit_value);
460 self.touched_accounts.insert(sender);
461
462 let dep_i128: i128 = info.deposit_value.try_into().unwrap_or(i128::MAX);
464 self.expected_balance_delta = self.expected_balance_delta.saturating_add(dep_i128);
465
466 let _ = db.load_cache_account(sender);
468 let balance_after_mint = db
469 .cache
470 .accounts
471 .get(&sender)
472 .and_then(|a| a.account.as_ref())
473 .map(|a| a.info.balance)
474 .unwrap_or(U256::ZERO);
475
476 let params = SubmitRetryableParams {
477 ticket_id,
478 from: sender,
479 fee_refund_addr: info.fee_refund_addr,
480 deposit_value: info.deposit_value,
481 retry_value: info.retry_value,
482 gas_fee_cap: info.gas_fee_cap,
483 gas: info.gas,
484 max_submission_fee: info.max_submission_fee,
485 retry_data_len: info.retry_data.len(),
486 l1_base_fee: info.l1_base_fee,
487 effective_base_fee,
488 current_time,
489 balance_after_mint,
490 infra_fee_account: self.arb_ctx.infra_fee_account,
491 min_base_fee: self.arb_ctx.min_base_fee,
492 arbos_version: self.arb_ctx.arbos_version,
493 };
494
495 let fees = compute_submit_retryable_fees(¶ms);
496
497 tracing::debug!(
498 target: "arb::executor",
499 can_pay = fees.can_pay_for_gas,
500 has_error = fees.error.is_some(),
501 submission_fee = %fees.submission_fee,
502 "submit retryable fee computation"
503 );
504
505 let user_gas = info.gas;
506
507 if let Some(ref err) = fees.error {
511 tracing::warn!(
512 target: "arb::executor",
513 ticket_id = %ticket_id,
514 error = %err,
515 "submit retryable fee validation failed"
516 );
517
518 self.pending_tx = Some(PendingArbTx {
519 sender,
520 tx_gas_limit: user_gas,
521 arb_tx_type: Some(ArbTxType::ArbitrumSubmitRetryableTx),
522 has_poster_costs: false,
523 poster_gas: 0,
524 evm_gas_used: 0,
525
526 charged_multi_gas: MultiGas::default(),
527 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
528 retry_context: None,
529 });
530
531 return Ok(EthTxResult {
532 result: revm::context::result::ResultAndState {
533 result: ExecutionResult::Revert {
534 gas_used: 0,
535 output: alloy_primitives::Bytes::new(),
536 },
537 state: Default::default(),
538 },
539 blob_gas_used: 0,
540 tx_type,
541 });
542 }
543
544 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
545
546 tracing::debug!(
547 target: "arb::executor",
548 %ticket_id,
549 %sender,
550 fee_refund = %info.fee_refund_addr,
551 deposit = %info.deposit_value,
552 retry_value = %info.retry_value,
553 submission_fee = %fees.submission_fee,
554 escrow = %fees.escrow,
555 can_pay = fees.can_pay_for_gas,
556 "SubmitRetryable fee breakdown"
557 );
558
559 if !fees.submission_fee.is_zero() {
561 transfer_balance(
562 db,
563 sender,
564 self.arb_ctx.network_fee_account,
565 fees.submission_fee,
566 );
567 self.touched_accounts.insert(sender);
568 self.touched_accounts
569 .insert(self.arb_ctx.network_fee_account);
570 }
571
572 transfer_balance(db, sender, info.fee_refund_addr, fees.submission_fee_refund);
574 self.touched_accounts.insert(sender);
575 self.touched_accounts.insert(info.fee_refund_addr);
576
577 if !try_transfer_balance(db, sender, fees.escrow, info.retry_value) {
581 self.touched_accounts.insert(sender);
582 self.touched_accounts.insert(fees.escrow);
583 transfer_balance(
585 db,
586 self.arb_ctx.network_fee_account,
587 sender,
588 fees.submission_fee,
589 );
590 self.touched_accounts
591 .insert(self.arb_ctx.network_fee_account);
592 transfer_balance(
594 db,
595 sender,
596 info.fee_refund_addr,
597 fees.withheld_submission_fee,
598 );
599 self.touched_accounts.insert(info.fee_refund_addr);
600
601 self.pending_tx = Some(PendingArbTx {
602 sender,
603 tx_gas_limit: user_gas,
604 arb_tx_type: Some(ArbTxType::ArbitrumSubmitRetryableTx),
605 has_poster_costs: false,
606 poster_gas: 0,
607 evm_gas_used: 0,
608
609 charged_multi_gas: MultiGas::default(),
610 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
611 retry_context: None,
612 });
613
614 return Ok(EthTxResult {
615 result: revm::context::result::ResultAndState {
616 result: ExecutionResult::Revert {
617 gas_used: 0,
618 output: alloy_primitives::Bytes::new(),
619 },
620 state: Default::default(),
621 },
622 blob_gas_used: 0,
623 tx_type,
624 });
625 }
626 self.touched_accounts.insert(sender);
627 self.touched_accounts.insert(fees.escrow);
628
629 let state_ptr: *mut State<DB> = db as *mut State<DB>;
631 if let Ok(arb_state) = ArbosState::open(state_ptr, SystemBurner::new(None, false)) {
632 let _ = arb_state.retryable_state.create_retryable(
633 ticket_id,
634 fees.timeout,
635 sender,
636 info.retry_to,
637 info.retry_value,
638 info.beneficiary,
639 &info.retry_data,
640 );
641 }
642
643 let mut receipt_logs: Vec<Log> = Vec::new();
645 receipt_logs.push(Log {
646 address: arb_precompiles::ARBRETRYABLETX_ADDRESS,
647 data: alloy_primitives::LogData::new_unchecked(
648 vec![arb_precompiles::ticket_created_topic(), ticket_id],
649 alloy_primitives::Bytes::new(),
650 ),
651 });
652
653 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
654
655 if fees.can_pay_for_gas {
657 if self.arb_ctx.infra_fee_account != Address::ZERO {
659 transfer_balance(db, sender, self.arb_ctx.infra_fee_account, fees.infra_cost);
660 self.touched_accounts.insert(sender);
661 self.touched_accounts.insert(self.arb_ctx.infra_fee_account);
662 }
663 if !fees.network_cost.is_zero() {
665 transfer_balance(
666 db,
667 sender,
668 self.arb_ctx.network_fee_account,
669 fees.network_cost,
670 );
671 self.touched_accounts.insert(sender);
672 self.touched_accounts
673 .insert(self.arb_ctx.network_fee_account);
674 }
675 transfer_balance(db, sender, info.fee_refund_addr, fees.gas_price_refund);
677 self.touched_accounts.insert(sender);
678 self.touched_accounts.insert(info.fee_refund_addr);
679
680 tracing::debug!(
682 target: "arb::executor",
683 filtered = is_filtered,
684 "auto-redeem: checking is_filtered"
685 );
686 if !is_filtered {
687 let state_ptr2: *mut State<DB> = db as *mut State<DB>;
691 match ArbosState::open(state_ptr2, SystemBurner::new(None, false)) {
692 Ok(arb_state) => {
693 match arb_state.retryable_state.open_retryable(
694 ticket_id, 0, ) {
696 Ok(Some(retryable)) => {
697 let _ = retryable.increment_num_tries();
698
699 match retryable.make_tx(
700 U256::from(self.arb_ctx.chain_id),
701 0, effective_base_fee,
703 user_gas,
704 ticket_id,
705 info.fee_refund_addr,
706 fees.available_refund,
707 fees.submission_fee,
708 ) {
709 Ok(retry_tx) => {
710 let retry_tx_hash = {
712 let mut enc = Vec::new();
713 enc.push(ArbTxType::ArbitrumRetryTx.as_u8());
714 alloy_rlp::Encodable::encode(&retry_tx, &mut enc);
715 keccak256(&enc)
716 };
717
718 let mut event_data = Vec::with_capacity(128);
720 event_data.extend_from_slice(
721 &B256::left_padding_from(&user_gas.to_be_bytes()).0,
722 );
723 event_data.extend_from_slice(
724 &B256::left_padding_from(
725 info.fee_refund_addr.as_slice(),
726 )
727 .0,
728 );
729 event_data.extend_from_slice(
730 &fees.available_refund.to_be_bytes::<32>(),
731 );
732 event_data.extend_from_slice(
733 &fees.submission_fee.to_be_bytes::<32>(),
734 );
735
736 receipt_logs.push(Log {
737 address: arb_precompiles::ARBRETRYABLETX_ADDRESS,
738 data: alloy_primitives::LogData::new_unchecked(
739 vec![
740 arb_precompiles::redeem_scheduled_topic(),
741 ticket_id,
742 retry_tx_hash,
743 B256::left_padding_from(&0u64.to_be_bytes()),
744 ],
745 event_data.into(),
746 ),
747 });
748
749 if let Some(hooks) = self.arb_hooks.as_mut() {
750 let mut encoded = Vec::new();
751 encoded.push(ArbTxType::ArbitrumRetryTx.as_u8());
752 alloy_rlp::Encodable::encode(&retry_tx, &mut encoded);
753 tracing::debug!(
754 target: "arb::executor",
755 encoded_len = encoded.len(),
756 "Scheduling auto-redeem retry tx"
757 );
758 hooks.tx_proc.scheduled_txs.push(encoded);
759 } else {
760 tracing::warn!(
761 target: "arb::executor",
762 "Cannot schedule auto-redeem: arb_hooks is None"
763 );
764 }
765 }
766 Err(_) => {
767 tracing::warn!(
768 target: "arb::executor",
769 "Auto-redeem make_tx failed"
770 );
771 }
772 }
773 }
774 Ok(None) => {
775 tracing::warn!(
776 target: "arb::executor",
777 %ticket_id,
778 "open_retryable returned None after create"
779 );
780 }
781 Err(_) => {
782 tracing::warn!(
783 target: "arb::executor",
784 "open_retryable failed"
785 );
786 }
787 }
788 }
789 Err(_) => {
790 tracing::warn!(
791 target: "arb::executor",
792 "ArbosState::open failed for auto-redeem"
793 );
794 }
795 }
796 }
797 } else if !fees.gas_cost_refund.is_zero() {
798 transfer_balance(db, sender, info.fee_refund_addr, fees.gas_cost_refund);
800 self.touched_accounts.insert(sender);
801 self.touched_accounts.insert(info.fee_refund_addr);
802 }
803
804 let gas_used = if fees.can_pay_for_gas { user_gas } else { 0 };
810 self.pending_tx = Some(PendingArbTx {
811 sender,
812 tx_gas_limit: user_gas,
813 arb_tx_type: Some(ArbTxType::ArbitrumSubmitRetryableTx),
814 has_poster_costs: false, poster_gas: 0,
816 evm_gas_used: gas_used,
817 charged_multi_gas: if fees.can_pay_for_gas {
818 MultiGas::l2_calldata_gas(user_gas)
819 } else {
820 MultiGas::default()
821 },
822 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
823 retry_context: None,
824 });
825
826 let ticket_bytes = alloy_primitives::Bytes::copy_from_slice(ticket_id.as_slice());
830
831 if is_filtered {
832 Ok(EthTxResult {
833 result: revm::context::result::ResultAndState {
834 result: ExecutionResult::Revert {
835 gas_used,
836 output: ticket_bytes,
837 },
838 state: Default::default(),
839 },
840 blob_gas_used: 0,
841 tx_type,
842 })
843 } else {
844 Ok(EthTxResult {
845 result: revm::context::result::ResultAndState {
846 result: ExecutionResult::Success {
847 reason: revm::context::result::SuccessReason::Return,
848 gas_used,
849 gas_refunded: 0,
850 output: revm::context::result::Output::Call(ticket_bytes),
851 logs: receipt_logs,
852 },
853 state: Default::default(),
854 },
855 blob_gas_used: 0,
856 tx_type,
857 })
858 }
859 }
860}
861
862impl<'db, DB, E, Spec, R> BlockExecutor for ArbBlockExecutor<'_, E, Spec, R>
863where
864 DB: Database + 'db,
865 E: Evm<
866 DB = &'db mut State<DB>,
867 Tx: FromRecoveredTx<R::Transaction> + FromTxWithEncoded<R::Transaction> + ArbTransactionEnv,
868 >,
869 Spec: EthExecutorSpec,
870 R: ReceiptBuilder<
871 Transaction: Transaction + Encodable2718 + ArbTransactionExt,
872 Receipt: TxReceipt<Log = Log> + arb_primitives::SetArbReceiptFields,
873 >,
874 R::Transaction: TransactionEnvelope,
875{
876 type Transaction = R::Transaction;
877 type Receipt = R::Receipt;
878 type Evm = E;
879 type Result = EthTxResult<E::HaltReason, <R::Transaction as TransactionEnvelope>::TxType>;
880
881 fn apply_pre_execution_changes(&mut self) -> Result<(), BlockExecutionError> {
882 self.inner.apply_pre_execution_changes()?;
883
884 {
886 let block = self.inner.evm().block();
887 let timestamp = revm::context::Block::timestamp(block).to::<u64>();
888 if self.arb_ctx.block_timestamp == 0 {
889 self.arb_ctx.block_timestamp = timestamp;
890 }
891 self.arb_ctx.coinbase = revm::context::Block::beneficiary(block);
892 self.arb_ctx.basefee = U256::from(revm::context::Block::basefee(block));
893 if let Some(prevrandao) = revm::context::Block::prevrandao(block) {
894 if self.arb_ctx.l1_block_number == 0 {
895 self.arb_ctx.l1_block_number =
896 crate::config::l1_block_number_from_mix_hash(&prevrandao);
897 }
898 }
899 }
900
901 if self.arb_ctx.l2_block_number > 0 {
906 arb_precompiles::set_current_l2_block(self.arb_ctx.l2_block_number);
907 arb_precompiles::set_cached_l1_block_number(
908 self.arb_ctx.l2_block_number,
909 self.arb_ctx.l1_block_number,
910 );
911 }
912
913 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
917 let state_ptr: *mut State<DB> = db as *mut State<DB>;
918
919 if let Ok(arb_state) = ArbosState::open(state_ptr, SystemBurner::new(None, false)) {
920 let _ = arb_state.l2_pricing_state.commit_multi_gas_fees();
922
923 if let Ok(base_fee) = arb_state.l2_pricing_state.base_fee_wei() {
926 self.arb_ctx.basefee = base_fee;
927 }
928
929 self.load_state_params(&arb_state);
931
932 self.block_gas_left = arb_state
934 .l2_pricing_state
935 .per_block_gas_limit()
936 .unwrap_or(0);
937
938 if let Ok(l1_block_number) = arb_state.blockhashes.l1_block_number() {
944 let lower = l1_block_number.saturating_sub(256);
945 let state_ref = unsafe { &mut *state_ptr };
947 for n in lower..l1_block_number {
948 if let Ok(Some(hash)) = arb_state.blockhashes.block_hash(n) {
949 state_ref.block_hashes.insert(n, hash);
950 }
951 }
952 }
953 }
954
955 tracing::trace!(
959 target: "arb::executor",
960 l1_block = self.arb_ctx.l1_block_number,
961 delayed_msgs = self.arb_ctx.delayed_messages_read,
962 chain_id = self.arb_ctx.chain_id,
963 basefee = %self.arb_ctx.basefee,
964 arbos_version = self.arb_ctx.arbos_version,
965 has_hooks = self.arb_hooks.is_some(),
966 "starting block execution"
967 );
968
969 Ok(())
970 }
971
972 fn execute_transaction_without_commit(
973 &mut self,
974 tx: impl ExecutableTx<Self>,
975 ) -> Result<Self::Result, BlockExecutionError> {
976 let (tx_env, recovered) = tx.into_parts();
978 let sender = *recovered.signer();
979 let tx_type_raw = recovered.tx().ty();
980 let tx_gas_limit = recovered.tx().gas_limit();
981 let envelope_tx_type = recovered.tx().tx_type();
982
983 let arb_tx_type = ArbTxType::from_u8(tx_type_raw).ok();
985 let is_arb_internal = arb_tx_type == Some(ArbTxType::ArbitrumInternalTx);
986 let is_arb_deposit = arb_tx_type == Some(ArbTxType::ArbitrumDepositTx);
987 let is_submit_retryable = arb_tx_type == Some(ArbTxType::ArbitrumSubmitRetryableTx);
988 let is_retry_tx = arb_tx_type == Some(ArbTxType::ArbitrumRetryTx);
989 let is_contract_tx = arb_tx_type == Some(ArbTxType::ArbitrumContractTx);
990 let has_poster_costs = tx_type_has_poster_costs(tx_type_raw);
991
992 let is_user_tx =
996 !is_arb_internal && !is_arb_deposit && !is_submit_retryable && !is_retry_tx;
997 const TX_GAS_MIN: u64 = 21_000;
998 if is_user_tx && self.block_gas_left < TX_GAS_MIN {
999 return Err(BlockExecutionError::msg("block gas limit reached"));
1000 }
1001
1002 crate::evm::reset_stylus_pages();
1004 arb_precompiles::set_poster_balance_correction(U256::ZERO);
1005 arb_precompiles::set_current_tx_sender(Address::ZERO);
1006 if let Some(hooks) = self.arb_hooks.as_mut() {
1007 hooks.tx_proc.poster_fee = U256::ZERO;
1008 hooks.tx_proc.poster_gas = 0;
1009 hooks.tx_proc.compute_hold_gas = 0;
1010 hooks.tx_proc.current_retryable = None;
1011 hooks.tx_proc.current_refund_to = None;
1012 hooks.tx_proc.scheduled_txs.clear();
1013 }
1014
1015 if is_arb_internal {
1019 use arbos::tx_processor::ARBOS_ADDRESS;
1020
1021 if sender != ARBOS_ADDRESS {
1022 return Err(BlockExecutionError::msg(
1023 "internal tx not from ArbOS address",
1024 ));
1025 }
1026
1027 let tx_data = recovered.tx().input().to_vec();
1028 let tx_type = recovered.tx().tx_type();
1029 let mut tx_err = None;
1030
1031 if tx_data.len() >= 4 {
1032 let selector: [u8; 4] = tx_data[0..4].try_into().unwrap();
1033 let is_start_block = selector == internal_tx::INTERNAL_TX_START_BLOCK_METHOD_ID;
1034
1035 if is_start_block {
1036 if let Ok(start_data) = internal_tx::decode_start_block_data(&tx_data) {
1037 self.arb_ctx.l1_base_fee = start_data.l1_base_fee;
1038 self.arb_ctx.time_passed = start_data.time_passed;
1039 }
1040 }
1041
1042 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1043 let state_ptr: *mut State<DB> = db as *mut State<DB>;
1044 if let Ok(mut arb_state) =
1045 ArbosState::open(state_ptr, SystemBurner::new(None, false))
1046 {
1047 let block = self.inner.evm().block();
1048 let current_time = revm::context::Block::timestamp(block).to::<u64>();
1049 let ctx = InternalTxContext {
1050 block_number: revm::context::Block::number(block).to::<u64>(),
1051 current_time,
1052 prev_hash: self.arb_ctx.parent_hash,
1053 };
1054
1055 if is_start_block
1057 && arb_state.arbos_version()
1058 >= arb_chainspec::arbos_version::ARBOS_VERSION_40
1059 {
1060 process_parent_block_hash(
1062 unsafe { &mut *state_ptr },
1063 ctx.block_number,
1064 ctx.prev_hash,
1065 );
1066 }
1067
1068 let touched_ptr =
1069 &mut self.touched_accounts as *mut std::collections::HashSet<Address>;
1070 let zombie_ptr =
1071 &mut self.zombie_accounts as *mut std::collections::HashSet<Address>;
1072 let finalise_ptr =
1073 &self.finalise_deleted as *const std::collections::HashSet<Address>;
1074 let arbos_ver = self.arb_ctx.arbos_version;
1075 let mut do_transfer = |from: Address, to: Address, amount: U256| {
1076 unsafe {
1078 if amount.is_zero()
1079 && arbos_ver < arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS
1080 {
1081 create_zombie_if_deleted(
1082 &mut *state_ptr,
1083 from,
1084 &*finalise_ptr,
1085 &mut *zombie_ptr,
1086 &mut *touched_ptr,
1087 );
1088 }
1089 transfer_balance(&mut *state_ptr, from, to, amount);
1090 if !amount.is_zero() {
1091 (*zombie_ptr).remove(&from);
1092 }
1093 (*zombie_ptr).remove(&to);
1094 (*touched_ptr).insert(from);
1095 (*touched_ptr).insert(to);
1096 }
1097 Ok(())
1098 };
1099 let mut do_balance = |addr: Address| -> U256 {
1100 unsafe { get_balance(&mut *state_ptr, addr) }
1102 };
1103 if let Err(e) = internal_tx::apply_internal_tx_update(
1104 &tx_data,
1105 &mut arb_state,
1106 &ctx,
1107 &mut do_transfer,
1108 &mut do_balance,
1109 ) {
1110 tracing::warn!(
1111 target: "arb::executor",
1112 error = %e,
1113 "internal tx processing failed"
1114 );
1115 tx_err = Some(e);
1116 }
1117
1118 if is_start_block {
1119 self.load_state_params(&arb_state);
1120
1121 if let Ok(l1_block_number) = arb_state.blockhashes.l1_block_number() {
1126 self.arb_ctx.l1_block_number = l1_block_number;
1127 arb_precompiles::set_l1_block_number_for_evm(l1_block_number);
1128 arb_precompiles::set_cached_l1_block_number(
1129 self.arb_ctx.l2_block_number,
1130 l1_block_number,
1131 );
1132
1133 let lower = l1_block_number.saturating_sub(256);
1135 let state_ref = unsafe { &mut *state_ptr };
1136 for n in lower..l1_block_number {
1137 if let Ok(Some(hash)) = arb_state.blockhashes.block_hash(n) {
1138 state_ref.block_hashes.insert(n, hash);
1139 }
1140 }
1141 }
1142 }
1143 }
1144 }
1145
1146 self.pending_tx = Some(PendingArbTx {
1148 sender,
1149 tx_gas_limit: 0,
1150 arb_tx_type: Some(ArbTxType::ArbitrumInternalTx),
1151 has_poster_costs: false,
1152 poster_gas: 0,
1153 evm_gas_used: 0,
1154
1155 charged_multi_gas: MultiGas::default(),
1156 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
1157 retry_context: None,
1158 });
1159
1160 if let Some(err) = tx_err {
1162 return Err(BlockExecutionError::msg(format!(
1163 "failed to apply internal transaction: {err}"
1164 )));
1165 }
1166
1167 return Ok(EthTxResult {
1168 result: revm::context::result::ResultAndState {
1169 result: ExecutionResult::Success {
1170 reason: revm::context::result::SuccessReason::Return,
1171 gas_used: 0,
1172 gas_refunded: 0,
1173 output: revm::context::result::Output::Call(alloy_primitives::Bytes::new()),
1174 logs: Vec::new(),
1175 },
1176 state: Default::default(),
1177 },
1178 blob_gas_used: 0,
1179 tx_type,
1180 });
1181 }
1182
1183 if is_arb_deposit {
1186 let value = recovered.tx().value();
1187 let mut to = match recovered.tx().kind() {
1188 TxKind::Call(addr) => addr,
1189 TxKind::Create => {
1190 return Err(BlockExecutionError::msg("deposit tx has no To address"));
1191 }
1192 };
1193 let tx_type = recovered.tx().tx_type();
1194 let tx_hash = recovered.tx().trie_hash();
1195
1196 let mut is_filtered = false;
1200 {
1201 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1202 let state_ptr: *mut State<DB> = db as *mut State<DB>;
1203 if let Ok(arb_state) = ArbosState::open(state_ptr, SystemBurner::new(None, false)) {
1204 if arb_state.filtered_transactions.is_filtered_free(tx_hash) {
1205 if let Ok(recipient) = arb_state.filtered_funds_recipient_or_default() {
1206 to = recipient;
1207 }
1208 is_filtered = true;
1209 }
1210 }
1211 }
1212
1213 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1214 mint_balance(db, sender, value);
1216 transfer_balance(db, sender, to, value);
1217 self.touched_accounts.insert(sender);
1218 self.touched_accounts.insert(to);
1219
1220 let value_i128: i128 = value.try_into().unwrap_or(i128::MAX);
1222 self.expected_balance_delta = self.expected_balance_delta.saturating_add(value_i128);
1223
1224 self.pending_tx = Some(PendingArbTx {
1225 sender,
1226 tx_gas_limit: 0,
1227 arb_tx_type: Some(ArbTxType::ArbitrumDepositTx),
1228 has_poster_costs: false,
1229 poster_gas: 0,
1230 evm_gas_used: 0,
1231
1232 charged_multi_gas: MultiGas::default(),
1233 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
1234 retry_context: None,
1235 });
1236
1237 let result = if is_filtered {
1241 ExecutionResult::Revert {
1242 gas_used: 0,
1243 output: alloy_primitives::Bytes::from("filtered transaction"),
1244 }
1245 } else {
1246 ExecutionResult::Success {
1247 reason: revm::context::result::SuccessReason::Return,
1248 gas_used: 0,
1249 gas_refunded: 0,
1250 output: revm::context::result::Output::Call(alloy_primitives::Bytes::new()),
1251 logs: Vec::new(),
1252 }
1253 };
1254
1255 return Ok(EthTxResult {
1256 result: revm::context::result::ResultAndState {
1257 result,
1258 state: Default::default(),
1259 },
1260 blob_gas_used: 0,
1261 tx_type,
1262 });
1263 }
1264
1265 if is_submit_retryable {
1267 if let Some(info) = recovered.tx().submit_retryable_info() {
1268 let ticket_id = recovered.tx().trie_hash();
1269 let tx_type = recovered.tx().tx_type();
1270 return self.execute_submit_retryable(ticket_id, tx_type, info);
1271 }
1272 }
1273
1274 let mut retry_context = None;
1276 if is_retry_tx {
1277 if let Some(info) = recovered.tx().retry_tx_info() {
1278 let block = self.inner.evm().block();
1279 let current_time = revm::context::Block::timestamp(block).to::<u64>();
1280 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1281 let state_ptr: *mut State<DB> = db as *mut State<DB>;
1282
1283 if let Ok(arb_state) = ArbosState::open(state_ptr, SystemBurner::new(None, false)) {
1285 let retryable = arb_state
1286 .retryable_state
1287 .open_retryable(info.ticket_id, current_time);
1288
1289 match retryable {
1290 Ok(Some(_)) => {
1291 let escrow = retryables::retryable_escrow_address(info.ticket_id);
1293 let value = recovered.tx().value();
1294
1295 if value.is_zero()
1298 && self.arb_ctx.arbos_version
1299 < arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS
1300 {
1301 create_zombie_if_deleted(
1302 db,
1303 escrow,
1304 &self.finalise_deleted,
1305 &mut self.zombie_accounts,
1306 &mut self.touched_accounts,
1307 );
1308 }
1309
1310 if !try_transfer_balance(db, escrow, sender, value) {
1311 let tx_type = recovered.tx().tx_type();
1313 self.pending_tx = Some(PendingArbTx {
1314 sender,
1315 tx_gas_limit: 0,
1316 arb_tx_type: Some(ArbTxType::ArbitrumRetryTx),
1317 has_poster_costs: false,
1318 poster_gas: 0,
1319 evm_gas_used: 0,
1320
1321 charged_multi_gas: MultiGas::default(),
1322 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
1323 retry_context: None,
1324 });
1325 return Ok(EthTxResult {
1326 result: revm::context::result::ResultAndState {
1327 result: ExecutionResult::Revert {
1328 gas_used: 0,
1329 output: alloy_primitives::Bytes::new(),
1330 },
1331 state: Default::default(),
1332 },
1333 blob_gas_used: 0,
1334 tx_type,
1335 });
1336 }
1337
1338 if !value.is_zero() {
1340 self.zombie_accounts.remove(&escrow);
1341 }
1342 self.zombie_accounts.remove(&sender);
1343 self.touched_accounts.insert(escrow);
1344 self.touched_accounts.insert(sender);
1345
1346 let prepaid = self
1348 .arb_ctx
1349 .basefee
1350 .saturating_mul(U256::from(tx_gas_limit));
1351 mint_balance(db, sender, prepaid);
1352
1353 if let Some(hooks) = self.arb_hooks.as_mut() {
1355 hooks
1356 .tx_proc
1357 .prepare_retry_tx(info.ticket_id, info.refund_to);
1358 }
1359
1360 retry_context = Some(PendingRetryContext {
1361 ticket_id: info.ticket_id,
1362 refund_to: info.refund_to,
1363 gas_fee_cap: info.gas_fee_cap,
1364 max_refund: info.max_refund,
1365 submission_fee_refund: info.submission_fee_refund,
1366 call_value: recovered.tx().value(),
1367 });
1368 }
1369 Ok(None) => {
1370 let tx_type = recovered.tx().tx_type();
1372 self.pending_tx = Some(PendingArbTx {
1373 sender,
1374 tx_gas_limit: 0,
1375 arb_tx_type: Some(ArbTxType::ArbitrumRetryTx),
1376 has_poster_costs: false,
1377 poster_gas: 0,
1378 evm_gas_used: 0,
1379
1380 charged_multi_gas: MultiGas::default(),
1381 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
1382 retry_context: None,
1383 });
1384 let err_msg = format!("retryable ticket {} not found", info.ticket_id,);
1385 return Ok(EthTxResult {
1386 result: revm::context::result::ResultAndState {
1387 result: ExecutionResult::Revert {
1388 gas_used: 0,
1389 output: alloy_primitives::Bytes::from(err_msg.into_bytes()),
1390 },
1391 state: Default::default(),
1392 },
1393 blob_gas_used: 0,
1394 tx_type,
1395 });
1396 }
1397 Err(_) => {
1398 let tx_type = recovered.tx().tx_type();
1400 self.pending_tx = Some(PendingArbTx {
1401 sender,
1402 tx_gas_limit: 0,
1403 arb_tx_type: Some(ArbTxType::ArbitrumRetryTx),
1404 has_poster_costs: false,
1405 poster_gas: 0,
1406 evm_gas_used: 0,
1407
1408 charged_multi_gas: MultiGas::default(),
1409 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
1410 retry_context: None,
1411 });
1412 return Ok(EthTxResult {
1413 result: revm::context::result::ResultAndState {
1414 result: ExecutionResult::Revert {
1415 gas_used: 0,
1416 output: alloy_primitives::Bytes::from(
1417 format!("error opening retryable {}", info.ticket_id,)
1418 .into_bytes(),
1419 ),
1420 },
1421 state: Default::default(),
1422 },
1423 blob_gas_used: 0,
1424 tx_type,
1425 });
1426 }
1427 }
1428 }
1429 }
1430 }
1431
1432 let mut poster_gas = 0u64;
1435 let mut compute_hold_gas = 0u64;
1436 let calldata_units = if has_poster_costs {
1437 let tx_bytes = recovered.tx().encoded_2718();
1438 let (_poster_cost, units) = l1_pricing::compute_poster_cost_standalone(
1439 &tx_bytes,
1440 self.arb_ctx.coinbase,
1441 self.arb_ctx.l1_price_per_unit,
1442 self.arb_ctx.brotli_compression_level,
1443 );
1444
1445 if let Some(hooks) = self.arb_hooks.as_mut() {
1446 let base_fee = self.arb_ctx.basefee;
1447 hooks.tx_proc.poster_gas =
1448 compute_poster_gas(_poster_cost, base_fee, false, self.arb_ctx.min_base_fee);
1449 hooks.tx_proc.poster_fee =
1450 base_fee.saturating_mul(U256::from(hooks.tx_proc.poster_gas));
1451 poster_gas = hooks.tx_proc.poster_gas;
1452 }
1453
1454 units
1455 } else {
1456 0
1457 };
1458
1459 if let Some(hooks) = self.arb_hooks.as_mut() {
1464 if !hooks.is_eth_call {
1465 let spec = arb_chainspec::spec_id_by_arbos_version(self.arb_ctx.arbos_version);
1466 let intrinsic_estimate = estimate_intrinsic_gas(recovered.tx(), spec);
1467 let gas_after_intrinsic = tx_gas_limit.saturating_sub(intrinsic_estimate);
1468 let gas_after_poster = gas_after_intrinsic.saturating_sub(poster_gas);
1469
1470 let max_compute =
1471 if hooks.arbos_version < arb_chainspec::arbos_version::ARBOS_VERSION_50 {
1472 hooks.per_block_gas_limit
1473 } else {
1474 hooks.per_tx_gas_limit.saturating_sub(intrinsic_estimate)
1475 };
1476
1477 if max_compute > 0 && gas_after_poster > max_compute {
1478 compute_hold_gas = gas_after_poster - max_compute;
1479 hooks.tx_proc.compute_hold_gas = compute_hold_gas;
1480 }
1481 }
1482 }
1483
1484 if is_user_tx
1489 && self.arb_ctx.arbos_version < arb_chainspec::arbos_version::ARBOS_VERSION_50
1490 && self.user_txs_processed > 0
1491 {
1492 const TX_GAS: u64 = 21_000;
1493 let compute_gas = tx_gas_limit.saturating_sub(poster_gas).max(TX_GAS);
1494 if compute_gas > self.block_gas_left {
1495 return Err(BlockExecutionError::msg("block gas limit reached"));
1496 }
1497 }
1498
1499 if calldata_units > 0 {
1502 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1503 let state_ptr: *mut State<DB> = db as *mut State<DB>;
1504 if let Ok(arb_state) = ArbosState::open(state_ptr, SystemBurner::new(None, false)) {
1505 let _ = arb_state
1506 .l1_pricing_state
1507 .add_to_units_since_update(calldata_units);
1508 }
1509 }
1510
1511 let mut tx_env = tx_env;
1519 let gas_deduction = poster_gas.saturating_add(compute_hold_gas);
1520 let evm_gas_limit_before = revm::context_interface::Transaction::gas_limit(&tx_env);
1521 if gas_deduction > 0 {
1522 tx_env.set_gas_limit(evm_gas_limit_before.saturating_sub(gas_deduction));
1523 }
1524
1525 {
1530 let correction = self
1531 .arb_ctx
1532 .basefee
1533 .saturating_mul(U256::from(poster_gas.saturating_add(compute_hold_gas)));
1534 arb_precompiles::set_poster_balance_correction(correction);
1535 arb_precompiles::set_current_tx_sender(sender);
1536 }
1537
1538 {
1541 use arbos::tx_processor::RevertedTxAction;
1542
1543 let tx_hash = recovered.tx().trie_hash();
1545
1546 let is_filtered = {
1548 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1549 let state_ptr: *mut State<DB> = db as *mut State<DB>;
1550 ArbosState::open(state_ptr, SystemBurner::new(None, false))
1551 .ok()
1552 .map(|s| s.filtered_transactions.is_filtered_free(tx_hash))
1553 .unwrap_or(false)
1554 };
1555
1556 if let Some(hooks) = self.arb_hooks.as_ref() {
1557 let action = hooks.tx_proc.reverted_tx_hook(
1558 Some(tx_hash),
1559 None, is_filtered,
1561 );
1562
1563 match action {
1564 RevertedTxAction::PreRecordedRevert { gas_to_consume } => {
1565 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1566 increment_nonce(db, sender);
1567 self.touched_accounts.insert(sender);
1568 let gas_used = poster_gas + gas_to_consume;
1569 let charged_multi_gas = MultiGas::l1_calldata_gas(poster_gas)
1570 .saturating_add(MultiGas::computation_gas(gas_to_consume));
1571 self.pending_tx = Some(PendingArbTx {
1572 sender,
1573 tx_gas_limit,
1574 arb_tx_type,
1575 has_poster_costs,
1576 poster_gas,
1577 evm_gas_used: 0,
1578 charged_multi_gas,
1579 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
1580 retry_context,
1581 });
1582 return Ok(EthTxResult {
1583 result: revm::context::result::ResultAndState {
1584 result: ExecutionResult::Revert {
1585 gas_used,
1586 output: alloy_primitives::Bytes::new(),
1587 },
1588 state: Default::default(),
1589 },
1590 blob_gas_used: 0,
1591 tx_type: envelope_tx_type,
1592 });
1593 }
1594 RevertedTxAction::FilteredTx => {
1595 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1596 increment_nonce(db, sender);
1597 self.touched_accounts.insert(sender);
1598 let gas_remaining = tx_gas_limit
1600 .saturating_sub(poster_gas)
1601 .saturating_sub(compute_hold_gas);
1602 let gas_used = tx_gas_limit;
1603 let charged_multi_gas = MultiGas::l1_calldata_gas(poster_gas)
1604 .saturating_add(MultiGas::computation_gas(gas_remaining));
1605 self.pending_tx = Some(PendingArbTx {
1606 sender,
1607 tx_gas_limit,
1608 arb_tx_type,
1609 has_poster_costs,
1610 poster_gas,
1611 evm_gas_used: 0,
1612 charged_multi_gas,
1613 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
1614 retry_context,
1615 });
1616 return Ok(EthTxResult {
1617 result: revm::context::result::ResultAndState {
1618 result: ExecutionResult::Revert {
1619 gas_used,
1620 output: alloy_primitives::Bytes::from(
1621 "filtered transaction".as_bytes(),
1622 ),
1623 },
1624 state: Default::default(),
1625 },
1626 blob_gas_used: 0,
1627 tx_type: envelope_tx_type,
1628 });
1629 }
1630 RevertedTxAction::None => {}
1631 }
1632 }
1633 }
1634
1635 let upfront_gas_price: u128 = revm::context_interface::Transaction::gas_price(&tx_env);
1641
1642 let should_drop_tip = self
1646 .arb_hooks
1647 .as_ref()
1648 .map(|h| h.drop_tip())
1649 .unwrap_or(false);
1650 if should_drop_tip {
1651 let base_fee: u128 = self.arb_ctx.basefee.try_into().unwrap_or(u128::MAX);
1652 if upfront_gas_price > base_fee {
1653 tx_env.set_gas_price(base_fee);
1654 tx_env.set_gas_priority_fee(Some(0));
1655 }
1656 }
1657
1658 arb_precompiles::set_tx_is_aliased(arbos::util::does_tx_type_alias(tx_type_raw));
1662
1663 {
1667 use arb_precompiles::storage_slot::current_tx_poster_fee_slot;
1668 let poster_fee_val = self
1669 .arb_hooks
1670 .as_ref()
1671 .map(|h| h.tx_proc.poster_fee)
1672 .unwrap_or(U256::ZERO);
1673 arb_precompiles::set_current_tx_poster_fee(
1674 poster_fee_val.try_into().unwrap_or(u128::MAX),
1675 );
1676 arb_storage::write_arbos_storage(
1677 self.inner.evm_mut().db_mut(),
1678 current_tx_poster_fee_slot(),
1679 poster_fee_val,
1680 );
1681 }
1682
1683 {
1686 use arb_precompiles::storage_slot::{current_redeemer_slot, current_retryable_slot};
1687 let retryable_id = retry_context
1688 .as_ref()
1689 .map(|ctx| U256::from_be_bytes(ctx.ticket_id.0))
1690 .unwrap_or(U256::ZERO);
1691 arb_storage::write_arbos_storage(
1692 self.inner.evm_mut().db_mut(),
1693 current_retryable_slot(),
1694 retryable_id,
1695 );
1696 let redeemer = retry_context
1698 .as_ref()
1699 .map(|ctx| U256::from_be_bytes(B256::left_padding_from(ctx.refund_to.as_slice()).0))
1700 .unwrap_or(U256::ZERO);
1701 arb_storage::write_arbos_storage(
1702 self.inner.evm_mut().db_mut(),
1703 current_redeemer_slot(),
1704 redeemer,
1705 );
1706 }
1707
1708 let rollback_pre_exec_state = |this: &mut Self, units: u64| {
1711 use arb_precompiles::storage_slot::{
1712 current_redeemer_slot, current_retryable_slot, current_tx_poster_fee_slot,
1713 };
1714 let db: &mut State<DB> = this.inner.evm_mut().db_mut();
1715 arb_storage::write_arbos_storage(db, current_tx_poster_fee_slot(), U256::ZERO);
1716 arb_storage::write_arbos_storage(db, current_retryable_slot(), U256::ZERO);
1717 arb_storage::write_arbos_storage(db, current_redeemer_slot(), U256::ZERO);
1718 if units > 0 {
1719 let state_ptr: *mut State<DB> = db as *mut State<DB>;
1720 if let Ok(arb_state) = ArbosState::open(state_ptr, SystemBurner::new(None, false)) {
1721 let _ = arb_state
1722 .l1_pricing_state
1723 .subtract_from_units_since_update(units);
1724 }
1725 }
1726 };
1727
1728 if is_user_tx {
1731 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1732 let account = db
1733 .load_cache_account(sender)
1734 .ok()
1735 .and_then(|a| a.account_info());
1736 let sender_balance = account.as_ref().map(|a| a.balance).unwrap_or(U256::ZERO);
1737 let sender_nonce = account.as_ref().map(|a| a.nonce).unwrap_or(0);
1738
1739 if !is_contract_tx {
1741 let tx_nonce = revm::context_interface::Transaction::nonce(&tx_env);
1742 if tx_nonce != sender_nonce {
1743 rollback_pre_exec_state(self, calldata_units);
1744 return Err(BlockExecutionError::msg(format!(
1745 "nonce mismatch: address {sender} tx nonce {tx_nonce} != state nonce {sender_nonce}"
1746 )));
1747 }
1748 }
1749
1750 let gas_cost = U256::from(tx_gas_limit) * U256::from(upfront_gas_price);
1751 let tx_value = revm::context_interface::Transaction::value(&tx_env);
1752 let total_cost = gas_cost.saturating_add(tx_value);
1753 if sender_balance < total_cost {
1754 rollback_pre_exec_state(self, calldata_units);
1755 return Err(BlockExecutionError::msg(format!(
1756 "insufficient funds: address {sender} have {sender_balance} want {total_cost}"
1757 )));
1758 }
1759 }
1760
1761 if is_retry_tx || is_contract_tx {
1767 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1768 let sender_nonce = db
1769 .load_cache_account(sender)
1770 .map(|a| a.account_info().map(|i| i.nonce).unwrap_or(0))
1771 .unwrap_or(0);
1772 tx_env.set_nonce(sender_nonce);
1773 }
1774
1775 let mut output = match self
1776 .inner
1777 .execute_transaction_without_commit((tx_env, recovered))
1778 {
1779 Ok(o) => o,
1780 Err(e) => {
1781 rollback_pre_exec_state(self, calldata_units);
1782 return Err(e);
1783 }
1784 };
1785
1786 let evm_gas_used = output.result.result.gas_used();
1789
1790 if poster_gas > 0 {
1797 adjust_result_gas_used(&mut output.result.result, poster_gas);
1798 }
1799
1800 let mut total_donated_gas = 0u64;
1808 let mut retry_tx_hash_fixes: Vec<(usize, B256)> = Vec::new();
1810 if let ExecutionResult::Success { ref logs, .. } = output.result.result {
1811 let redeem_topic = arb_precompiles::redeem_scheduled_topic();
1812 let precompile_addr = arb_precompiles::ARBRETRYABLETX_ADDRESS;
1813
1814 for (log_idx, log) in logs.iter().enumerate() {
1815 if log.address != precompile_addr {
1816 continue;
1817 }
1818 if log.topics().is_empty() || log.topics()[0] != redeem_topic {
1819 continue;
1820 }
1821 if log.topics().len() < 4 || log.data.data.len() < 128 {
1822 continue;
1823 }
1824
1825 let ticket_id = log.topics()[1];
1826 let seq_num_bytes = log.topics()[3];
1827 let nonce =
1828 u64::from_be_bytes(seq_num_bytes.0[24..32].try_into().unwrap_or([0u8; 8]));
1829 let data = &log.data.data;
1830 let donated_gas = U256::from_be_slice(&data[0..32]).to::<u64>();
1831 total_donated_gas = total_donated_gas.saturating_add(donated_gas);
1832 let gas_donor = Address::from_slice(&data[44..64]);
1833 let max_refund = U256::from_be_slice(&data[64..96]);
1834 let submission_fee_refund = U256::from_be_slice(&data[96..128]);
1835
1836 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1838 let state_ptr: *mut State<DB> = db as *mut State<DB>;
1839 let current_time = {
1840 let block = self.inner.evm().block();
1841 revm::context::Block::timestamp(block).to::<u64>()
1842 };
1843 if let Ok(arb_state) = ArbosState::open(state_ptr, SystemBurner::new(None, false)) {
1844 if let Ok(Some(retryable)) = arb_state
1845 .retryable_state
1846 .open_retryable(ticket_id, current_time)
1847 {
1848 let _ = retryable.increment_num_tries();
1849
1850 if let Ok(retry_tx) = retryable.make_tx(
1851 U256::from(self.arb_ctx.chain_id),
1852 nonce,
1853 self.arb_ctx.basefee,
1854 donated_gas,
1855 ticket_id,
1856 gas_donor,
1857 max_refund,
1858 submission_fee_refund,
1859 ) {
1860 let mut encoded = Vec::new();
1862 encoded.push(ArbTxType::ArbitrumRetryTx.as_u8());
1863 alloy_rlp::Encodable::encode(&retry_tx, &mut encoded);
1864 let correct_hash = keccak256(&encoded);
1865 retry_tx_hash_fixes.push((log_idx, correct_hash));
1866
1867 if let Some(hooks) = self.arb_hooks.as_mut() {
1868 hooks.tx_proc.scheduled_txs.push(encoded);
1869 }
1870 }
1871 }
1872
1873 let _ = arb_state
1875 .l2_pricing_state
1876 .shrink_backlog(donated_gas, MultiGas::default());
1877 if let Ok(b) = arb_state.l2_pricing_state.gas_backlog() {
1878 arb_precompiles::set_current_gas_backlog(b);
1879 }
1880 }
1881 }
1882 }
1883
1884 if !retry_tx_hash_fixes.is_empty() {
1888 if let ExecutionResult::Success { ref mut logs, .. } = output.result.result {
1889 for (log_idx, correct_hash) in &retry_tx_hash_fixes {
1890 if let Some(log) = logs.get_mut(*log_idx) {
1891 if log.data.topics().len() > 2 {
1892 let topics = log.data.topics_mut_unchecked();
1893 topics[2] = *correct_hash;
1894 }
1895 }
1896 }
1897 }
1898 }
1899
1900 let charged_multi_gas = MultiGas::l1_calldata_gas(poster_gas)
1905 .saturating_add(MultiGas::computation_gas(evm_gas_used));
1906
1907 self.pending_tx = Some(PendingArbTx {
1908 sender,
1909 tx_gas_limit,
1910 arb_tx_type,
1911 has_poster_costs,
1912 poster_gas,
1913 evm_gas_used,
1914 charged_multi_gas,
1915 gas_price_positive: self.arb_ctx.basefee > U256::ZERO,
1916 retry_context,
1917 });
1918
1919 Ok(output)
1920 }
1921
1922 fn commit_transaction(&mut self, output: Self::Result) -> Result<u64, BlockExecutionError> {
1923 let pending = self.pending_tx.take();
1925 let gas_used_total = output.result.result.gas_used();
1926 let success = matches!(&output.result.result, ExecutionResult::Success { .. });
1927
1928 let mut withdrawal_value = U256::ZERO;
1932 if let ExecutionResult::Success { ref logs, .. } = output.result.result {
1933 let arbsys_addr = arb_precompiles::ARBSYS_ADDRESS;
1934 let l2_to_l1_tx_topic = keccak256(
1935 b"L2ToL1Tx(address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes)",
1936 );
1937 for log in logs {
1938 if log.address == arbsys_addr
1939 && !log.data.topics().is_empty()
1940 && log.data.topics()[0] == l2_to_l1_tx_topic
1941 {
1942 if log.data.data.len() >= 160 {
1945 let callvalue = U256::from_be_slice(&log.data.data[128..160]);
1946 withdrawal_value = withdrawal_value.saturating_add(callvalue);
1947 let val_i128: i128 = callvalue.try_into().unwrap_or(i128::MAX);
1948 self.expected_balance_delta =
1949 self.expected_balance_delta.saturating_sub(val_i128);
1950 }
1951 }
1952 }
1953 }
1954
1955 for addr in output.result.state.keys() {
1957 self.touched_accounts.insert(*addr);
1958 }
1959
1960 let gas_used = self.inner.commit_transaction(output)?;
1962
1963 if !withdrawal_value.is_zero() {
1965 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
1966 burn_balance(db, arb_precompiles::ARBSYS_ADDRESS, withdrawal_value);
1967 self.touched_accounts
1968 .insert(arb_precompiles::ARBSYS_ADDRESS);
1969 }
1970
1971 let poster_gas_for_receipt = pending.as_ref().map_or(0, |p| p.poster_gas);
1973 self.gas_used_for_l1.push(poster_gas_for_receipt);
1974 let multi_gas_for_receipt = pending
1975 .as_ref()
1976 .map_or(MultiGas::zero(), |p| p.charged_multi_gas);
1977 self.multi_gas_used.push(multi_gas_for_receipt);
1978
1979 if let Some(pending) = pending {
1981 let is_retry = pending.retry_context.is_some();
1982
1983 debug_assert!(
1985 gas_used_total <= pending.tx_gas_limit,
1986 "gas_used ({gas_used_total}) exceeds gas_limit ({})",
1987 pending.tx_gas_limit
1988 );
1989
1990 let sender_extra_gas = gas_used_total.saturating_sub(pending.evm_gas_used);
1998 if sender_extra_gas > 0 {
1999 let extra_cost = self
2000 .arb_ctx
2001 .basefee
2002 .saturating_mul(U256::from(sender_extra_gas));
2003 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
2004 burn_balance(db, pending.sender, extra_cost);
2005 self.touched_accounts.insert(pending.sender);
2006 }
2007
2008 if let Some(retry_ctx) = pending.retry_context {
2009 let gas_left = pending.tx_gas_limit.saturating_sub(gas_used_total);
2011
2012 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
2013 let state_ptr: *mut State<DB> = db as *mut State<DB>;
2014 let touched_ptr =
2015 &mut self.touched_accounts as *mut std::collections::HashSet<Address>;
2016 let zombie_ptr =
2017 &mut self.zombie_accounts as *mut std::collections::HashSet<Address>;
2018 let finalise_ptr =
2019 &self.finalise_deleted as *const std::collections::HashSet<Address>;
2020 let arbos_ver = self.arb_ctx.arbos_version;
2021
2022 let multi_dimensional_cost = if self.arb_ctx.arbos_version
2024 >= arb_chainspec::arbos_version::ARBOS_VERSION_MULTI_GAS_CONSTRAINTS
2025 {
2026 ArbosState::open(state_ptr, SystemBurner::new(None, false))
2027 .ok()
2028 .and_then(|s| {
2029 s.l2_pricing_state
2030 .multi_dimensional_price_for_refund(pending.charged_multi_gas)
2031 .ok()
2032 })
2033 } else {
2034 None
2035 };
2036
2037 let result = self.arb_hooks.as_ref().map(|hooks| {
2038 hooks.tx_proc.end_tx_retryable(
2039 &EndTxRetryableParams {
2040 gas_left,
2041 gas_used: gas_used_total,
2042 effective_base_fee: self.arb_ctx.basefee,
2043 from: pending.sender,
2044 refund_to: retry_ctx.refund_to,
2045 max_refund: retry_ctx.max_refund,
2046 submission_fee_refund: retry_ctx.submission_fee_refund,
2047 ticket_id: retry_ctx.ticket_id,
2048 value: U256::ZERO, success,
2050 network_fee_account: self.arb_ctx.network_fee_account,
2051 infra_fee_account: self.arb_ctx.infra_fee_account,
2052 min_base_fee: self.arb_ctx.min_base_fee,
2053 arbos_version: self.arb_ctx.arbos_version,
2054 multi_dimensional_cost,
2055 block_base_fee: self.arb_ctx.basefee,
2056 },
2057 |addr, amount| unsafe {
2058 burn_balance(&mut *state_ptr, addr, amount);
2059 (*touched_ptr).insert(addr);
2060 },
2061 |from, to, amount| {
2062 unsafe {
2063 if amount.is_zero()
2064 && arbos_ver
2065 < arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS
2066 {
2067 create_zombie_if_deleted(
2068 &mut *state_ptr,
2069 from,
2070 &*finalise_ptr,
2071 &mut *zombie_ptr,
2072 &mut *touched_ptr,
2073 );
2074 }
2075 transfer_balance(&mut *state_ptr, from, to, amount);
2076 if !amount.is_zero() {
2079 (*zombie_ptr).remove(&from);
2080 }
2081 (*zombie_ptr).remove(&to);
2083 (*touched_ptr).insert(from);
2084 (*touched_ptr).insert(to);
2085 }
2086 Ok(())
2087 },
2088 )
2089 });
2090
2091 if let Some(ref result) = result {
2092 tracing::debug!(
2093 target: "arb::executor",
2094 ticket_id = %retry_ctx.ticket_id,
2095 should_delete = result.should_delete_retryable,
2096 "Retry EndTxHook result"
2097 );
2098 if result.should_delete_retryable {
2099 if let Ok(arb_state) =
2101 ArbosState::open(state_ptr, SystemBurner::new(None, false))
2102 {
2103 let _ = arb_state.retryable_state.delete_retryable(
2104 retry_ctx.ticket_id,
2105 |from, to, amount| {
2106 unsafe {
2107 if amount.is_zero()
2108 && arbos_ver
2109 < arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS
2110 {
2111 create_zombie_if_deleted(
2112 &mut *state_ptr,
2113 from,
2114 &*finalise_ptr,
2115 &mut *zombie_ptr,
2116 &mut *touched_ptr,
2117 );
2118 }
2119 transfer_balance(&mut *state_ptr, from, to, amount);
2120 if !amount.is_zero() {
2121 (*zombie_ptr).remove(&from);
2122 }
2123 (*zombie_ptr).remove(&to);
2124 (*touched_ptr).insert(from);
2125 (*touched_ptr).insert(to);
2126 }
2127 Ok(())
2128 },
2129 |addr| unsafe { get_balance(&mut *state_ptr, addr) },
2130 );
2131 }
2132 } else if result.should_return_value_to_escrow {
2133 unsafe {
2135 if retry_ctx.call_value.is_zero()
2136 && arbos_ver < arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS
2137 {
2138 create_zombie_if_deleted(
2139 &mut *state_ptr,
2140 pending.sender,
2141 &*finalise_ptr,
2142 &mut *zombie_ptr,
2143 &mut *touched_ptr,
2144 );
2145 }
2146 transfer_balance(
2147 &mut *state_ptr,
2148 pending.sender,
2149 result.escrow_address,
2150 retry_ctx.call_value,
2151 );
2152 if !retry_ctx.call_value.is_zero() {
2154 (*zombie_ptr).remove(&pending.sender);
2155 }
2156 (*zombie_ptr).remove(&result.escrow_address);
2158 (*touched_ptr).insert(pending.sender);
2159 (*touched_ptr).insert(result.escrow_address);
2160 }
2161 }
2162
2163 {
2166 if let Ok(arb_state) =
2168 ArbosState::open(state_ptr, SystemBurner::new(None, false))
2169 {
2170 let _ = arb_state.l2_pricing_state.grow_backlog(
2171 result.compute_gas_for_backlog,
2172 pending.charged_multi_gas,
2173 );
2174 if let Ok(b) = arb_state.l2_pricing_state.gas_backlog() {
2176 arb_precompiles::set_current_gas_backlog(b);
2177 }
2178 }
2179 }
2180 }
2181 } else if pending.has_poster_costs {
2182 let gas_left = pending.tx_gas_limit.saturating_sub(gas_used_total);
2185
2186 let fee_dist = self.arb_hooks.as_ref().map(|hooks| {
2187 hooks.compute_end_tx_fees(&EndTxContext {
2188 sender: pending.sender,
2189 gas_left,
2190 gas_used: gas_used_total,
2191 gas_price: self.arb_ctx.basefee,
2192 base_fee: self.arb_ctx.basefee,
2193 tx_type: pending.arb_tx_type.unwrap_or(ArbTxType::ArbitrumLegacyTx),
2194 success,
2195 refund_to: pending.sender,
2196 })
2197 });
2198
2199 if let Some(ref dist) = fee_dist {
2200 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
2201 apply_fee_distribution(db, dist, None);
2202 if !dist.network_fee_amount.is_zero() {
2205 self.touched_accounts.insert(dist.network_fee_account);
2206 }
2207 self.touched_accounts.insert(dist.infra_fee_account);
2208 self.touched_accounts.insert(dist.poster_fee_destination);
2209
2210 if self.arb_ctx.arbos_version
2213 >= arb_chainspec::arbos_version::ARBOS_VERSION_MULTI_GAS_CONSTRAINTS
2214 {
2215 let total_cost = self
2216 .arb_ctx
2217 .basefee
2218 .saturating_mul(U256::from(gas_used_total));
2219 let state_ptr: *mut State<DB> = db as *mut State<DB>;
2220 if let Ok(arb_state) =
2221 ArbosState::open(state_ptr, SystemBurner::new(None, false))
2222 {
2223 if let Ok(multi_cost) = arb_state
2224 .l2_pricing_state
2225 .multi_dimensional_price_for_refund(pending.charged_multi_gas)
2226 {
2227 if total_cost > multi_cost {
2228 let refund_amount = total_cost.saturating_sub(multi_cost);
2229 transfer_balance(
2230 db,
2231 dist.network_fee_account,
2232 pending.sender,
2233 refund_amount,
2234 );
2235 self.touched_accounts.insert(dist.network_fee_account);
2236 self.touched_accounts.insert(pending.sender);
2237 }
2238 }
2239 }
2240 }
2241
2242 let used_multi_gas = pending
2246 .charged_multi_gas
2247 .saturating_sub(MultiGas::l1_calldata_gas(pending.poster_gas));
2248
2249 let state_ptr: *mut State<DB> = db as *mut State<DB>;
2250 if let Ok(arb_state) =
2251 ArbosState::open(state_ptr, SystemBurner::new(None, false))
2252 {
2253 if pending.gas_price_positive {
2255 let _ = arb_state
2256 .l2_pricing_state
2257 .grow_backlog(dist.compute_gas_for_backlog, used_multi_gas);
2258 if let Ok(b) = arb_state.l2_pricing_state.gas_backlog() {
2260 arb_precompiles::set_current_gas_backlog(b);
2261 }
2262 }
2263 if !dist.l1_fees_to_add.is_zero() {
2264 let _ = arb_state
2265 .l1_pricing_state
2266 .add_to_l1_fees_available(dist.l1_fees_to_add);
2267 }
2268 } else {
2269 tracing::error!(
2270 target: "arb::backlog",
2271 "NormalTx: ArbosState::open FAILED for grow_backlog"
2272 );
2273 }
2274 }
2275 }
2276
2277 let mut adjusted_gas_used = gas_used_total;
2281 if self.arb_ctx.arbos_version
2282 >= arb_chainspec::arbos_version::ARBOS_VERSION_FIX_REDEEM_GAS
2283 {
2284 if let Some(hooks) = self.arb_hooks.as_ref() {
2285 for scheduled in &hooks.tx_proc.scheduled_txs {
2286 if let Some(retry_gas) = decode_retry_tx_gas(scheduled) {
2287 adjusted_gas_used = adjusted_gas_used.saturating_sub(retry_gas);
2288 }
2289 }
2290 }
2291 }
2292
2293 const TX_GAS: u64 = 21_000;
2295 let data_gas = pending.poster_gas;
2296 let compute_used = if adjusted_gas_used < data_gas {
2297 TX_GAS
2298 } else {
2299 let compute = adjusted_gas_used - data_gas;
2300 if compute < TX_GAS {
2301 TX_GAS
2302 } else {
2303 compute
2304 }
2305 };
2306 self.block_gas_left = self.block_gas_left.saturating_sub(compute_used);
2307
2308 let is_user_tx = !matches!(
2310 pending.arb_tx_type,
2311 Some(ArbTxType::ArbitrumInternalTx)
2312 | Some(ArbTxType::ArbitrumDepositTx)
2313 | Some(ArbTxType::ArbitrumSubmitRetryableTx)
2314 | Some(ArbTxType::ArbitrumRetryTx)
2315 );
2316 if is_user_tx {
2317 self.user_txs_processed += 1;
2318 }
2319
2320 let _ = is_retry; }
2322
2323 {
2328 use arb_precompiles::storage_slot::{
2329 current_redeemer_slot, current_retryable_slot, current_tx_poster_fee_slot,
2330 };
2331 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
2332 arb_storage::write_arbos_storage(db, current_tx_poster_fee_slot(), U256::ZERO);
2333 arb_storage::write_arbos_storage(db, current_retryable_slot(), U256::ZERO);
2334 arb_storage::write_arbos_storage(db, current_redeemer_slot(), U256::ZERO);
2335 }
2336
2337 {
2347 let keccak_empty = alloy_primitives::B256::from(alloy_primitives::keccak256([]));
2348 let db: &mut State<DB> = self.inner.evm_mut().db_mut();
2349 let to_remove: Vec<Address> = self
2350 .touched_accounts
2351 .drain()
2352 .filter(|addr| {
2353 if self.zombie_accounts.contains(addr) {
2355 return false;
2356 }
2357 if let Some(cached) = db.cache.accounts.get(addr) {
2358 if let Some(ref acct) = cached.account {
2359 let is_empty = acct.info.nonce == 0
2360 && acct.info.balance.is_zero()
2361 && acct.info.code_hash == keccak_empty;
2362 return is_empty;
2363 }
2364 }
2365 false
2366 })
2367 .collect();
2368
2369 for addr in &to_remove {
2377 if let Some(cached) = db.cache.accounts.get_mut(addr) {
2378 cached.account = None;
2379 cached.status = revm::database::states::AccountStatus::Destroyed;
2380 }
2381 }
2382 self.finalise_deleted.extend(to_remove);
2383 }
2384
2385 Ok(gas_used)
2386 }
2387
2388 fn finish(self) -> Result<(Self::Evm, BlockExecutionResult<R::Receipt>), BlockExecutionError> {
2389 if self.expected_balance_delta != 0 {
2391 tracing::trace!(
2392 target: "arb::executor",
2393 delta = self.expected_balance_delta,
2394 "expected balance delta from deposits/withdrawals"
2395 );
2396 }
2397 let mut result = BlockExecutionResult {
2401 receipts: self.inner.receipts,
2402 requests: Default::default(),
2403 gas_used: self.inner.gas_used,
2404 blob_gas_used: self.inner.blob_gas_used,
2405 };
2406 for (i, receipt) in result.receipts.iter_mut().enumerate() {
2408 if let Some(&l1_gas) = self.gas_used_for_l1.get(i) {
2409 arb_primitives::SetArbReceiptFields::set_gas_used_for_l1(receipt, l1_gas);
2410 }
2411 if let Some(&multi_gas) = self.multi_gas_used.get(i) {
2412 arb_primitives::SetArbReceiptFields::set_multi_gas_used(receipt, multi_gas);
2413 }
2414 }
2415 Ok((self.inner.evm, result))
2416 }
2417
2418 fn set_state_hook(&mut self, hook: Option<Box<dyn OnStateHook>>) {
2419 self.inner.set_state_hook(hook);
2420 }
2421
2422 fn evm_mut(&mut self) -> &mut Self::Evm {
2423 self.inner.evm_mut()
2424 }
2425
2426 fn evm(&self) -> &Self::Evm {
2427 self.inner.evm()
2428 }
2429
2430 fn receipts(&self) -> &[Self::Receipt] {
2431 self.inner.receipts()
2432 }
2433}
2434
2435fn adjust_result_gas_used<H>(result: &mut ExecutionResult<H>, extra_gas: u64) {
2444 match result {
2445 ExecutionResult::Success { gas_used, .. } => *gas_used = gas_used.saturating_add(extra_gas),
2446 ExecutionResult::Revert { gas_used, .. } => *gas_used = gas_used.saturating_add(extra_gas),
2447 ExecutionResult::Halt { gas_used, .. } => *gas_used = gas_used.saturating_add(extra_gas),
2448 }
2449}
2450
2451fn mint_balance<DB: Database>(state: &mut State<DB>, address: Address, amount: U256) {
2454 let _ = state.load_cache_account(address);
2455 if let Some(cache_acct) = state.cache.accounts.get_mut(&address) {
2456 if let Some(ref mut acct) = cache_acct.account {
2457 acct.info.balance = acct.info.balance.saturating_add(amount);
2458 } else {
2459 cache_acct.account = Some(revm_database::states::plain_account::PlainAccount {
2460 info: revm_state::AccountInfo {
2461 balance: amount,
2462 ..Default::default()
2463 },
2464 storage: Default::default(),
2465 });
2466 }
2467 }
2468}
2469
2470fn burn_balance<DB: Database>(state: &mut State<DB>, address: Address, amount: U256) {
2473 let _ = state.load_cache_account(address);
2474 if let Some(cache_acct) = state.cache.accounts.get_mut(&address) {
2475 if let Some(ref mut acct) = cache_acct.account {
2476 acct.info.balance = acct.info.balance.saturating_sub(amount);
2477 }
2478 }
2479}
2480
2481fn increment_nonce<DB: Database>(state: &mut State<DB>, address: Address) {
2485 let _ = state.load_cache_account(address);
2486 if let Some(cache_acct) = state.cache.accounts.get_mut(&address) {
2487 if let Some(ref mut acct) = cache_acct.account {
2488 acct.info.nonce += 1;
2489 }
2490 }
2491}
2492
2493fn get_balance<DB: Database>(state: &mut State<DB>, address: Address) -> U256 {
2495 match revm::Database::basic(state, address) {
2496 Ok(Some(info)) => info.balance,
2497 _ => U256::ZERO,
2498 }
2499}
2500
2501fn transfer_balance<DB: Database>(state: &mut State<DB>, from: Address, to: Address, amount: U256) {
2503 if amount.is_zero() {
2504 ensure_account_exists(state, from);
2505 ensure_account_exists(state, to);
2506 return;
2507 }
2508 let balance = get_balance(state, from);
2512 if balance < amount {
2513 tracing::warn!(
2514 target: "arb::executor",
2515 %from, %to, %amount, %balance,
2516 "transfer_balance: insufficient funds, skipping"
2517 );
2518 return;
2519 }
2520 burn_balance(state, from, amount);
2521 mint_balance(state, to, amount);
2522}
2523
2524fn ensure_account_exists<DB: Database>(state: &mut State<DB>, addr: Address) {
2527 let _ = state.load_cache_account(addr);
2528 if let Some(cached) = state.cache.accounts.get_mut(&addr) {
2529 if cached.account.is_none() {
2530 cached.account = Some(revm_database::states::plain_account::PlainAccount {
2531 info: revm_state::AccountInfo::default(),
2532 storage: Default::default(),
2533 });
2534 cached.status = revm_database::AccountStatus::InMemoryChange;
2535 }
2536 }
2537}
2538
2539fn create_zombie_if_deleted<DB: Database>(
2545 state: &mut State<DB>,
2546 addr: Address,
2547 finalise_deleted: &std::collections::HashSet<Address>,
2548 zombie_accounts: &mut std::collections::HashSet<Address>,
2549 touched_accounts: &mut std::collections::HashSet<Address>,
2550) {
2551 let _ = state.load_cache_account(addr);
2552 let account_missing = state
2553 .cache
2554 .accounts
2555 .get(&addr)
2556 .is_none_or(|c| c.account.is_none());
2557 if account_missing && finalise_deleted.contains(&addr) {
2558 if let Some(cached) = state.cache.accounts.get_mut(&addr) {
2559 cached.account = Some(revm_database::states::plain_account::PlainAccount {
2560 info: revm_state::AccountInfo::default(),
2561 storage: Default::default(),
2562 });
2563 cached.status = revm_database::AccountStatus::InMemoryChange;
2564 }
2565 zombie_accounts.insert(addr);
2566 touched_accounts.insert(addr);
2567 }
2568}
2569
2570fn try_transfer_balance<DB: Database>(
2573 state: &mut State<DB>,
2574 from: Address,
2575 to: Address,
2576 amount: U256,
2577) -> bool {
2578 if amount.is_zero() {
2579 ensure_account_exists(state, from);
2580 ensure_account_exists(state, to);
2581 return true;
2582 }
2583 if get_balance(state, from) < amount {
2584 return false;
2585 }
2586 burn_balance(state, from, amount);
2587 mint_balance(state, to, amount);
2588 true
2589}
2590
2591fn apply_fee_distribution<DB: Database>(
2593 state: &mut State<DB>,
2594 dist: &EndTxFeeDistribution,
2595 l1_pricing: Option<&l1_pricing::L1PricingState<DB>>,
2596) {
2597 if !dist.network_fee_amount.is_zero() {
2600 mint_balance(state, dist.network_fee_account, dist.network_fee_amount);
2601 }
2602 mint_balance(state, dist.infra_fee_account, dist.infra_fee_amount);
2603 mint_balance(state, dist.poster_fee_destination, dist.poster_fee_amount);
2604
2605 if !dist.l1_fees_to_add.is_zero() {
2606 if let Some(l1_state) = l1_pricing {
2607 let _ = l1_state.add_to_l1_fees_available(dist.l1_fees_to_add);
2608 }
2609 }
2610
2611 tracing::trace!(
2612 target: "arb::executor",
2613 network_fee = %dist.network_fee_amount,
2614 infra_fee = %dist.infra_fee_amount,
2615 poster_fee = %dist.poster_fee_amount,
2616 poster_dest = %dist.poster_fee_destination,
2617 l1_fees_added = %dist.l1_fees_to_add,
2618 backlog_gas = dist.compute_gas_for_backlog,
2619 "applied fee distribution"
2620 );
2621}
2622
2623fn estimate_intrinsic_gas(tx: &impl Transaction, spec: revm::primitives::hardfork::SpecId) -> u64 {
2629 const TX_GAS: u64 = 21_000;
2630 const TX_CREATE_GAS: u64 = 32_000;
2631 const TX_DATA_ZERO_GAS: u64 = 4;
2632 const TX_DATA_NON_ZERO_GAS: u64 = 16;
2633 const TX_ACCESS_LIST_ADDRESS_GAS: u64 = 2400;
2634 const TX_ACCESS_LIST_STORAGE_KEY_GAS: u64 = 1900;
2635 const INIT_CODE_WORD_GAS: u64 = 2;
2636
2637 let is_create = tx.to().is_none();
2638
2639 let mut gas = TX_GAS;
2640 if is_create {
2641 gas += TX_CREATE_GAS;
2642 }
2643
2644 let data = tx.input();
2645
2646 let data_gas: u64 = data
2648 .iter()
2649 .map(|&b| {
2650 if b == 0 {
2651 TX_DATA_ZERO_GAS
2652 } else {
2653 TX_DATA_NON_ZERO_GAS
2654 }
2655 })
2656 .sum();
2657 gas = gas.saturating_add(data_gas);
2658
2659 if let Some(access_list) = tx.access_list() {
2661 for item in access_list.iter() {
2662 gas = gas.saturating_add(TX_ACCESS_LIST_ADDRESS_GAS);
2663 gas = gas.saturating_add(
2664 (item.storage_keys.len() as u64).saturating_mul(TX_ACCESS_LIST_STORAGE_KEY_GAS),
2665 );
2666 }
2667 }
2668
2669 if spec.is_enabled_in(revm::primitives::hardfork::SpecId::SHANGHAI)
2671 && is_create
2672 && !data.is_empty()
2673 {
2674 let words = (data.len() as u64).div_ceil(32);
2675 gas = gas.saturating_add(words.saturating_mul(INIT_CODE_WORD_GAS));
2676 }
2677
2678 gas
2679}
2680
2681fn decode_extra_fields(extra_bytes: &[u8]) -> (u64, u64) {
2684 let delayed = if extra_bytes.len() >= 40 {
2685 let mut buf = [0u8; 8];
2686 buf.copy_from_slice(&extra_bytes[32..40]);
2687 u64::from_be_bytes(buf)
2688 } else {
2689 0
2690 };
2691 let l2_block = if extra_bytes.len() >= 48 {
2692 let mut buf = [0u8; 8];
2693 buf.copy_from_slice(&extra_bytes[40..48]);
2694 u64::from_be_bytes(buf)
2695 } else {
2696 0
2697 };
2698 (delayed, l2_block)
2699}
2700
2701fn process_parent_block_hash<DB: Database>(
2705 state: &mut State<DB>,
2706 l2_block_number: u64,
2707 prev_hash: B256,
2708) {
2709 use arb_primitives::arbos_versions::HISTORY_STORAGE_ADDRESS;
2710
2711 const HISTORY_SERVE_WINDOW: u64 = 393168;
2713
2714 if l2_block_number == 0 {
2715 return;
2716 }
2717
2718 let slot = U256::from((l2_block_number - 1) % HISTORY_SERVE_WINDOW);
2719 let value = U256::from_be_slice(prev_hash.as_slice());
2720
2721 arb_storage::write_storage_at(state, HISTORY_STORAGE_ADDRESS, slot, value);
2722}
2723
2724fn decode_retry_tx_gas(encoded: &[u8]) -> Option<u64> {
2728 if encoded.is_empty() {
2729 return None;
2730 }
2731 if encoded[0] != ArbTxType::ArbitrumRetryTx.as_u8() {
2732 tracing::warn!(
2733 target: "arb::executor",
2734 tx_type = encoded[0],
2735 "unexpected scheduled tx type"
2736 );
2737 return None;
2738 }
2739 let rlp_data = &encoded[1..];
2740 let retry =
2741 <arb_alloy_consensus::tx::ArbRetryTx as alloy_rlp::Decodable>::decode(&mut &rlp_data[..])
2742 .ok()?;
2743 Some(retry.gas)
2744}