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}
44
45impl DefaultArbOsHooks {
46    pub fn new(
47        coinbase: Address,
48        arbos_version: u64,
49        network_fee_account: Address,
50        infra_fee_account: Address,
51        min_base_fee: U256,
52        per_block_gas_limit: u64,
53        per_tx_gas_limit: u64,
54        is_eth_call: bool,
55        l1_base_fee: U256,
56        calldata_pricing_increase_enabled: bool,
57    ) -> Self {
58        Self {
59            tx_proc: TxProcessor::new(coinbase),
60            arbos_version,
61            network_fee_account,
62            infra_fee_account,
63            min_base_fee,
64            per_block_gas_limit,
65            per_tx_gas_limit,
66            coinbase,
67            is_eth_call,
68            l1_base_fee,
69            calldata_pricing_increase_enabled,
70        }
71    }
72
73    /// Compute the end-of-tx fee distribution for a normal transaction.
74    pub fn compute_end_tx_fees(&self, ctx: &EndTxContext) -> EndTxFeeDistribution {
75        self.tx_proc
76            .compute_end_tx_fee_distribution(&EndTxNormalParams {
77                gas_used: ctx.gas_used,
78                gas_price: ctx.gas_price,
79                base_fee: ctx.base_fee,
80                coinbase: self.coinbase,
81                network_fee_account: self.network_fee_account,
82                infra_fee_account: self.infra_fee_account,
83                min_base_fee: self.min_base_fee,
84                arbos_version: self.arbos_version,
85            })
86    }
87}
88
89/// Error type for ArbOS hooks.
90#[derive(Debug, thiserror::Error)]
91pub enum ArbHookError {
92    #[error("gas charging: {0}")]
93    GasCharging(#[from] GasChargingError),
94    #[error("state access: {0}")]
95    StateAccess(String),
96}
97
98impl ArbOsHooks for DefaultArbOsHooks {
99    type Error = ArbHookError;
100
101    fn start_tx(&mut self, ctx: &StartTxContext) -> Result<(), Self::Error> {
102        self.tx_proc.set_tx_type(ctx.tx_type as u8);
103        Ok(())
104    }
105
106    fn gas_charging(&mut self, ctx: &GasChargingContext) -> Result<GasChargingResult, Self::Error> {
107        let mut gas_remaining = ctx.gas_limit.saturating_sub(ctx.intrinsic_gas);
108
109        let skip_l1_charging = !tx_type_has_poster_costs(ctx.tx_type.as_u8());
110
111        // Use the pre-computed poster cost from L1PricingState (brotli-based).
112        let poster_cost = if skip_l1_charging {
113            U256::ZERO
114        } else {
115            ctx.poster_cost
116        };
117
118        let params = GasChargingParams {
119            base_fee: ctx.base_fee,
120            poster_cost,
121            is_gas_estimation: self.is_eth_call,
122            is_eth_call: self.is_eth_call,
123            skip_l1_charging,
124            min_base_fee: self.min_base_fee,
125            per_block_gas_limit: self.per_block_gas_limit,
126            per_tx_gas_limit: self.per_tx_gas_limit,
127            arbos_version: self.arbos_version,
128        };
129
130        self.tx_proc
131            .gas_charging_hook(&mut gas_remaining, ctx.intrinsic_gas, &params)?;
132
133        // L1 calldata gas is tracked as a multi-gas dimension.
134        let multi_gas = MultiGas::l1_calldata_gas(self.tx_proc.poster_gas);
135
136        Ok(GasChargingResult {
137            poster_cost: self.tx_proc.poster_fee,
138            poster_gas: self.tx_proc.poster_gas,
139            compute_hold_gas: self.tx_proc.compute_hold_gas,
140            calldata_units: ctx.calldata_units,
141            multi_gas,
142        })
143    }
144
145    fn end_tx(&mut self, _ctx: &EndTxContext) -> Result<(), Self::Error> {
146        // Fee distribution and backlog update are handled by the block executor
147        // using compute_end_tx_fees(). The hooks trait just signals completion.
148        Ok(())
149    }
150
151    fn nonrefundable_gas(&self) -> u64 {
152        self.tx_proc.nonrefundable_gas()
153    }
154
155    fn held_gas(&self) -> u64 {
156        self.tx_proc.held_gas()
157    }
158
159    fn scheduled_txs(&mut self) -> Vec<Vec<u8>> {
160        core::mem::take(&mut self.tx_proc.scheduled_txs)
161    }
162
163    fn drop_tip(&self) -> bool {
164        self.tx_proc.drop_tip(self.arbos_version)
165    }
166
167    fn gas_price_op(&self, gas_price: U256, base_fee: U256) -> U256 {
168        self.tx_proc
169            .gas_price_op(self.arbos_version, base_fee, gas_price)
170    }
171
172    fn msg_is_non_mutating(&self) -> bool {
173        self.is_eth_call
174    }
175
176    fn is_calldata_pricing_increase_enabled(&self) -> bool {
177        self.calldata_pricing_increase_enabled
178    }
179}