arb_evm/
executor.rs

1use alloy_primitives::{Address, U256};
2use arb_primitives::multigas::MultiGas;
3use arbos::{
4    tx_processor::{
5        EndTxFeeDistribution, EndTxNormalParams, GasChargingError, GasChargingParams, TxProcessor,
6    },
7    util::tx_type_has_poster_costs,
8};
9
10use crate::hooks::{
11    ArbOsHooks, EndTxContext, GasChargingContext, GasChargingResult, StartTxContext,
12};
13
14/// Concrete ArbOS hooks implementation backed by `TxProcessor`.
15///
16/// Bridges the `ArbOsHooks` trait to the arbos crate's `TxProcessor` which
17/// contains the core fee accounting logic.
18#[derive(Debug)]
19pub struct DefaultArbOsHooks {
20    /// Per-transaction processor state.
21    pub tx_proc: TxProcessor,
22    /// Current ArbOS version.
23    pub arbos_version: u64,
24    /// Network fee account from ArbOS state.
25    pub network_fee_account: Address,
26    /// Infrastructure fee account from ArbOS state.
27    pub infra_fee_account: Address,
28    /// Minimum L2 base fee from L2 pricing state.
29    pub min_base_fee: U256,
30    /// Per-block gas limit from L2 pricing state.
31    pub per_block_gas_limit: u64,
32    /// Per-tx gas limit from L2 pricing state (ArbOS v50+).
33    pub per_tx_gas_limit: u64,
34    /// Block coinbase (poster address).
35    pub coinbase: Address,
36    /// Whether this is an eth_call (non-mutating).
37    pub is_eth_call: bool,
38    /// Cached L1 base fee for poster cost computation.
39    pub l1_base_fee: U256,
40    /// Whether calldata pricing increase feature is enabled (ArbOS >= 40 + feature flag).
41    pub calldata_pricing_increase_enabled: bool,
42    /// Whether tip collection is enabled (ArbOS >= 60 + state flag).
43    /// When true, the priority-fee tip is paid to coinbase rather than dropped.
44    pub collect_tips_enabled: bool,
45}
46
47impl DefaultArbOsHooks {
48    pub fn new(
49        coinbase: Address,
50        arbos_version: u64,
51        network_fee_account: Address,
52        infra_fee_account: Address,
53        min_base_fee: U256,
54        per_block_gas_limit: u64,
55        per_tx_gas_limit: u64,
56        is_eth_call: bool,
57        l1_base_fee: U256,
58        calldata_pricing_increase_enabled: bool,
59        collect_tips_enabled: bool,
60    ) -> Self {
61        Self {
62            tx_proc: TxProcessor::new(coinbase),
63            arbos_version,
64            network_fee_account,
65            infra_fee_account,
66            min_base_fee,
67            per_block_gas_limit,
68            per_tx_gas_limit,
69            coinbase,
70            is_eth_call,
71            l1_base_fee,
72            calldata_pricing_increase_enabled,
73            collect_tips_enabled,
74        }
75    }
76
77    /// Compute the end-of-tx fee distribution for a normal transaction.
78    pub fn compute_end_tx_fees(&self, ctx: &EndTxContext) -> EndTxFeeDistribution {
79        self.tx_proc
80            .compute_end_tx_fee_distribution(&EndTxNormalParams {
81                gas_used: ctx.gas_used,
82                gas_price: ctx.gas_price,
83                base_fee: ctx.base_fee,
84                coinbase: self.coinbase,
85                network_fee_account: self.network_fee_account,
86                infra_fee_account: self.infra_fee_account,
87                min_base_fee: self.min_base_fee,
88                arbos_version: self.arbos_version,
89            })
90    }
91}
92
93/// Error type for ArbOS hooks.
94#[derive(Debug, thiserror::Error)]
95pub enum ArbHookError {
96    #[error("gas charging: {0}")]
97    GasCharging(#[from] GasChargingError),
98}
99
100impl ArbOsHooks for DefaultArbOsHooks {
101    type Error = ArbHookError;
102
103    fn start_tx(&mut self, ctx: &StartTxContext) -> Result<(), Self::Error> {
104        self.tx_proc.set_tx_type(ctx.tx_type as u8);
105        Ok(())
106    }
107
108    fn gas_charging(&mut self, ctx: &GasChargingContext) -> Result<GasChargingResult, Self::Error> {
109        let mut gas_remaining = ctx.gas_limit.saturating_sub(ctx.intrinsic_gas);
110
111        let skip_l1_charging = !tx_type_has_poster_costs(ctx.tx_type.as_u8());
112
113        // Use the pre-computed poster cost from L1PricingState (brotli-based).
114        let poster_cost = if skip_l1_charging {
115            U256::ZERO
116        } else {
117            ctx.poster_cost
118        };
119
120        let params = GasChargingParams {
121            base_fee: ctx.base_fee,
122            poster_cost,
123            is_gas_estimation: self.is_eth_call,
124            is_eth_call: self.is_eth_call,
125            skip_l1_charging,
126            min_base_fee: self.min_base_fee,
127            per_block_gas_limit: self.per_block_gas_limit,
128            per_tx_gas_limit: self.per_tx_gas_limit,
129            arbos_version: self.arbos_version,
130        };
131
132        self.tx_proc
133            .gas_charging_hook(&mut gas_remaining, ctx.intrinsic_gas, &params)?;
134
135        // L1 calldata gas is tracked as a multi-gas dimension.
136        let multi_gas = MultiGas::single_dim_gas(self.tx_proc.poster_gas);
137
138        Ok(GasChargingResult {
139            poster_cost: self.tx_proc.poster_fee,
140            poster_gas: self.tx_proc.poster_gas,
141            compute_hold_gas: self.tx_proc.compute_hold_gas,
142            calldata_units: ctx.calldata_units,
143            multi_gas,
144        })
145    }
146
147    fn end_tx(&mut self, _ctx: &EndTxContext) -> Result<(), Self::Error> {
148        // Fee distribution and backlog update are handled by the block executor
149        // using compute_end_tx_fees(). The hooks trait just signals completion.
150        Ok(())
151    }
152
153    fn nonrefundable_gas(&self) -> u64 {
154        self.tx_proc.nonrefundable_gas()
155    }
156
157    fn held_gas(&self) -> u64 {
158        self.tx_proc.held_gas()
159    }
160
161    fn scheduled_txs(&mut self) -> Vec<Vec<u8>> {
162        core::mem::take(&mut self.tx_proc.scheduled_txs)
163    }
164
165    fn drop_tip(&self) -> bool {
166        self.tx_proc
167            .drop_tip_with_collect(self.arbos_version, self.collect_tips_enabled)
168    }
169
170    fn gas_price_op(&self, gas_price: U256, base_fee: U256) -> U256 {
171        self.tx_proc.gas_price_op_with_collect(
172            self.arbos_version,
173            base_fee,
174            gas_price,
175            self.collect_tips_enabled,
176        )
177    }
178
179    fn msg_is_non_mutating(&self) -> bool {
180        self.is_eth_call
181    }
182
183    fn is_calldata_pricing_increase_enabled(&self) -> bool {
184        self.calldata_pricing_increase_enabled
185    }
186}