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::{StorageKey, B256, U256};
9use alloy_rpc_types_eth::{state::StateOverride, BlockId};
10use reth_primitives_traits::{Recovered, WithEncoded};
11use reth_rpc::eth::core::EthApiInner;
12use reth_rpc_convert::{RpcConvert, RpcTxReq};
13use reth_rpc_eth_api::{
14    helpers::{
15        estimate::EstimateCall, pending_block::PendingEnvBuilder, Call, EthApiSpec, EthBlocks,
16        EthCall, EthFees, EthSigner, EthState, EthTransactions, LoadBlock, LoadFee,
17        LoadPendingBlock, LoadReceipt, LoadState, LoadTransaction, SpawnBlocking, Trace,
18    },
19    EthApiTypes, FromEvmError, RpcNodeCore, RpcNodeCoreExt,
20};
21use reth_rpc_eth_types::{
22    builder::config::PendingBlockKind, EthApiError, EthStateCache, FeeHistoryCache, GasPriceOracle,
23    PendingBlock,
24};
25use reth_storage_api::{ProviderHeader, StateProviderFactory, TransactionsProvider};
26use reth_tasks::{
27    pool::{BlockingTaskGuard, BlockingTaskPool},
28    Runtime,
29};
30use reth_transaction_pool::{
31    AddedTransactionOutcome, PoolPooledTx, PoolTransaction, TransactionOrigin, TransactionPool,
32};
33use tracing::trace;
34
35use arb_precompiles::storage_slot::{
36    subspace_slot, ARBOS_STATE_ADDRESS, L1_PRICING_SUBSPACE, L2_PRICING_SUBSPACE,
37};
38
39/// Type alias matching reth's `SignersForRpc`.
40type SignersForRpc<Provider, Rpc> = parking_lot::RwLock<
41    Vec<Box<dyn EthSigner<<Provider as TransactionsProvider>::Transaction, RpcTxReq<Rpc>>>>,
42>;
43
44/// L1 pricing field offset for price per unit.
45const L1_PRICE_PER_UNIT: u64 = 7;
46
47/// L2 pricing field offset for base fee.
48const L2_BASE_FEE: u64 = 2;
49
50/// Non-zero calldata gas cost per byte (EIP-2028).
51const TX_DATA_NON_ZERO_GAS: u64 = 16;
52
53/// Padding applied to L1 fee estimates (110% = 11000 bips).
54const GAS_ESTIMATION_L1_PRICE_PADDING: u64 = 11000;
55
56/// Arbitrum Eth API wrapping the standard reth EthApiInner.
57///
58/// This wrapper overrides gas estimation to add L1 posting costs.
59pub struct ArbEthApi<N: RpcNodeCore, Rpc: RpcConvert> {
60    inner: Arc<EthApiInner<N, Rpc>>,
61}
62
63impl<N: RpcNodeCore, Rpc: RpcConvert> Clone for ArbEthApi<N, Rpc> {
64    fn clone(&self) -> Self {
65        Self {
66            inner: self.inner.clone(),
67        }
68    }
69}
70
71impl<N: RpcNodeCore, Rpc: RpcConvert> std::fmt::Debug for ArbEthApi<N, Rpc> {
72    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73        f.debug_struct("ArbEthApi").finish_non_exhaustive()
74    }
75}
76
77impl<N: RpcNodeCore, Rpc: RpcConvert> ArbEthApi<N, Rpc> {
78    /// Create a new `ArbEthApi` wrapping the given inner.
79    pub fn new(inner: EthApiInner<N, Rpc>) -> Self {
80        Self {
81            inner: Arc::new(inner),
82        }
83    }
84}
85
86impl<N, Rpc> ArbEthApi<N, Rpc>
87where
88    N: RpcNodeCore<Provider: StateProviderFactory>,
89    Rpc: RpcConvert,
90{
91    /// Compute L1 posting gas for gas estimation.
92    ///
93    /// Reads L1 pricing state from ArbOS to estimate the gas needed to cover
94    /// L1 data posting costs for the given calldata length.
95    fn l1_posting_gas(&self, calldata_len: usize, at: BlockId) -> Result<u64, EthApiError> {
96        if calldata_len == 0 {
97            return Ok(0);
98        }
99
100        let state = self
101            .inner
102            .provider()
103            .state_by_block_id(at)
104            .map_err(|e| EthApiError::Internal(e.into()))?;
105
106        let l1_price_slot = subspace_slot(L1_PRICING_SUBSPACE, L1_PRICE_PER_UNIT);
107        let l1_price = state
108            .storage(
109                ARBOS_STATE_ADDRESS,
110                StorageKey::from(B256::from(l1_price_slot.to_be_bytes::<32>())),
111            )
112            .map_err(|e| EthApiError::Internal(e.into()))?
113            .unwrap_or_default();
114
115        let basefee_slot = subspace_slot(L2_PRICING_SUBSPACE, L2_BASE_FEE);
116        let basefee = state
117            .storage(
118                ARBOS_STATE_ADDRESS,
119                StorageKey::from(B256::from(basefee_slot.to_be_bytes::<32>())),
120            )
121            .map_err(|e| EthApiError::Internal(e.into()))?
122            .unwrap_or_default();
123
124        if l1_price.is_zero() || basefee.is_zero() {
125            return Ok(0);
126        }
127
128        // L1 fee = l1_price * calldata_bytes * TX_DATA_NON_ZERO_GAS
129        let l1_fee = l1_price
130            .saturating_mul(U256::from(TX_DATA_NON_ZERO_GAS))
131            .saturating_mul(U256::from(calldata_len));
132
133        // Apply 110% padding for L1 price volatility.
134        let padded = l1_fee.saturating_mul(U256::from(GAS_ESTIMATION_L1_PRICE_PADDING))
135            / U256::from(10000u64);
136
137        // Use 7/8 of basefee as congestion discount for estimation.
138        let adjusted_basefee = basefee.saturating_mul(U256::from(7)) / U256::from(8);
139        let adjusted_basefee = if adjusted_basefee.is_zero() {
140            U256::from(1)
141        } else {
142            adjusted_basefee
143        };
144
145        // Convert to gas units: posting_gas = padded_fee / adjusted_basefee
146        let gas = padded / adjusted_basefee;
147        Ok(gas.try_into().unwrap_or(u64::MAX))
148    }
149}
150
151// ---- Trait delegations (matching reth's EthApi bounds exactly) ----
152
153impl<N, Rpc> EthApiTypes for ArbEthApi<N, Rpc>
154where
155    N: RpcNodeCore,
156    Rpc: RpcConvert<Error = EthApiError>,
157{
158    type Error = EthApiError;
159    type NetworkTypes = Rpc::Network;
160    type RpcConvert = Rpc;
161
162    fn converter(&self) -> &Self::RpcConvert {
163        self.inner.converter()
164    }
165}
166
167impl<N, Rpc> RpcNodeCore for ArbEthApi<N, Rpc>
168where
169    N: RpcNodeCore,
170    Rpc: RpcConvert,
171{
172    type Primitives = N::Primitives;
173    type Provider = N::Provider;
174    type Pool = N::Pool;
175    type Evm = N::Evm;
176    type Network = N::Network;
177
178    #[inline]
179    fn pool(&self) -> &Self::Pool {
180        self.inner.pool()
181    }
182
183    #[inline]
184    fn evm_config(&self) -> &Self::Evm {
185        self.inner.evm_config()
186    }
187
188    #[inline]
189    fn network(&self) -> &Self::Network {
190        self.inner.network()
191    }
192
193    #[inline]
194    fn provider(&self) -> &Self::Provider {
195        self.inner.provider()
196    }
197}
198
199impl<N, Rpc> RpcNodeCoreExt for ArbEthApi<N, Rpc>
200where
201    N: RpcNodeCore,
202    Rpc: RpcConvert,
203{
204    #[inline]
205    fn cache(&self) -> &EthStateCache<N::Primitives> {
206        self.inner.cache()
207    }
208}
209
210impl<N, Rpc> EthApiSpec for ArbEthApi<N, Rpc>
211where
212    N: RpcNodeCore,
213    Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError>,
214{
215    fn starting_block(&self) -> U256 {
216        self.inner.starting_block()
217    }
218}
219
220impl<N, Rpc> SpawnBlocking for ArbEthApi<N, Rpc>
221where
222    N: RpcNodeCore,
223    Rpc: RpcConvert<Error = EthApiError>,
224{
225    #[inline]
226    fn io_task_spawner(&self) -> &Runtime {
227        self.inner.task_spawner()
228    }
229
230    #[inline]
231    fn tracing_task_pool(&self) -> &BlockingTaskPool {
232        self.inner.blocking_task_pool()
233    }
234
235    #[inline]
236    fn tracing_task_guard(&self) -> &BlockingTaskGuard {
237        self.inner.blocking_task_guard()
238    }
239
240    #[inline]
241    fn blocking_io_task_guard(&self) -> &Arc<tokio::sync::Semaphore> {
242        self.inner.blocking_io_request_semaphore()
243    }
244}
245
246impl<N, Rpc> LoadFee for ArbEthApi<N, Rpc>
247where
248    N: RpcNodeCore,
249    EthApiError: FromEvmError<N::Evm>,
250    Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError>,
251{
252    fn gas_oracle(&self) -> &GasPriceOracle<Self::Provider> {
253        self.inner.gas_oracle()
254    }
255
256    fn fee_history_cache(&self) -> &FeeHistoryCache<ProviderHeader<N::Provider>> {
257        self.inner.fee_history_cache()
258    }
259}
260
261impl<N, Rpc> LoadState for ArbEthApi<N, Rpc>
262where
263    N: RpcNodeCore,
264    Rpc: RpcConvert<Primitives = N::Primitives>,
265    Self: LoadPendingBlock,
266{
267}
268
269impl<N, Rpc> EthState for ArbEthApi<N, Rpc>
270where
271    N: RpcNodeCore,
272    Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError>,
273    Self: LoadPendingBlock,
274{
275    fn max_proof_window(&self) -> u64 {
276        self.inner.eth_proof_window()
277    }
278}
279
280impl<N, Rpc> EthFees for ArbEthApi<N, Rpc>
281where
282    N: RpcNodeCore,
283    EthApiError: FromEvmError<N::Evm>,
284    Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError>,
285{
286}
287
288impl<N, Rpc> Trace for ArbEthApi<N, Rpc>
289where
290    N: RpcNodeCore,
291    EthApiError: FromEvmError<N::Evm>,
292    Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError, Evm = N::Evm>,
293{
294}
295
296impl<N, Rpc> LoadPendingBlock for ArbEthApi<N, Rpc>
297where
298    N: RpcNodeCore,
299    EthApiError: FromEvmError<N::Evm>,
300    Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError>,
301{
302    fn pending_block(&self) -> &tokio::sync::Mutex<Option<PendingBlock<N::Primitives>>> {
303        self.inner.pending_block()
304    }
305
306    fn pending_env_builder(&self) -> &dyn PendingEnvBuilder<N::Evm> {
307        self.inner.pending_env_builder()
308    }
309
310    fn pending_block_kind(&self) -> PendingBlockKind {
311        self.inner.pending_block_kind()
312    }
313}
314
315impl<N, Rpc> LoadBlock for ArbEthApi<N, Rpc>
316where
317    Self: LoadPendingBlock,
318    N: RpcNodeCore,
319    Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError>,
320{
321}
322
323impl<N, Rpc> LoadTransaction for ArbEthApi<N, Rpc>
324where
325    N: RpcNodeCore,
326    EthApiError: FromEvmError<N::Evm>,
327    Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError>,
328{
329}
330
331impl<N, Rpc> EthBlocks for ArbEthApi<N, Rpc>
332where
333    N: RpcNodeCore,
334    EthApiError: FromEvmError<N::Evm>,
335    Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError>,
336{
337}
338
339impl<N, Rpc> EthTransactions for ArbEthApi<N, Rpc>
340where
341    N: RpcNodeCore,
342    EthApiError: FromEvmError<N::Evm>,
343    Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError>,
344{
345    fn signers(&self) -> &SignersForRpc<Self::Provider, Self::NetworkTypes> {
346        self.inner.signers()
347    }
348
349    fn send_raw_transaction_sync_timeout(&self) -> Duration {
350        self.inner.send_raw_transaction_sync_timeout()
351    }
352
353    async fn send_transaction(
354        &self,
355        origin: TransactionOrigin,
356        tx: WithEncoded<Recovered<PoolPooledTx<Self::Pool>>>,
357    ) -> Result<B256, Self::Error> {
358        let (_tx_bytes, recovered) = tx.split();
359        let pool_transaction = <Self::Pool as TransactionPool>::Transaction::from_pooled(recovered);
360
361        let AddedTransactionOutcome { hash, .. } = self
362            .inner
363            .add_pool_transaction(origin, pool_transaction)
364            .await?;
365
366        Ok(hash)
367    }
368}
369
370impl<N, Rpc> LoadReceipt for ArbEthApi<N, Rpc>
371where
372    N: RpcNodeCore,
373    EthApiError: FromEvmError<N::Evm>,
374    Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError>,
375{
376}
377
378// ---- Gas estimation override ----
379
380impl<N, Rpc> Call for ArbEthApi<N, Rpc>
381where
382    N: RpcNodeCore,
383    EthApiError: FromEvmError<N::Evm>,
384    Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError, Evm = N::Evm>,
385{
386    #[inline]
387    fn call_gas_limit(&self) -> u64 {
388        self.inner.gas_cap()
389    }
390
391    #[inline]
392    fn max_simulate_blocks(&self) -> u64 {
393        self.inner.max_simulate_blocks()
394    }
395
396    #[inline]
397    fn evm_memory_limit(&self) -> u64 {
398        self.inner.evm_memory_limit()
399    }
400}
401
402impl<N, Rpc> EstimateCall for ArbEthApi<N, Rpc>
403where
404    N: RpcNodeCore,
405    EthApiError: FromEvmError<N::Evm>,
406    Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError, Evm = N::Evm>,
407{
408    // Uses default binary search. L1 posting gas is added in EthCall below.
409}
410
411impl<N, Rpc> EthCall for ArbEthApi<N, Rpc>
412where
413    N: RpcNodeCore<Provider: StateProviderFactory>,
414    EthApiError: FromEvmError<N::Evm>,
415    Rpc: RpcConvert<Primitives = N::Primitives, Error = EthApiError, Evm = N::Evm>,
416{
417    /// Override gas estimation to add L1 posting costs.
418    #[allow(clippy::manual_async_fn)]
419    fn estimate_gas_at(
420        &self,
421        request: RpcTxReq<<Self::RpcConvert as RpcConvert>::Network>,
422        at: BlockId,
423        state_override: Option<StateOverride>,
424    ) -> impl std::future::Future<Output = Result<U256, Self::Error>> + Send {
425        async move {
426            // Extract calldata length before request is consumed by the binary search.
427            let calldata_len = request.as_ref().input.input().map(|b| b.len()).unwrap_or(0);
428
429            // Run the standard binary search to find compute gas.
430            let compute_gas =
431                EstimateCall::estimate_gas_at(self, request, at, state_override).await?;
432
433            // Add L1 posting gas.
434            let l1_gas = self.l1_posting_gas(calldata_len, at)?;
435
436            if l1_gas > 0 {
437                trace!(target: "rpc::eth::estimate", %compute_gas, l1_gas, "Adding L1 posting gas to estimate");
438            }
439
440            Ok(compute_gas.saturating_add(U256::from(l1_gas)))
441        }
442    }
443}