arb_rpc/
api.rs

1//! Arbitrum EthApi wrapper with L1 gas estimation.
2//!
3//! Wraps reth's [`EthApiInner`] to override gas estimation
4//! with L1 posting cost awareness.
5
6use std::{sync::Arc, time::Duration};
7
8use alloy_primitives::{Address, B256, StorageKey, U256};
9use alloy_rpc_types_eth::{BlockId, state::StateOverride};
10use arb_storage::{
11    ARBOS_STATE_ADDRESS,
12    layout::{
13        BROTLI_COMPRESSION_LEVEL_OFFSET, CHAIN_ID_OFFSET, GENESIS_BLOCK_NUM_OFFSET,
14        L1_PRICING_SUBSPACE, L2_PRICING_SUBSPACE, root_slot, subspace_slot,
15    },
16};
17use reth_primitives_traits::{Recovered, WithEncoded};
18use reth_rpc::eth::core::EthApiInner;
19use reth_rpc_convert::{RpcConvert, RpcTxReq};
20use reth_rpc_eth_api::{
21    EthApiTypes, FromEvmError, RpcNodeCore, RpcNodeCoreExt,
22    helpers::{
23        Call, EthApiSpec, EthBlocks, EthCall, EthFees, EthSigner, EthState, EthTransactions,
24        GetBlockAccessList, LoadBlock, LoadFee, LoadPendingBlock, LoadReceipt, LoadState,
25        LoadTransaction, SpawnBlocking, Trace, estimate::EstimateCall,
26        pending_block::PendingEnvBuilder,
27    },
28};
29use reth_rpc_eth_types::{
30    EthApiError, EthStateCache, FeeHistoryCache, GasPriceOracle, PendingBlock,
31    builder::config::PendingBlockKind,
32};
33use reth_storage_api::{ProviderHeader, StateProviderFactory, TransactionsProvider};
34use reth_tasks::{
35    Runtime,
36    pool::{BlockingTaskGuard, BlockingTaskPool},
37};
38use reth_transaction_pool::{
39    AddedTransactionOutcome, PoolPooledTx, PoolTransaction, TransactionOrigin, TransactionPool,
40};
41use tracing::trace;
42
43/// Type alias matching reth's `SignersForRpc`.
44type SignersForRpc<Provider, Rpc> = parking_lot::RwLock<
45    Vec<Box<dyn EthSigner<<Provider as TransactionsProvider>::Transaction, RpcTxReq<Rpc>>>>,
46>;
47
48use arbos::{
49    l1_pricing::{
50        PRICE_PER_UNIT_OFFSET as L1_PRICE_PER_UNIT, UNITS_SINCE_OFFSET as L1_UNITS_SINCE_UPDATE,
51    },
52    l2_pricing::{BASE_FEE_WEI_OFFSET as L2_BASE_FEE, MIN_BASE_FEE_WEI_OFFSET as L2_MIN_BASE_FEE},
53};
54
55/// Non-zero calldata gas cost per byte (EIP-2028).
56const TX_DATA_NON_ZERO_GAS: u64 = 16;
57
58/// Padding applied to L1 fee estimates (110% = 11000 bips).
59const GAS_ESTIMATION_L1_PRICE_PADDING: u64 = 11000;
60
61/// Selector for `ArbGasInfo.getCurrentTxL1GasFees()`.
62const SEL_GET_CURRENT_TX_L1_FEES: &[u8] = &[0xc6, 0xf7, 0xde, 0x0e];
63
64/// Apply Arbitrum's L1→L2 address aliasing (offset by
65/// 0x1111000000000000000000000000000000001111).
66fn apply_l1_to_l2_alias(addr: Address) -> Address {
67    let mut offset = [0u8; 32];
68    offset[12] = 0x11;
69    offset[13] = 0x11;
70    offset[30] = 0x11;
71    offset[31] = 0x11;
72    let lhs = U256::from_be_slice(addr.as_slice());
73    let rhs = U256::from_be_bytes(offset);
74    let sum = lhs.wrapping_add(rhs);
75    let bytes = sum.to_be_bytes::<32>();
76    Address::from_slice(&bytes[12..32])
77}
78
79/// Selector for `ArbGasInfo.getL1PricingUnitsSinceUpdate()`.
80const SEL_GET_L1_PRICING_UNITS_SINCE_UPDATE: &[u8] = &[0xef, 0xf0, 0x13, 0x06];
81
82/// Arbitrum Eth API wrapping the standard reth EthApiInner.
83///
84/// This wrapper overrides gas estimation to add L1 posting costs.
85pub struct ArbEthApi<N: RpcNodeCore, Rpc: RpcConvert> {
86    inner: Arc<EthApiInner<N, Rpc>>,
87}
88
89impl<N: RpcNodeCore, Rpc: RpcConvert> Clone for ArbEthApi<N, Rpc> {
90    fn clone(&self) -> Self {
91        Self {
92            inner: self.inner.clone(),
93        }
94    }
95}
96
97impl<N: RpcNodeCore, Rpc: RpcConvert> std::fmt::Debug for ArbEthApi<N, Rpc> {
98    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99        f.debug_struct("ArbEthApi").finish_non_exhaustive()
100    }
101}
102
103impl<N: RpcNodeCore, Rpc: RpcConvert> ArbEthApi<N, Rpc> {
104    /// Create a new `ArbEthApi` wrapping the given inner.
105    pub fn new(inner: EthApiInner<N, Rpc>) -> Self {
106        Self {
107            inner: Arc::new(inner),
108        }
109    }
110}
111
112impl<N, Rpc> ArbEthApi<N, Rpc>
113where
114    N: RpcNodeCore<Provider: StateProviderFactory>,
115    Rpc: RpcConvert,
116{
117    /// Compute L1 posting gas for gas estimation.
118    ///
119    /// Reads L1 pricing state from ArbOS to estimate the gas needed to cover
120    /// L1 data posting costs for the given calldata length.
121    fn l1_posting_gas(&self, calldata_len: usize, at: BlockId) -> Result<u64, EthApiError> {
122        if calldata_len == 0 {
123            return Ok(0);
124        }
125
126        let state = self
127            .inner
128            .provider()
129            .state_by_block_id(at)
130            .map_err(|e| EthApiError::Internal(e.into()))?;
131
132        let l1_price_slot = subspace_slot(L1_PRICING_SUBSPACE, L1_PRICE_PER_UNIT);
133        let l1_price = state
134            .storage(
135                ARBOS_STATE_ADDRESS,
136                StorageKey::from(B256::from(l1_price_slot.to_be_bytes::<32>())),
137            )
138            .map_err(|e| EthApiError::Internal(e.into()))?
139            .unwrap_or_default();
140
141        let basefee_slot = subspace_slot(L2_PRICING_SUBSPACE, L2_BASE_FEE);
142        let basefee = state
143            .storage(
144                ARBOS_STATE_ADDRESS,
145                StorageKey::from(B256::from(basefee_slot.to_be_bytes::<32>())),
146            )
147            .map_err(|e| EthApiError::Internal(e.into()))?
148            .unwrap_or_default();
149
150        if l1_price.is_zero() || basefee.is_zero() {
151            return Ok(0);
152        }
153
154        // L1 fee = l1_price * calldata_bytes * TX_DATA_NON_ZERO_GAS
155        let l1_fee = l1_price
156            .saturating_mul(U256::from(TX_DATA_NON_ZERO_GAS))
157            .saturating_mul(U256::from(calldata_len));
158
159        // Apply 110% padding for L1 price volatility.
160        let padded = l1_fee.saturating_mul(U256::from(GAS_ESTIMATION_L1_PRICE_PADDING))
161            / U256::from(10000u64);
162
163        // Use 7/8 of basefee as congestion discount for estimation.
164        let adjusted_basefee = basefee.saturating_mul(U256::from(7)) / U256::from(8);
165        let adjusted_basefee = if adjusted_basefee.is_zero() {
166            U256::from(1)
167        } else {
168            adjusted_basefee
169        };
170
171        // Convert to gas units: posting_gas = padded_fee / adjusted_basefee
172        let gas = padded / adjusted_basefee;
173        Ok(gas.try_into().unwrap_or(u64::MAX))
174    }
175}
176
177impl<N, Rpc> ArbEthApi<N, Rpc>
178where
179    N: RpcNodeCore<Provider: StateProviderFactory>,
180    EthApiError: FromEvmError<N::Evm>,
181    Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError, Evm = N::Evm>,
182    RpcTxReq<<Rpc as RpcConvert>::Network>: AsRef<alloy_rpc_types_eth::TransactionRequest>
183        + AsMut<alloy_rpc_types_eth::TransactionRequest>
184        + Clone
185        + Default,
186{
187    fn compute_eth_call_units_since_update(
188        &self,
189        request: RpcTxReq<<Rpc as RpcConvert>::Network>,
190        at: BlockId,
191    ) -> Result<alloy_primitives::Bytes, EthApiError> {
192        let inner = request.as_ref();
193        let (to, contract_creation) = match inner.to {
194            Some(alloy_primitives::TxKind::Call(addr)) => (addr, false),
195            Some(alloy_primitives::TxKind::Create) => (Address::ZERO, true),
196            None => (Address::ZERO, false),
197        };
198        let value = inner.value.unwrap_or(U256::ZERO);
199        let data: alloy_primitives::Bytes = inner.input.input().cloned().unwrap_or_default();
200
201        let state = self
202            .inner
203            .provider()
204            .state_by_block_id(at)
205            .map_err(|e| EthApiError::Internal(e.into()))?;
206        let read = |slot: U256| -> Result<U256, EthApiError> {
207            Ok(state
208                .storage(
209                    ARBOS_STATE_ADDRESS,
210                    StorageKey::from(B256::from(slot.to_be_bytes::<32>())),
211                )
212                .map_err(|e| EthApiError::Internal(e.into()))?
213                .unwrap_or_default())
214        };
215        let stored = read(subspace_slot(L1_PRICING_SUBSPACE, L1_UNITS_SINCE_UPDATE))?;
216        let chain_id_u: u64 = read(root_slot(CHAIN_ID_OFFSET))?.try_into().unwrap_or(0);
217        let brotli_level: u64 = read(root_slot(BROTLI_COMPRESSION_LEVEL_OFFSET))?
218            .try_into()
219            .unwrap_or(0);
220
221        let tx_bytes =
222            arb_precompiles::build_fake_tx_bytes(chain_id_u, to, contract_creation, value, data);
223        let raw_units = arbos::l1_pricing::poster_units_from_bytes(&tx_bytes, brotli_level);
224        let padded_units = raw_units
225            .saturating_add(arbos::l1_pricing::ESTIMATION_PADDING_UNITS)
226            .saturating_mul(10_000 + arbos::l1_pricing::ESTIMATION_PADDING_BASIS_POINTS)
227            / 10_000;
228        let total = stored.saturating_add(U256::from(padded_units));
229
230        Ok(alloy_primitives::Bytes::from(
231            total.to_be_bytes::<32>().to_vec(),
232        ))
233    }
234
235    fn compute_eth_call_current_tx_l1_fees(
236        &self,
237        request: RpcTxReq<<Rpc as RpcConvert>::Network>,
238        at: BlockId,
239    ) -> Result<alloy_primitives::Bytes, EthApiError> {
240        let inner = request.as_ref();
241        let (to, contract_creation) = match inner.to {
242            Some(alloy_primitives::TxKind::Call(addr)) => (addr, false),
243            Some(alloy_primitives::TxKind::Create) => (Address::ZERO, true),
244            None => (Address::ZERO, false),
245        };
246        let value = inner.value.unwrap_or(U256::ZERO);
247        let data: alloy_primitives::Bytes = inner.input.input().cloned().unwrap_or_default();
248
249        let state = self
250            .inner
251            .provider()
252            .state_by_block_id(at)
253            .map_err(|e| EthApiError::Internal(e.into()))?;
254        let read = |slot: U256| -> Result<U256, EthApiError> {
255            Ok(state
256                .storage(
257                    ARBOS_STATE_ADDRESS,
258                    StorageKey::from(B256::from(slot.to_be_bytes::<32>())),
259                )
260                .map_err(|e| EthApiError::Internal(e.into()))?
261                .unwrap_or_default())
262        };
263        let l1_price = read(subspace_slot(L1_PRICING_SUBSPACE, L1_PRICE_PER_UNIT))?;
264        if l1_price.is_zero() {
265            return Ok(alloy_primitives::Bytes::from(vec![0u8; 32]));
266        }
267        let chain_id_u: u64 = read(root_slot(CHAIN_ID_OFFSET))?.try_into().unwrap_or(0);
268        let brotli_level: u64 = read(root_slot(BROTLI_COMPRESSION_LEVEL_OFFSET))?
269            .try_into()
270            .unwrap_or(0);
271
272        let tx_bytes =
273            arb_precompiles::build_fake_tx_bytes(chain_id_u, to, contract_creation, value, data);
274        let raw_units = arbos::l1_pricing::poster_units_from_bytes(&tx_bytes, brotli_level);
275        let padded_units = raw_units
276            .saturating_add(arbos::l1_pricing::ESTIMATION_PADDING_UNITS)
277            .saturating_mul(10_000 + arbos::l1_pricing::ESTIMATION_PADDING_BASIS_POINTS)
278            / 10_000;
279        let poster_fee = l1_price.saturating_mul(U256::from(padded_units));
280
281        Ok(alloy_primitives::Bytes::from(
282            poster_fee.to_be_bytes::<32>().to_vec(),
283        ))
284    }
285
286    /// Combined gas estimator matching Arbitrum's `DoEstimateGas`: the
287    /// binary search operates on total gas (L2 compute + L1 poster) so the
288    /// 64/63 optimistic multiplier and 0.015 error ratio apply to the
289    /// combined figure. Each simulation passes `total − l1_gas` as the L2
290    /// gas limit so the poster-gas deduction is accounted for.
291    async fn estimate_arb_combined_gas(
292        &self,
293        inner_req: RpcTxReq<<Rpc as RpcConvert>::Network>,
294        gas_for_l1: u64,
295        at: BlockId,
296        state_override: Option<StateOverride>,
297    ) -> Result<u64, EthApiError>
298    where
299        RpcTxReq<<Rpc as RpcConvert>::Network>: From<alloy_rpc_types_eth::TransactionRequest>,
300    {
301        use alloy_rpc_types_eth::state::EvmOverrides;
302
303        const CALL_STIPEND: u64 = 2300;
304        const ERROR_RATIO: f64 = 0.015;
305
306        let rpc_gas_cap = self.call_gas_limit();
307
308        let simulate = async |req_gas: u64| -> Result<(u64, bool), EthApiError> {
309            let mut req = inner_req.clone();
310            req.as_mut().gas = Some(req_gas);
311            let _permit = self.acquire_owned_blocking_io().await;
312            let res = self
313                .transact_call_at(req, at, EvmOverrides::state(state_override.clone()))
314                .await?;
315            Ok((res.result.gas_used(), res.result.is_success()))
316        };
317
318        let compute_cap = rpc_gas_cap.saturating_sub(gas_for_l1).max(1);
319        let (used_compute, success_first) = simulate(compute_cap).await?;
320        if !success_first {
321            return Ok(rpc_gas_cap);
322        }
323
324        let used_total = used_compute.saturating_add(gas_for_l1);
325        let mut lo = used_total.saturating_sub(1);
326        let mut hi = rpc_gas_cap.max(used_total);
327
328        let optimistic = used_total.saturating_add(CALL_STIPEND).saturating_mul(64) / 63;
329        if optimistic < hi {
330            let compute_limit = optimistic.saturating_sub(gas_for_l1);
331            let (_, ok) = simulate(compute_limit).await?;
332            if ok {
333                hi = optimistic;
334            } else {
335                lo = optimistic;
336            }
337        }
338
339        while lo + 1 < hi {
340            let ratio = (hi - lo) as f64 / hi as f64;
341            if ratio < ERROR_RATIO {
342                break;
343            }
344            let mut mid = lo + (hi - lo) / 2;
345            let two_lo = lo.saturating_mul(2);
346            if mid > two_lo {
347                mid = two_lo;
348            }
349            let compute_limit = mid.saturating_sub(gas_for_l1);
350            let (_, ok) = simulate(compute_limit).await?;
351            if ok {
352                hi = mid;
353            } else {
354                lo = mid;
355            }
356        }
357
358        Ok(hi)
359    }
360
361    /// Gas estimate for `NodeInterface.estimateRetryableTicket` —
362    /// `submit_intrinsic + auto_redeem_gas`.
363    async fn estimate_retryable_ticket_gas(
364        &self,
365        input: &alloy_primitives::Bytes,
366        at: BlockId,
367        state_override: Option<StateOverride>,
368    ) -> Result<U256, EthApiError>
369    where
370        RpcTxReq<<Rpc as RpcConvert>::Network>: From<alloy_rpc_types_eth::TransactionRequest>,
371    {
372        use alloy_primitives::{Bytes, TxKind};
373        use alloy_rpc_types_eth::TransactionRequest;
374
375        // ABI decode: selector(4) + 7 heads(32 each) + bytes tail.
376        // sender, deposit, to, l2CallValue, excessFeeRefundAddr,
377        // callValueRefundAddr, <bytes data offset>.
378        const HEAD_LEN: usize = 4 + 32 * 7;
379        if input.len() < HEAD_LEN {
380            return Err(EthApiError::InvalidParams(
381                "estimateRetryableTicket: calldata too short".into(),
382            ));
383        }
384        let sender = Address::from_slice(&input[4 + 12..4 + 32]);
385        let _deposit = U256::from_be_slice(&input[36..68]);
386        let to_word = &input[68..100];
387        let to = Address::from_slice(&to_word[12..32]);
388        let l2_call_value = U256::from_be_slice(&input[100..132]);
389        let _excess_fee_refund = Address::from_slice(&input[132 + 12..132 + 32]);
390        let _call_value_refund = Address::from_slice(&input[164 + 12..164 + 32]);
391        let data_offset: usize =
392            U256::from_be_slice(&input[196..228])
393                .try_into()
394                .map_err(|_| {
395                    EthApiError::InvalidParams(
396                        "estimateRetryableTicket: invalid data offset".into(),
397                    )
398                })?;
399        let abi_body = &input[4..];
400        let data: Bytes = if data_offset + 32 <= abi_body.len() {
401            let len: usize = U256::from_be_slice(&abi_body[data_offset..data_offset + 32])
402                .try_into()
403                .map_err(|_| {
404                    EthApiError::InvalidParams(
405                        "estimateRetryableTicket: data length too large".into(),
406                    )
407                })?;
408            if data_offset + 32 + len > abi_body.len() {
409                return Err(EthApiError::InvalidParams(
410                    "estimateRetryableTicket: data out of bounds".into(),
411                ));
412            }
413            Bytes::copy_from_slice(&abi_body[data_offset + 32..data_offset + 32 + len])
414        } else {
415            Bytes::new()
416        };
417
418        // `to == zero` means the retryable is a contract-create — map
419        // that to TxKind::Create for the estimate request.
420        let kind = if to == Address::ZERO {
421            TxKind::Create
422        } else {
423            TxKind::Call(to)
424        };
425        let data_ref = data.clone();
426        let equivalent = TransactionRequest {
427            from: Some(sender),
428            to: Some(kind),
429            value: Some(l2_call_value),
430            input: data.into(),
431            ..Default::default()
432        };
433        let equivalent_req: RpcTxReq<<Rpc as RpcConvert>::Network> = equivalent.into();
434
435        // Binary-search the auto-redeem gas via the standard eth
436        // estimation machinery. The equivalent call has the exact
437        // same state transitions as what the auto-redeem runs, so
438        // its gas result is the auto-redeem's gas 1:1.
439        let redeem_gas =
440            EstimateCall::estimate_gas_at(self, equivalent_req, at, state_override).await?;
441
442        // Submit-retryable intrinsic gas matches the default
443        // IntrinsicGas for ArbitrumSubmitRetryableTx: 21,000 tx base +
444        // EIP-2028 calldata (16 × non-zero + 4 × zero). ArbOS's
445        // state-transition overhead for retryable creation, escrow,
446        // and auto-redeem scheduling is charged inside the auto-redeem
447        // itself and is therefore already captured by `redeem_gas`.
448        let (zeros, non_zeros) =
449            data_ref.iter().fold(
450                (0u64, 0u64),
451                |(z, nz), &b| if b == 0 { (z + 1, nz) } else { (z, nz + 1) },
452            );
453        let calldata_gas = zeros
454            .saturating_mul(4)
455            .saturating_add(non_zeros.saturating_mul(16));
456        let submit_intrinsic = 21_000u64.saturating_add(calldata_gas);
457
458        Ok(redeem_gas.saturating_add(U256::from(submit_intrinsic)))
459    }
460
461    /// `eth_call` of `NodeInterface.estimateRetryableTicket(...)`:
462    /// synthesize the ArbitrumSubmitRetryableTx that corresponds to this
463    /// call and return its EIP-2718 envelope hash (the ticket ID).
464    async fn simulate_retryable_ticket_call(
465        &self,
466        input: &alloy_primitives::Bytes,
467        at: BlockId,
468        _overrides: alloy_rpc_types_eth::state::EvmOverrides,
469    ) -> Result<alloy_primitives::Bytes, EthApiError>
470    where
471        RpcTxReq<<Rpc as RpcConvert>::Network>: From<alloy_rpc_types_eth::TransactionRequest>,
472    {
473        use alloy_primitives::{Bytes, keccak256};
474        use arb_alloy_consensus::{ArbTxEnvelope, tx::ArbSubmitRetryableTx};
475
476        const HEAD_LEN: usize = 4 + 32 * 7;
477        if input.len() < HEAD_LEN {
478            return Err(EthApiError::InvalidParams(
479                "estimateRetryableTicket: calldata too short".into(),
480            ));
481        }
482        let sender = Address::from_slice(&input[4 + 12..4 + 32]);
483        let deposit = U256::from_be_slice(&input[36..68]);
484        let to = Address::from_slice(&input[68 + 12..100]);
485        let l2_call_value = U256::from_be_slice(&input[100..132]);
486        let excess_fee_refund = Address::from_slice(&input[132 + 12..164]);
487        let call_value_refund = Address::from_slice(&input[164 + 12..196]);
488        let data_offset: usize =
489            U256::from_be_slice(&input[196..228])
490                .try_into()
491                .map_err(|_| {
492                    EthApiError::InvalidParams(
493                        "estimateRetryableTicket: invalid data offset".into(),
494                    )
495                })?;
496        let abi_body = &input[4..];
497        let data: Bytes = if data_offset + 32 <= abi_body.len() {
498            let len: usize = U256::from_be_slice(&abi_body[data_offset..data_offset + 32])
499                .try_into()
500                .map_err(|_| {
501                    EthApiError::InvalidParams(
502                        "estimateRetryableTicket: data length too large".into(),
503                    )
504                })?;
505            if data_offset + 32 + len > abi_body.len() {
506                return Err(EthApiError::InvalidParams(
507                    "estimateRetryableTicket: data out of bounds".into(),
508                ));
509            }
510            Bytes::copy_from_slice(&abi_body[data_offset + 32..data_offset + 32 + len])
511        } else {
512            Bytes::new()
513        };
514
515        let l1_base_fee = {
516            let state = self
517                .inner
518                .provider()
519                .state_by_block_id(at)
520                .map_err(|e| EthApiError::Internal(e.into()))?;
521            let slot = subspace_slot(L1_PRICING_SUBSPACE, L1_PRICE_PER_UNIT);
522            state
523                .storage(
524                    ARBOS_STATE_ADDRESS,
525                    StorageKey::from(B256::from(slot.to_be_bytes::<32>())),
526                )
527                .map_err(|e| EthApiError::Internal(e.into()))?
528                .unwrap_or_default()
529        };
530
531        let aliased_from = apply_l1_to_l2_alias(sender);
532        let max_submission_fee =
533            arbos::retryables::retryable_submission_fee(data.len(), l1_base_fee);
534        let retry_to = if to == Address::ZERO { None } else { Some(to) };
535
536        let gas_cap = self.inner.gas_cap();
537        let gas = gas_cap;
538
539        let tx = ArbSubmitRetryableTx {
540            chain_id: U256::ZERO,
541            request_id: B256::ZERO,
542            from: aliased_from,
543            l1_base_fee,
544            deposit_value: deposit,
545            gas_fee_cap: U256::ZERO,
546            gas,
547            retry_to,
548            retry_value: l2_call_value,
549            beneficiary: call_value_refund,
550            max_submission_fee,
551            fee_refund_addr: excess_fee_refund,
552            retry_data: data,
553        };
554        let envelope = ArbTxEnvelope::SubmitRetryable(tx);
555        let encoded = envelope.encode_typed();
556        let hash = keccak256(&encoded);
557        Ok(alloy_primitives::Bytes::from(hash.0.to_vec()))
558    }
559
560    /// Handle `eth_call` dispatch of
561    /// `NodeInterfaceDebug.getRetryable(bytes32 ticketId)` — reads the
562    /// retryable record from storage and returns the 7-tuple
563    /// `(timeout, from, to, value, beneficiary, tries, data)`.
564    async fn get_retryable_abi(
565        &self,
566        input: &alloy_primitives::Bytes,
567        at: BlockId,
568    ) -> Result<alloy_primitives::Bytes, EthApiError> {
569        use arb_storage::layout::{ROOT_STORAGE_KEY, derive_subspace_key, map_slot};
570        use arbos::retryables::{
571            BENEFICIARY_OFFSET, CALLDATA_KEY, CALLVALUE_OFFSET, FROM_OFFSET, NUM_TRIES_OFFSET,
572            TIMEOUT_OFFSET, TO_OFFSET,
573        };
574
575        if input.len() < 4 + 32 {
576            return Err(EthApiError::InvalidParams(
577                "getRetryable: expected bytes32 ticket".into(),
578            ));
579        }
580        let ticket = B256::from_slice(&input[4..36]);
581
582        let state = self
583            .inner
584            .provider()
585            .state_by_block_id(at)
586            .map_err(|e| EthApiError::Internal(e.into()))?;
587        let load = |slot: U256| -> Result<U256, EthApiError> {
588            let k = StorageKey::from(B256::from(slot.to_be_bytes::<32>()));
589            Ok(state
590                .storage(ARBOS_STATE_ADDRESS, k)
591                .map_err(|e| EthApiError::Internal(e.into()))?
592                .unwrap_or(U256::ZERO))
593        };
594
595        let retryables_key =
596            derive_subspace_key(ROOT_STORAGE_KEY, arb_storage::layout::RETRYABLES_SUBSPACE);
597        let r_key = derive_subspace_key(retryables_key.as_slice(), ticket.as_slice());
598
599        let timeout: u64 = load(map_slot(r_key.as_slice(), TIMEOUT_OFFSET))?
600            .try_into()
601            .unwrap_or(0);
602        if timeout == 0 {
603            return Err(EthApiError::InvalidParams(format!(
604                "no retryable with id 0x{ticket:x}"
605            )));
606        }
607        let from_word = load(map_slot(r_key.as_slice(), FROM_OFFSET))?;
608        let from = Address::from_slice(&from_word.to_be_bytes::<32>()[12..]);
609        let to_word = load(map_slot(r_key.as_slice(), TO_OFFSET))?;
610        let to_bytes: [u8; 32] = to_word.to_be_bytes();
611        // StorageBackedAddressOrNil uses all-ones in the high 12 bytes
612        // to encode Nil; actual encoding varies, so just take low 20
613        // bytes and let callers treat zero as nil.
614        let to = Address::from_slice(&to_bytes[12..]);
615        let value = load(map_slot(r_key.as_slice(), CALLVALUE_OFFSET))?;
616        let beneficiary_word = load(map_slot(r_key.as_slice(), BENEFICIARY_OFFSET))?;
617        let beneficiary = Address::from_slice(&beneficiary_word.to_be_bytes::<32>()[12..]);
618        let tries: u64 = load(map_slot(r_key.as_slice(), NUM_TRIES_OFFSET))?
619            .try_into()
620            .unwrap_or(0);
621
622        // Calldata lives under its own subspace with StorageBackedBytes
623        // layout: slot 0 = size, slot 1+ = chunks. We read the size,
624        // then each 32-byte chunk, and truncate.
625        let cd_key = derive_subspace_key(r_key.as_slice(), CALLDATA_KEY);
626        let size: usize = load(map_slot(cd_key.as_slice(), 0))?
627            .try_into()
628            .unwrap_or(0);
629        let chunks = size.div_ceil(32);
630        let mut data = Vec::with_capacity(size);
631        for i in 0..chunks {
632            let chunk = load(map_slot(cd_key.as_slice(), 1 + i as u64))?;
633            data.extend_from_slice(&chunk.to_be_bytes::<32>());
634        }
635        data.truncate(size);
636
637        // ABI-encode the 7-tuple:
638        //   head (7 × 32):
639        //     timeout, from, to, value, beneficiary, tries, data_offset
640        //   tail: data_len, data_bytes (padded to 32)
641        let mut out = vec![0u8; 7 * 32];
642        U256::from(timeout)
643            .to_be_bytes::<32>()
644            .iter()
645            .enumerate()
646            .for_each(|(i, b)| out[i] = *b);
647        out[32 + 12..32 + 32].copy_from_slice(from.as_slice());
648        out[64 + 12..64 + 32].copy_from_slice(to.as_slice());
649        out[96..128].copy_from_slice(&value.to_be_bytes::<32>());
650        out[128 + 12..128 + 32].copy_from_slice(beneficiary.as_slice());
651        U256::from(tries)
652            .to_be_bytes::<32>()
653            .iter()
654            .enumerate()
655            .for_each(|(i, b)| out[160 + i] = *b);
656        // data offset = 0xe0 (7 × 32).
657        U256::from(7u64 * 32)
658            .to_be_bytes::<32>()
659            .iter()
660            .enumerate()
661            .for_each(|(i, b)| out[192 + i] = *b);
662        // Tail.
663        let padded_len = size.div_ceil(32) * 32;
664        let mut tail = vec![0u8; 32 + padded_len];
665        U256::from(size as u64)
666            .to_be_bytes::<32>()
667            .iter()
668            .enumerate()
669            .for_each(|(i, b)| tail[i] = *b);
670        tail[32..32 + size].copy_from_slice(&data);
671        out.extend_from_slice(&tail);
672        Ok(alloy_primitives::Bytes::from(out))
673    }
674
675    /// Handle `eth_call` dispatch of
676    /// `NodeInterface.constructOutboxProof(size, leaf)`. Scans ArbSys
677    /// (0x64) L2ToL1Tx / SendMerkleUpdate event logs over the chain up
678    /// to `at` to resolve every node hash the proof walk needs, then
679    /// feeds the map to `outbox_proof::finalize_proof`.
680    async fn construct_outbox_proof(
681        &self,
682        input: &alloy_primitives::Bytes,
683        at: BlockId,
684    ) -> Result<alloy_primitives::Bytes, EthApiError>
685    where
686        N: RpcNodeCore<
687            Provider: reth_provider::BlockReaderIdExt + reth_storage_api::ReceiptProvider,
688        >,
689    {
690        use std::collections::HashMap;
691
692        use alloy_consensus::TxReceipt;
693        use arb_precompiles::arbsys::{
694            ARBSYS_ADDRESS, l2_to_l1_tx_topic, send_merkle_update_topic,
695        };
696        use reth_provider::{BlockNumReader, ReceiptProvider};
697
698        use crate::outbox_proof::{LevelAndLeaf, encode_outbox_proof, finalize_proof, plan_proof};
699
700        if input.len() < 4 + 64 {
701            return Err(EthApiError::InvalidParams(
702                "constructOutboxProof: expected (uint64 size, uint64 leaf)".into(),
703            ));
704        }
705        let size: u64 = U256::from_be_slice(&input[4..36])
706            .try_into()
707            .unwrap_or(u64::MAX);
708        let leaf: u64 = U256::from_be_slice(&input[36..68])
709            .try_into()
710            .unwrap_or(u64::MAX);
711
712        let plan = plan_proof(size, leaf).ok_or_else(|| {
713            EthApiError::InvalidParams(format!("constructOutboxProof: leaf {leaf} ≥ size {size}"))
714        })?;
715
716        // Resolve `at` to a concrete block number upper-bound. If
717        // `latest` or missing, use the chain tip.
718        let provider = self.inner.provider();
719        let tip = provider
720            .best_block_number()
721            .map_err(|e| EthApiError::Internal(e.into()))?;
722        let upper = match at {
723            BlockId::Number(alloy_rpc_types_eth::BlockNumberOrTag::Number(n)) => n.min(tip),
724            _ => tip,
725        };
726
727        // Scan receipts over [0..=upper] for ArbSys merkle + L2ToL1Tx
728        // logs. Topic layout for both events: topic[3] = position (a
729        // LevelAndLeaf packed as uint256), topic[1..3] carry the hash
730        // depending on which event variant.
731        let merkle_topic = send_merkle_update_topic();
732        let l2tol1_topic = l2_to_l1_tx_topic();
733
734        // Position → hash map. Keyed by the 32-byte position bytes.
735        let mut positions: HashMap<[u8; 32], B256> = HashMap::new();
736
737        let receipts_per_block = provider
738            .receipts_by_block_range(0..=upper)
739            .map_err(|e| EthApiError::Internal(e.into()))?;
740
741        for block_receipts in receipts_per_block {
742            for receipt in block_receipts {
743                for log in receipt.logs() {
744                    if log.address != ARBSYS_ADDRESS {
745                        continue;
746                    }
747                    let topics = log.data.topics();
748                    if topics.len() < 4 {
749                        continue;
750                    }
751                    let kind = topics[0];
752                    let is_merkle = kind == merkle_topic;
753                    let is_l2tol1 = kind == l2tol1_topic;
754                    if !is_merkle && !is_l2tol1 {
755                        continue;
756                    }
757                    // position encoded in topic[3]; hash in topic[2]
758                    // for both events (hash is an indexed arg).
759                    let pos: [u8; 32] = topics[3].0;
760                    let hash: B256 = topics[2];
761                    positions.insert(pos, hash);
762                }
763            }
764        }
765
766        let lookup = |p: LevelAndLeaf| -> Option<B256> {
767            let topic = p.as_topic();
768            positions.get(&topic.0).copied()
769        };
770
771        let (send, root, proof) = finalize_proof(&plan, leaf, lookup)
772            .map_err(|e| EthApiError::InvalidParams(format!("constructOutboxProof: {e}")))?;
773
774        Ok(encode_outbox_proof(send, root, &proof))
775    }
776}
777
778// ---- Trait delegations (matching reth's EthApi bounds exactly) ----
779
780impl<N, Rpc> EthApiTypes for ArbEthApi<N, Rpc>
781where
782    N: RpcNodeCore,
783    Rpc: RpcConvert<Error = EthApiError>,
784{
785    type Error = EthApiError;
786    type NetworkTypes = Rpc::Network;
787    type RpcConvert = Rpc;
788
789    fn converter(&self) -> &Self::RpcConvert {
790        self.inner.converter()
791    }
792}
793
794impl<N, Rpc> RpcNodeCore for ArbEthApi<N, Rpc>
795where
796    N: RpcNodeCore,
797    Rpc: RpcConvert,
798{
799    type Primitives = N::Primitives;
800    type Provider = N::Provider;
801    type Pool = N::Pool;
802    type Evm = N::Evm;
803    type Network = N::Network;
804
805    #[inline]
806    fn pool(&self) -> &Self::Pool {
807        self.inner.pool()
808    }
809
810    #[inline]
811    fn evm_config(&self) -> &Self::Evm {
812        self.inner.evm_config()
813    }
814
815    #[inline]
816    fn network(&self) -> &Self::Network {
817        self.inner.network()
818    }
819
820    #[inline]
821    fn provider(&self) -> &Self::Provider {
822        self.inner.provider()
823    }
824}
825
826impl<N, Rpc> RpcNodeCoreExt for ArbEthApi<N, Rpc>
827where
828    N: RpcNodeCore,
829    Rpc: RpcConvert,
830{
831    #[inline]
832    fn cache(&self) -> &EthStateCache<N::Primitives> {
833        self.inner.cache()
834    }
835}
836
837impl<N, Rpc> EthApiSpec for ArbEthApi<N, Rpc>
838where
839    N: RpcNodeCore,
840    Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError>,
841{
842    fn starting_block(&self) -> U256 {
843        self.inner.starting_block()
844    }
845}
846
847impl<N, Rpc> SpawnBlocking for ArbEthApi<N, Rpc>
848where
849    N: RpcNodeCore,
850    Rpc: RpcConvert<Error = EthApiError>,
851{
852    #[inline]
853    fn io_task_spawner(&self) -> &Runtime {
854        self.inner.task_spawner()
855    }
856
857    #[inline]
858    fn tracing_task_pool(&self) -> &BlockingTaskPool {
859        self.inner.blocking_task_pool()
860    }
861
862    #[inline]
863    fn tracing_task_guard(&self) -> &BlockingTaskGuard {
864        self.inner.blocking_task_guard()
865    }
866
867    #[inline]
868    fn blocking_io_task_guard(&self) -> &Arc<tokio::sync::Semaphore> {
869        self.inner.blocking_io_request_semaphore()
870    }
871}
872
873impl<N, Rpc> LoadFee for ArbEthApi<N, Rpc>
874where
875    N: RpcNodeCore,
876    EthApiError: FromEvmError<N::Evm>,
877    Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError>,
878{
879    fn gas_oracle(&self) -> &GasPriceOracle<Self::Provider> {
880        self.inner.gas_oracle()
881    }
882
883    fn fee_history_cache(&self) -> &FeeHistoryCache<ProviderHeader<N::Provider>> {
884        self.inner.fee_history_cache()
885    }
886}
887
888impl<N, Rpc> LoadState for ArbEthApi<N, Rpc>
889where
890    N: RpcNodeCore,
891    Rpc: RpcConvert<Primitives = N::Primitives>,
892    Self: LoadPendingBlock,
893{
894}
895
896impl<N, Rpc> EthState for ArbEthApi<N, Rpc>
897where
898    N: RpcNodeCore,
899    Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError>,
900    Self: LoadPendingBlock,
901{
902    fn max_proof_window(&self) -> u64 {
903        self.inner.eth_proof_window()
904    }
905}
906
907impl<N, Rpc> EthFees for ArbEthApi<N, Rpc>
908where
909    N: RpcNodeCore,
910    EthApiError: FromEvmError<N::Evm>,
911    Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError>,
912{
913    /// `eth_gasPrice` returns just the latest base fee — there is no
914    /// priority-fee market on this chain.
915    fn gas_price(&self) -> impl std::future::Future<Output = Result<U256, Self::Error>> + Send
916    where
917        Self: reth_rpc_eth_api::helpers::LoadBlock,
918    {
919        use alloy_consensus::BlockHeader;
920        use reth_storage_api::{BlockNumReader, HeaderProvider};
921        async move {
922            let best = self
923                .provider()
924                .best_block_number()
925                .map_err(|e| EthApiError::Internal(e.into()))?;
926            let header_opt = HeaderProvider::sealed_header(self.provider(), best)
927                .map_err(|e| EthApiError::Internal(e.into()))?;
928            let base_fee = match header_opt {
929                Some(sealed) => sealed.header().base_fee_per_gas().unwrap_or_default(),
930                None => 0,
931            };
932            Ok(U256::from(base_fee))
933        }
934    }
935
936    #[allow(clippy::manual_async_fn)]
937    fn suggested_priority_fee(
938        &self,
939    ) -> impl std::future::Future<Output = Result<U256, Self::Error>> + Send
940    where
941        Self: 'static,
942    {
943        async move { Ok(U256::ZERO) }
944    }
945}
946
947impl<N, Rpc> Trace for ArbEthApi<N, Rpc>
948where
949    N: RpcNodeCore,
950    EthApiError: FromEvmError<N::Evm>,
951    Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError, Evm = N::Evm>,
952{
953}
954
955impl<N, Rpc> GetBlockAccessList for ArbEthApi<N, Rpc>
956where
957    N: RpcNodeCore,
958    EthApiError: FromEvmError<N::Evm>,
959    Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError, Evm = N::Evm>,
960{
961    /// `eth_getBlockAccessList*` is unsupported on Arbitrum for now.
962    ///
963    /// reth's default implementation reconstructs the access list by replaying
964    /// the block over a `State` whose BAL recorder is fed only through
965    /// `DatabaseCommit::commit`. The Arbitrum executor applies mints, burns,
966    /// nonce bumps, and fee distribution by writing `state.cache.accounts`
967    /// directly, so those balance changes bypass
968    /// the recorder and the default would return an access list silently
969    /// missing them for every block. Return an explicit error instead of
970    /// incomplete data until the executor feeds the BAL builder.
971    async fn get_block_access_list(
972        &self,
973        _block_id: BlockId,
974    ) -> Result<Option<alloy_eips::eip7928::BlockAccessList>, Self::Error> {
975        Err(EthApiError::Unsupported(
976            "eth_getBlockAccessList is not supported on Arbitrum",
977        ))
978    }
979}
980
981impl<N, Rpc> LoadPendingBlock for ArbEthApi<N, Rpc>
982where
983    N: RpcNodeCore,
984    EthApiError: FromEvmError<N::Evm>,
985    Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError>,
986{
987    fn pending_block(&self) -> &tokio::sync::Mutex<Option<PendingBlock<N::Primitives>>> {
988        self.inner.pending_block()
989    }
990
991    fn pending_env_builder(&self) -> &dyn PendingEnvBuilder<N::Evm> {
992        self.inner.pending_env_builder()
993    }
994
995    fn pending_block_kind(&self) -> PendingBlockKind {
996        self.inner.pending_block_kind()
997    }
998}
999
1000impl<N, Rpc> LoadBlock for ArbEthApi<N, Rpc>
1001where
1002    Self: LoadPendingBlock,
1003    N: RpcNodeCore,
1004    Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError>,
1005{
1006}
1007
1008impl<N, Rpc> LoadTransaction for ArbEthApi<N, Rpc>
1009where
1010    N: RpcNodeCore,
1011    EthApiError: FromEvmError<N::Evm>,
1012    Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError>,
1013{
1014}
1015
1016impl<N, Rpc> EthBlocks for ArbEthApi<N, Rpc>
1017where
1018    N: RpcNodeCore,
1019    EthApiError: FromEvmError<N::Evm>,
1020    Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError>,
1021{
1022}
1023
1024impl<N, Rpc> EthTransactions for ArbEthApi<N, Rpc>
1025where
1026    N: RpcNodeCore,
1027    EthApiError: FromEvmError<N::Evm>,
1028    Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError>,
1029{
1030    fn signers(&self) -> &SignersForRpc<Self::Provider, Self::NetworkTypes> {
1031        self.inner.signers()
1032    }
1033
1034    fn send_raw_transaction_sync_timeout(&self) -> Duration {
1035        self.inner.send_raw_transaction_sync_timeout()
1036    }
1037
1038    async fn send_transaction(
1039        &self,
1040        origin: TransactionOrigin,
1041        tx: WithEncoded<Recovered<PoolPooledTx<Self::Pool>>>,
1042    ) -> Result<B256, Self::Error> {
1043        let (_tx_bytes, recovered) = tx.split();
1044        let pool_transaction = <Self::Pool as TransactionPool>::Transaction::from_pooled(recovered);
1045
1046        let AddedTransactionOutcome { hash, .. } = self
1047            .inner
1048            .add_pool_transaction(origin, pool_transaction)
1049            .await?;
1050
1051        Ok(hash)
1052    }
1053}
1054
1055impl<N, Rpc> LoadReceipt for ArbEthApi<N, Rpc>
1056where
1057    N: RpcNodeCore<Primitives = arb_primitives::ArbPrimitives>,
1058    EthApiError: FromEvmError<N::Evm>,
1059    Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError>,
1060    Self::Error: reth_rpc_eth_types::error::FromEthApiError,
1061{
1062    /// Override to use `convert_receipts_with_block` so every single-tx
1063    /// receipt fetch (e.g. `eth_getTransactionReceipt`) includes the
1064    /// Arbitrum `l1BlockNumber` field sourced from the block's mix_hash.
1065    ///
1066    /// Reth's default impl uses `convert_receipts` (no-block path), which
1067    /// our `ArbReceiptConverter` populates with `l1_block_number = None`.
1068    /// That breaks Arbitrum spec (bridges, indexers, explorers all expect
1069    /// `l1BlockNumber` on every receipt).
1070    fn build_transaction_receipt(
1071        &self,
1072        tx: reth_primitives_traits::Recovered<reth_storage_api::ProviderTx<Self::Provider>>,
1073        meta: alloy_consensus::transaction::TransactionMeta,
1074        receipt: reth_storage_api::ProviderReceipt<Self::Provider>,
1075        all_receipts: Option<
1076            std::sync::Arc<Vec<reth_storage_api::ProviderReceipt<Self::Provider>>>,
1077        >,
1078    ) -> impl std::future::Future<
1079        Output = Result<reth_rpc_eth_api::RpcReceipt<Self::NetworkTypes>, Self::Error>,
1080    > + Send {
1081        use alloy_consensus::TxReceipt;
1082        use reth_rpc_convert::transaction::ConvertReceiptInput;
1083        use reth_rpc_eth_api::RpcNodeCoreExt;
1084        use reth_rpc_eth_types::{
1085            EthApiError, error::FromEthApiError, utils::calculate_gas_used_and_next_log_index,
1086        };
1087        async move {
1088            let hash = meta.block_hash;
1089            let all_receipts = match all_receipts {
1090                Some(receipts) => receipts,
1091                None => self
1092                    .cache()
1093                    .get_receipts(hash)
1094                    .await
1095                    .map_err(<Self::Error as FromEthApiError>::from_eth_err)?
1096                    .ok_or_else(|| {
1097                        <Self::Error as FromEthApiError>::from_eth_err(EthApiError::HeaderNotFound(
1098                            hash.into(),
1099                        ))
1100                    })?,
1101            };
1102
1103            let (gas_used, next_log_index) =
1104                calculate_gas_used_and_next_log_index(meta.index, &all_receipts);
1105
1106            let block = self
1107                .cache()
1108                .get_recovered_block(hash)
1109                .await
1110                .map_err(<Self::Error as FromEthApiError>::from_eth_err)?;
1111
1112            let input = ConvertReceiptInput {
1113                tx: tx.as_recovered_ref(),
1114                gas_used: receipt.cumulative_gas_used() - gas_used,
1115                receipt,
1116                next_log_index,
1117                meta,
1118            };
1119
1120            let result = match block {
1121                Some(sealed_block_with_senders) => self.converter().convert_receipts_with_block(
1122                    vec![input],
1123                    sealed_block_with_senders.sealed_block(),
1124                )?,
1125                None => self.converter().convert_receipts(vec![input])?,
1126            };
1127            Ok(result.into_iter().next().expect("one receipt in, one out"))
1128        }
1129    }
1130}
1131
1132// ---- Gas estimation override ----
1133
1134impl<N, Rpc> Call for ArbEthApi<N, Rpc>
1135where
1136    N: RpcNodeCore,
1137    EthApiError: FromEvmError<N::Evm>,
1138    Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError, Evm = N::Evm>,
1139{
1140    #[inline]
1141    fn call_gas_limit(&self) -> u64 {
1142        self.inner.gas_cap()
1143    }
1144
1145    #[inline]
1146    fn max_simulate_blocks(&self) -> u64 {
1147        self.inner.max_simulate_blocks()
1148    }
1149
1150    #[inline]
1151    fn evm_memory_limit(&self) -> u64 {
1152        self.inner.evm_memory_limit()
1153    }
1154}
1155
1156impl<N, Rpc> EstimateCall for ArbEthApi<N, Rpc>
1157where
1158    N: RpcNodeCore,
1159    EthApiError: FromEvmError<N::Evm>,
1160    Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError, Evm = N::Evm>,
1161{
1162    // Uses default binary search. L1 posting gas is added in EthCall below.
1163}
1164
1165impl<N, Rpc> EthCall for ArbEthApi<N, Rpc>
1166where
1167    N: RpcNodeCore<
1168            Provider: StateProviderFactory + reth_provider::BlockReaderIdExt + Clone,
1169            Primitives = arb_primitives::ArbPrimitives,
1170        >,
1171    EthApiError: FromEvmError<N::Evm>,
1172    Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError, Evm = N::Evm>,
1173    RpcTxReq<<Rpc as RpcConvert>::Network>: AsRef<alloy_rpc_types_eth::TransactionRequest>
1174        + AsMut<alloy_rpc_types_eth::TransactionRequest>
1175        + Clone
1176        + Default
1177        + From<alloy_rpc_types_eth::TransactionRequest>,
1178{
1179    /// Override gas estimation to add L1 posting costs.
1180    ///
1181    /// Also intercepts `estimateRetryableTicket` calls to the
1182    /// NodeInterface (0xc8): client calls
1183    /// `eth_estimateGas({to:0xc8, data: estimateRetryableTicket(...)})`
1184    /// and expects back the gas for the retryable submission. We parse
1185    /// the ABI args, build an equivalent transaction request targeting
1186    /// the retry_to with retry_value + retry_data, run the standard
1187    /// estimation on that, and add the submit-retryable overhead.
1188    #[allow(clippy::manual_async_fn)]
1189    fn estimate_gas_at(
1190        &self,
1191        request: RpcTxReq<<Self::RpcConvert as RpcConvert>::Network>,
1192        at: BlockId,
1193        state_override: Option<StateOverride>,
1194    ) -> impl std::future::Future<Output = Result<U256, Self::Error>> + Send {
1195        async move {
1196            use alloy_primitives::TxKind;
1197
1198            use crate::nodeinterface_rpc::NODE_INTERFACE_ADDRESS;
1199
1200            let inner = request.as_ref();
1201            let target: Option<Address> = match inner.to {
1202                Some(TxKind::Call(addr)) => Some(addr),
1203                _ => None,
1204            };
1205            let input_bytes: Option<alloy_primitives::Bytes> = inner.input.input().cloned();
1206
1207            // Intercept estimateRetryableTicket on NodeInterface (0xc8).
1208            //
1209            // ABI: estimateRetryableTicket(
1210            //   address sender, uint256 deposit, address to,
1211            //   uint256 l2CallValue, address excessFeeRefundAddress,
1212            //   address callValueRefundAddress, bytes data)
1213            //
1214            // selector: 0xc3dc5879
1215            if target == Some(NODE_INTERFACE_ADDRESS)
1216                && let Some(ref buf) = input_bytes
1217                && buf.len() >= 4
1218                && buf[..4] == [0xc3, 0xdc, 0x58, 0x79]
1219            {
1220                return self
1221                    .estimate_retryable_ticket_gas(buf, at, state_override)
1222                    .await;
1223            }
1224
1225            // Extract calldata length before request is consumed by the binary search.
1226            let calldata_len = input_bytes.as_ref().map(|b| b.len()).unwrap_or(0);
1227
1228            // Run the standard binary search to find compute gas.
1229            let compute_gas =
1230                EstimateCall::estimate_gas_at(self, request, at, state_override).await?;
1231
1232            // Add L1 posting gas.
1233            let l1_gas = self.l1_posting_gas(calldata_len, at)?;
1234
1235            if l1_gas > 0 {
1236                trace!(target: "rpc::eth::estimate", %compute_gas, l1_gas, "Adding L1 posting gas to estimate");
1237            }
1238
1239            Ok(compute_gas.saturating_add(U256::from(l1_gas)))
1240        }
1241    }
1242
1243    /// Intercept `eth_call` to the NodeInterface (0xc8) virtual contract
1244    /// for methods that need chain history or nested EVM calls. Methods
1245    /// that can be resolved at the precompile layer (with zero / empty
1246    /// fallbacks) are delegated to the default EVM path.
1247    #[allow(clippy::manual_async_fn)]
1248    fn call(
1249        &self,
1250        request: RpcTxReq<<Self::RpcConvert as RpcConvert>::Network>,
1251        block_number: Option<BlockId>,
1252        overrides: alloy_rpc_types_eth::state::EvmOverrides,
1253    ) -> impl std::future::Future<Output = Result<alloy_primitives::Bytes, Self::Error>> + Send
1254    {
1255        async move {
1256            use alloy_primitives::{Address, TxKind};
1257
1258            use crate::nodeinterface_rpc::{
1259                NODE_INTERFACE_ADDRESS, SEL_BLOCK_L1_NUM, SEL_FIND_BATCH_CONTAINING_BLOCK,
1260                SEL_GAS_ESTIMATE_COMPONENTS, SEL_GAS_ESTIMATE_L1_COMPONENT,
1261                SEL_GET_L1_CONFIRMATIONS, SEL_L2_BLOCK_RANGE_FOR_L1,
1262                SEL_LEGACY_LOOKUP_MESSAGE_BATCH_PROOF, SEL_NITRO_GENESIS_BLOCK,
1263                encode_gas_estimate_components, encode_l2_block_range, encode_legacy_lookup_empty,
1264                encode_u64_word, unpack_mix_hash,
1265            };
1266
1267            // Only intercept calls targeting the NodeInterface or
1268            // NodeInterfaceDebug addresses.
1269            let target: Option<Address> = match request.as_ref().to {
1270                Some(TxKind::Call(addr)) => Some(addr),
1271                _ => None,
1272            };
1273            let is_ni = target == Some(NODE_INTERFACE_ADDRESS);
1274            let is_ni_debug = target == Some(arb_precompiles::NODE_INTERFACE_DEBUG_ADDRESS);
1275
1276            // ArbGasInfo.getCurrentTxL1GasFees needs the poster fee for
1277            // this eth_call. The precompile can't see the outer message,
1278            // so compute the poster fee from the request envelope using
1279            // the same fake-tx + brotli + (units+256)*1.01 formula.
1280            if target == Some(arb_precompiles::ARBGASINFO_ADDRESS) {
1281                let input_bytes = request.as_ref().input.input().cloned().unwrap_or_default();
1282                if input_bytes.len() == 4 && input_bytes.as_ref() == SEL_GET_CURRENT_TX_L1_FEES {
1283                    return self.compute_eth_call_current_tx_l1_fees(
1284                        request,
1285                        block_number.unwrap_or_default(),
1286                    );
1287                }
1288                if input_bytes.len() == 4
1289                    && input_bytes.as_ref() == SEL_GET_L1_PRICING_UNITS_SINCE_UPDATE
1290                {
1291                    return self.compute_eth_call_units_since_update(
1292                        request,
1293                        block_number.unwrap_or_default(),
1294                    );
1295                }
1296            }
1297
1298            if !is_ni && !is_ni_debug {
1299                let _permit = self.acquire_owned_blocking_io().await;
1300                let res = self
1301                    .transact_call_at(request, block_number.unwrap_or_default(), overrides)
1302                    .await?;
1303                return <Self::Error as reth_rpc_eth_types::error::api::FromEvmError<N::Evm>>::ensure_success(res.result);
1304            }
1305
1306            // NodeInterfaceDebug (0xc9) has one method: getRetryable(bytes32).
1307            if is_ni_debug {
1308                let at = block_number.unwrap_or_default();
1309                let data: alloy_primitives::Bytes =
1310                    request.as_ref().input.input().cloned().unwrap_or_default();
1311                return self.get_retryable_abi(&data, at).await;
1312            }
1313
1314            // Parse selector.
1315            let input_bytes = request.as_ref().input.input().cloned().unwrap_or_default();
1316            if input_bytes.len() < 4 {
1317                // Fall back to EVM (which will revert with our precompile).
1318                let _permit = self.acquire_owned_blocking_io().await;
1319                let res = self
1320                    .transact_call_at(request, block_number.unwrap_or_default(), overrides)
1321                    .await?;
1322                return <Self::Error as reth_rpc_eth_types::error::api::FromEvmError<N::Evm>>::ensure_success(res.result);
1323            }
1324            let selector: [u8; 4] = [
1325                input_bytes[0],
1326                input_bytes[1],
1327                input_bytes[2],
1328                input_bytes[3],
1329            ];
1330            let at = block_number.unwrap_or_default();
1331
1332            match selector {
1333                SEL_GAS_ESTIMATE_COMPONENTS | SEL_GAS_ESTIMATE_L1_COMPONENT => {
1334                    use alloy_rpc_types_eth::TransactionRequest;
1335
1336                    let (inner_to, inner_creation, inner_data) =
1337                        arb_precompiles::decode_estimate_args(&input_bytes).ok_or_else(|| {
1338                            EthApiError::InvalidParams(
1339                                "gasEstimateComponents: malformed calldata".into(),
1340                            )
1341                        })?;
1342
1343                    let (l1_price, basefee, min_basefee, chain_id_u, brotli_level) = {
1344                        let state = self
1345                            .inner
1346                            .provider()
1347                            .state_by_block_id(at)
1348                            .map_err(|e| EthApiError::Internal(e.into()))?;
1349                        let read = |slot: U256| -> Result<U256, EthApiError> {
1350                            Ok(state
1351                                .storage(
1352                                    ARBOS_STATE_ADDRESS,
1353                                    StorageKey::from(B256::from(slot.to_be_bytes::<32>())),
1354                                )
1355                                .map_err(|e| EthApiError::Internal(e.into()))?
1356                                .unwrap_or_default())
1357                        };
1358                        let l1_price = read(subspace_slot(L1_PRICING_SUBSPACE, L1_PRICE_PER_UNIT))?;
1359                        let basefee = read(subspace_slot(L2_PRICING_SUBSPACE, L2_BASE_FEE))?;
1360                        let min_basefee =
1361                            read(subspace_slot(L2_PRICING_SUBSPACE, L2_MIN_BASE_FEE))?;
1362                        let chain_id_u: u64 =
1363                            read(root_slot(CHAIN_ID_OFFSET))?.try_into().unwrap_or(0);
1364                        let brotli_level: u64 = read(root_slot(BROTLI_COMPRESSION_LEVEL_OFFSET))?
1365                            .try_into()
1366                            .unwrap_or(0);
1367                        (l1_price, basefee, min_basefee, chain_id_u, brotli_level)
1368                    };
1369
1370                    let gas_for_l1 = arb_precompiles::compute_l1_gas_for_estimate(
1371                        chain_id_u,
1372                        inner_to,
1373                        inner_creation,
1374                        U256::ZERO,
1375                        inner_data.clone(),
1376                        l1_price,
1377                        basefee,
1378                        min_basefee,
1379                        brotli_level,
1380                    );
1381
1382                    if selector == SEL_GAS_ESTIMATE_L1_COMPONENT {
1383                        let mut out = vec![0u8; 96];
1384                        out[24..32].copy_from_slice(&gas_for_l1.to_be_bytes());
1385                        out[32..64].copy_from_slice(&basefee.to_be_bytes::<32>());
1386                        out[64..96].copy_from_slice(&l1_price.to_be_bytes::<32>());
1387                        return Ok(alloy_primitives::Bytes::from(out));
1388                    }
1389
1390                    let kind = if inner_creation {
1391                        TxKind::Create
1392                    } else {
1393                        TxKind::Call(inner_to)
1394                    };
1395                    let from = request.as_ref().from.unwrap_or(Address::ZERO);
1396                    let inner_request = TransactionRequest {
1397                        from: Some(from),
1398                        to: Some(kind),
1399                        value: Some(U256::ZERO),
1400                        input: inner_data.into(),
1401                        ..Default::default()
1402                    };
1403                    let inner_req: RpcTxReq<<Rpc as RpcConvert>::Network> = inner_request.into();
1404
1405                    let total = self
1406                        .estimate_arb_combined_gas(inner_req, gas_for_l1, at, overrides.state)
1407                        .await?;
1408
1409                    Ok(encode_gas_estimate_components(
1410                        total, gas_for_l1, basefee, l1_price,
1411                    ))
1412                }
1413
1414                SEL_L2_BLOCK_RANGE_FOR_L1 => {
1415                    use reth_provider::{BlockNumReader, BlockReaderIdExt};
1416
1417                    if input_bytes.len() < 4 + 32 {
1418                        return Err(EthApiError::InvalidParams(
1419                            "l2BlockRangeForL1: missing uint64 arg".into(),
1420                        ));
1421                    }
1422                    let target_l1: u64 = U256::from_be_slice(&input_bytes[4..36])
1423                        .try_into()
1424                        .unwrap_or(u64::MAX);
1425
1426                    let provider = self.inner.provider().clone();
1427                    let best = provider
1428                        .best_block_number()
1429                        .map_err(|e| EthApiError::Internal(e.into()))?;
1430
1431                    let mix_hash_of = move |n: u64| -> Option<B256> {
1432                        use alloy_consensus::BlockHeader;
1433                        provider
1434                            .sealed_header_by_number_or_tag(
1435                                alloy_rpc_types_eth::BlockNumberOrTag::Number(n),
1436                            )
1437                            .ok()
1438                            .flatten()
1439                            .and_then(|h| h.header().mix_hash())
1440                    };
1441
1442                    match crate::nodeinterface_rpc::find_l2_block_range(
1443                        target_l1,
1444                        best,
1445                        mix_hash_of,
1446                    ) {
1447                        Some((first, last)) => Ok(encode_l2_block_range(first, last)),
1448                        None => Err(EthApiError::InvalidParams(format!(
1449                            "l2BlockRangeForL1: no L2 blocks found for L1 block {target_l1}"
1450                        ))),
1451                    }
1452                }
1453
1454                // estimateRetryableTicket via eth_call.
1455                [0xc3, 0xdc, 0x58, 0x79] => {
1456                    self.simulate_retryable_ticket_call(&input_bytes, at, overrides)
1457                        .await
1458                }
1459
1460                // constructOutboxProof(uint64 size, uint64 leaf): scan
1461                // ArbSys SendMerkleUpdate / L2ToL1Tx events, build a
1462                // position → hash map, run the outbox-proof algorithm.
1463                [0x42, 0x69, 0x63, 0x50] => self.construct_outbox_proof(&input_bytes, at).await,
1464
1465                SEL_NITRO_GENESIS_BLOCK => {
1466                    let state = self
1467                        .inner
1468                        .provider()
1469                        .state_by_block_id(at)
1470                        .map_err(|e| EthApiError::Internal(e.into()))?;
1471                    let genesis: u64 = state
1472                        .storage(
1473                            ARBOS_STATE_ADDRESS,
1474                            StorageKey::from(B256::from(
1475                                root_slot(GENESIS_BLOCK_NUM_OFFSET).to_be_bytes::<32>(),
1476                            )),
1477                        )
1478                        .map_err(|e| EthApiError::Internal(e.into()))?
1479                        .unwrap_or_default()
1480                        .try_into()
1481                        .unwrap_or(0);
1482                    Ok(encode_u64_word(genesis))
1483                }
1484
1485                SEL_BLOCK_L1_NUM => {
1486                    use alloy_consensus::BlockHeader;
1487                    use reth_provider::BlockReaderIdExt;
1488                    if input_bytes.len() < 4 + 32 {
1489                        return Err(EthApiError::InvalidParams(
1490                            "blockL1Num: missing uint64 arg".into(),
1491                        ));
1492                    }
1493                    let l2_block: u64 = U256::from_be_slice(&input_bytes[4..36])
1494                        .try_into()
1495                        .unwrap_or(u64::MAX);
1496                    let l1_block = self
1497                        .inner
1498                        .provider()
1499                        .sealed_header_by_number_or_tag(
1500                            alloy_rpc_types_eth::BlockNumberOrTag::Number(l2_block),
1501                        )
1502                        .ok()
1503                        .flatten()
1504                        .and_then(|h| h.header().mix_hash())
1505                        .map(|mix| unpack_mix_hash(mix).1)
1506                        .unwrap_or(0);
1507                    Ok(encode_u64_word(l1_block))
1508                }
1509
1510                SEL_GET_L1_CONFIRMATIONS | SEL_FIND_BATCH_CONTAINING_BLOCK => {
1511                    Ok(encode_u64_word(0))
1512                }
1513
1514                SEL_LEGACY_LOOKUP_MESSAGE_BATCH_PROOF => Ok(encode_legacy_lookup_empty()),
1515
1516                _ => {
1517                    // Delegate to EVM (precompile returns zero / reverts).
1518                    let _permit = self.acquire_owned_blocking_io().await;
1519                    let res = self.transact_call_at(request, at, overrides).await?;
1520                    <Self::Error as reth_rpc_eth_types::error::api::FromEvmError<N::Evm>>::ensure_success(res.result)
1521                }
1522            }
1523        }
1524    }
1525}