arb_evm/
executor.rs

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