arb_precompiles/
nodeinterface.rs

1use alloy_evm::precompiles::{DynPrecompile, PrecompileInput};
2use alloy_primitives::{Address, U256};
3use revm::precompile::{PrecompileError, PrecompileId, PrecompileOutput, PrecompileResult};
4
5use crate::{
6    arbsys::get_cached_l1_block_number,
7    storage_slot::{
8        root_slot, subspace_slot, ARBOS_STATE_ADDRESS, GENESIS_BLOCK_NUM_OFFSET,
9        L1_PRICING_SUBSPACE, L2_PRICING_SUBSPACE,
10    },
11};
12
13/// NodeInterface virtual contract address (0xc8).
14pub const NODE_INTERFACE_ADDRESS: Address = Address::new([
15    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
16    0x00, 0x00, 0x00, 0xc8,
17]);
18
19// Function selectors.
20const GAS_ESTIMATE_COMPONENTS: [u8; 4] = [0xc9, 0x4e, 0x6e, 0xeb];
21const GAS_ESTIMATE_L1_COMPONENT: [u8; 4] = [0x77, 0xd4, 0x88, 0xa2];
22const NITRO_GENESIS_BLOCK: [u8; 4] = [0x93, 0xa2, 0xfe, 0x21];
23const BLOCK_L1_NUM: [u8; 4] = [0x6f, 0x27, 0x5e, 0xf2];
24const L2_BLOCK_RANGE_FOR_L1: [u8; 4] = [0x48, 0xe7, 0xf8, 0x11];
25const ESTIMATE_RETRYABLE_TICKET: [u8; 4] = [0xc3, 0xdc, 0x58, 0x79];
26const CONSTRUCT_OUTBOX_PROOF: [u8; 4] = [0x42, 0x69, 0x63, 0x50];
27const FIND_BATCH_CONTAINING_BLOCK: [u8; 4] = [0x81, 0xf1, 0xad, 0xaf];
28const GET_L1_CONFIRMATIONS: [u8; 4] = [0xe5, 0xca, 0x23, 0x8c];
29const LEGACY_LOOKUP_MESSAGE_BATCH_PROOF: [u8; 4] = [0x89, 0x49, 0x62, 0x70];
30
31// L1 pricing field offsets.
32const L1_PRICE_PER_UNIT: u64 = 7;
33
34// L2 pricing field offsets.
35const L2_BASE_FEE: u64 = 2;
36
37// Gas costs.
38const SLOAD_GAS: u64 = 800;
39const COPY_GAS: u64 = 3;
40
41/// Non-zero calldata gas cost per byte.
42const TX_DATA_NON_ZERO_GAS: u64 = 16;
43
44/// Padding applied to L1 fee estimates (110% = 11000 bips).
45const GAS_ESTIMATION_L1_PRICE_PADDING_BIPS: u64 = 11000;
46
47pub fn create_nodeinterface_precompile() -> DynPrecompile {
48    DynPrecompile::new_stateful(PrecompileId::custom("nodeinterface"), handler)
49}
50
51fn handler(mut input: PrecompileInput<'_>) -> PrecompileResult {
52    let gas_limit = input.gas;
53    let data = input.data;
54    if data.len() < 4 {
55        return Err(PrecompileError::other("input too short"));
56    }
57
58    let selector: [u8; 4] = [data[0], data[1], data[2], data[3]];
59
60    let result = match selector {
61        GAS_ESTIMATE_COMPONENTS => handle_gas_estimate_components(&mut input),
62        GAS_ESTIMATE_L1_COMPONENT => handle_gas_estimate_l1_component(&mut input),
63        NITRO_GENESIS_BLOCK => handle_nitro_genesis_block(&mut input),
64        BLOCK_L1_NUM => handle_block_l1_num(&mut input),
65        // Methods requiring chain-level access (blockchain history, batch data, logs).
66        // These are handled at the RPC layer via InterceptRPCMessage, not as
67        // EVM precompiles. Revert here since the required backend is not available.
68        L2_BLOCK_RANGE_FOR_L1
69        | ESTIMATE_RETRYABLE_TICKET
70        | CONSTRUCT_OUTBOX_PROOF
71        | FIND_BATCH_CONTAINING_BLOCK
72        | GET_L1_CONFIRMATIONS
73        | LEGACY_LOOKUP_MESSAGE_BATCH_PROOF => {
74            Err(PrecompileError::other("method only available via RPC"))
75        }
76        _ => Err(PrecompileError::other("unknown selector")),
77    };
78    crate::gas_check(gas_limit, result)
79}
80
81/// gasEstimateComponents(address,bool,bytes) → (uint64, uint64, uint256, uint256)
82///
83/// Returns: (gasEstimate, gasEstimateForL1, baseFee, l1BaseFeeEstimate)
84///
85/// The full gas estimate requires calling back into eth_estimateGas which
86/// is not possible from within a precompile. We return the L1 component
87/// and basefee; the total estimate is left as 0 (callers should use
88/// eth_estimateGas for the total).
89fn handle_gas_estimate_components(input: &mut PrecompileInput<'_>) -> PrecompileResult {
90    let gas_limit = input.gas;
91    load_arbos(input)?;
92
93    let l1_price = sload_field(input, subspace_slot(L1_PRICING_SUBSPACE, L1_PRICE_PER_UNIT))?;
94    let basefee = sload_field(input, subspace_slot(L2_PRICING_SUBSPACE, L2_BASE_FEE))?;
95
96    // Compute L1 gas cost for a simple transaction.
97    // PosterDataCost computes the L1 fee from the message data, then divides by basefee.
98    // Here we estimate using the calldata from the input parameters.
99    let gas_for_l1 = estimate_l1_gas(input, l1_price, basefee);
100
101    let mut out = Vec::with_capacity(128);
102    // gasEstimate: 0 (full estimate requires eth_estimateGas)
103    out.extend_from_slice(&U256::ZERO.to_be_bytes::<32>());
104    // gasEstimateForL1
105    out.extend_from_slice(&U256::from(gas_for_l1).to_be_bytes::<32>());
106    // baseFee
107    out.extend_from_slice(&basefee.to_be_bytes::<32>());
108    // l1BaseFeeEstimate
109    out.extend_from_slice(&l1_price.to_be_bytes::<32>());
110
111    Ok(PrecompileOutput::new(
112        (2 * SLOAD_GAS + COPY_GAS).min(gas_limit),
113        out.into(),
114    ))
115}
116
117/// gasEstimateL1Component(address,bool,bytes) → (uint64, uint256, uint256)
118///
119/// Returns: (gasEstimateForL1, baseFee, l1BaseFeeEstimate)
120fn handle_gas_estimate_l1_component(input: &mut PrecompileInput<'_>) -> PrecompileResult {
121    let gas_limit = input.gas;
122    load_arbos(input)?;
123
124    let l1_price = sload_field(input, subspace_slot(L1_PRICING_SUBSPACE, L1_PRICE_PER_UNIT))?;
125    let basefee = sload_field(input, subspace_slot(L2_PRICING_SUBSPACE, L2_BASE_FEE))?;
126
127    let gas_for_l1 = estimate_l1_gas(input, l1_price, basefee);
128
129    let mut out = Vec::with_capacity(96);
130    // gasEstimateForL1
131    out.extend_from_slice(&U256::from(gas_for_l1).to_be_bytes::<32>());
132    // baseFee
133    out.extend_from_slice(&basefee.to_be_bytes::<32>());
134    // l1BaseFeeEstimate
135    out.extend_from_slice(&l1_price.to_be_bytes::<32>());
136
137    Ok(PrecompileOutput::new(
138        (2 * SLOAD_GAS + COPY_GAS).min(gas_limit),
139        out.into(),
140    ))
141}
142
143/// nitroGenesisBlock() → uint64
144fn handle_nitro_genesis_block(input: &mut PrecompileInput<'_>) -> PrecompileResult {
145    let gas_limit = input.gas;
146    load_arbos(input)?;
147
148    let genesis_block_num = sload_field(input, root_slot(GENESIS_BLOCK_NUM_OFFSET))?;
149
150    Ok(PrecompileOutput::new(
151        (SLOAD_GAS + COPY_GAS).min(gas_limit),
152        genesis_block_num.to_be_bytes::<32>().to_vec().into(),
153    ))
154}
155
156/// blockL1Num(uint64 blockNum) → uint64
157///
158/// Returns the L1 block number associated with the given L2 block.
159/// Uses the cached L1→L2 block mapping populated during block execution.
160fn handle_block_l1_num(input: &mut PrecompileInput<'_>) -> PrecompileResult {
161    let data = input.data;
162    if data.len() < 4 + 32 {
163        return Err(PrecompileError::other("input too short"));
164    }
165
166    let block_num: u64 = U256::from_be_slice(&data[4..36])
167        .try_into()
168        .unwrap_or(u64::MAX);
169
170    let l1_block = get_cached_l1_block_number(block_num).unwrap_or(0);
171
172    Ok(PrecompileOutput::new(
173        COPY_GAS.min(input.gas),
174        U256::from(l1_block).to_be_bytes::<32>().to_vec().into(),
175    ))
176}
177
178/// Estimate L1 gas from calldata in the input.
179///
180/// Computes: posterDataCost = l1PricePerUnit * txDataNonZeroGas * calldataLen
181/// Then applies 110% padding and divides by basefee.
182fn estimate_l1_gas(input: &PrecompileInput<'_>, l1_price: U256, basefee: U256) -> u64 {
183    // Extract the `bytes data` parameter from calldata.
184    // ABI: selector(4) + address(32) + bool(32) + offset(32) + length(32) + data...
185    let calldata_len = if input.data.len() > 4 + 32 + 32 + 32 + 32 {
186        let len_offset = 4 + 32 + 32 + 32;
187        let len_bytes = &input.data[len_offset..len_offset + 32];
188        U256::from_be_slice(len_bytes).try_into().unwrap_or(0u64)
189    } else {
190        0u64
191    };
192
193    if basefee.is_zero() || l1_price.is_zero() {
194        return 0;
195    }
196
197    // L1 fee = l1PricePerUnit * txDataNonZeroGas * dataLength
198    let l1_fee = l1_price
199        .saturating_mul(U256::from(TX_DATA_NON_ZERO_GAS))
200        .saturating_mul(U256::from(calldata_len));
201
202    // Apply padding (110% = 11000/10000 bips).
203    let padded = l1_fee.saturating_mul(U256::from(GAS_ESTIMATION_L1_PRICE_PADDING_BIPS))
204        / U256::from(10000u64);
205
206    // Convert to gas units: gasForL1 = paddedFee / basefee
207    let gas_for_l1 = padded / basefee;
208
209    gas_for_l1.try_into().unwrap_or(u64::MAX)
210}
211
212fn load_arbos(input: &mut PrecompileInput<'_>) -> Result<(), PrecompileError> {
213    input
214        .internals_mut()
215        .load_account(ARBOS_STATE_ADDRESS)
216        .map_err(|e| PrecompileError::other(format!("load_account: {e:?}")))?;
217    Ok(())
218}
219
220fn sload_field(input: &mut PrecompileInput<'_>, slot: U256) -> Result<U256, PrecompileError> {
221    let val = input
222        .internals_mut()
223        .sload(ARBOS_STATE_ADDRESS, slot)
224        .map_err(|_| PrecompileError::other("sload failed"))?;
225    Ok(val.data)
226}