arb_precompiles/
nodeinterface.rs

1use std::sync::Arc;
2
3use alloy_consensus::{SignableTransaction, TxEip1559, TxEnvelope};
4use alloy_evm::precompiles::{DynPrecompile, PrecompileInput};
5use alloy_primitives::{Address, Bytes, ChainId, Signature, U256, keccak256};
6use alloy_sol_types::SolInterface;
7use arb_context::ArbPrecompileCtx;
8use arb_storage::ARBOS_STATE_ADDRESS;
9use revm::precompile::{PrecompileId, PrecompileOutput, PrecompileResult};
10
11use crate::{ArbPrecompileError, interfaces::INodeInterface};
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
19const SLOAD_GAS: u64 = 800;
20const COPY_GAS: u64 = 3;
21
22pub fn create_nodeinterface_precompile(ctx: Arc<ArbPrecompileCtx>) -> DynPrecompile {
23    DynPrecompile::new_stateful(PrecompileId::custom("nodeinterface"), move |input| {
24        handler(input, &ctx)
25    })
26}
27
28fn handler(mut input: PrecompileInput<'_>, ctx: &ArbPrecompileCtx) -> PrecompileResult {
29    let mut gas_used = 0u64;
30    let gas_limit = input.gas;
31    crate::init_precompile_gas(&mut gas_used, ctx, input.data.len());
32
33    let call = match INodeInterface::NodeInterfaceCalls::abi_decode(input.data) {
34        Ok(c) => c,
35        Err(_) => return crate::burn_all_revert(gas_limit),
36    };
37
38    use INodeInterface::NodeInterfaceCalls as Calls;
39    let result = match call {
40        Calls::gasEstimateComponents(_) => handle_gas_estimate_components(&mut input, ctx),
41        Calls::gasEstimateL1Component(_) => handle_gas_estimate_l1_component(&mut input, ctx),
42        Calls::nitroGenesisBlock(_) => handle_nitro_genesis_block(&mut input, ctx),
43        Calls::blockL1Num(c) => handle_block_l1_num(&input, ctx, c.l2BlockNum),
44        Calls::getL1Confirmations(_) => handle_zero_u64(&input),
45        Calls::findBatchContainingBlock(_) => handle_zero_u64(&input),
46        Calls::legacyLookupMessageBatchProof(_) => handle_legacy_lookup_empty(&input),
47        Calls::l2BlockRangeForL1(_)
48        | Calls::estimateRetryableTicket(_)
49        | Calls::constructOutboxProof(_) => Err(ArbPrecompileError::empty_revert(gas_used).into()),
50    };
51    crate::gas_check(ctx, gas_limit, gas_used, result)
52}
53
54/// gasEstimateComponents(address,bool,bytes) → (uint64, uint64, uint256, uint256)
55///
56/// Returns: (gasEstimate, gasEstimateForL1, baseFee, l1BaseFeeEstimate).
57/// `gasEstimate` is left as 0 — the full estimate requires eth_estimateGas
58/// which can't be invoked from a precompile.
59fn handle_gas_estimate_components(
60    input: &mut PrecompileInput<'_>,
61    ctx: &ArbPrecompileCtx,
62) -> PrecompileResult {
63    let gas_limit = input.gas;
64    load_arbos(input)?;
65
66    let (l1_price, basefee, min_basefee, chain_id, brotli_level) =
67        read_estimate_fields(input, ctx)?;
68    let gas_for_l1 = estimate_l1_gas(
69        input,
70        l1_price,
71        basefee,
72        min_basefee,
73        chain_id,
74        brotli_level,
75    );
76
77    let mut out = Vec::with_capacity(128);
78    out.extend_from_slice(&U256::ZERO.to_be_bytes::<32>());
79    out.extend_from_slice(&U256::from(gas_for_l1).to_be_bytes::<32>());
80    out.extend_from_slice(&basefee.to_be_bytes::<32>());
81    out.extend_from_slice(&l1_price.to_be_bytes::<32>());
82
83    Ok(PrecompileOutput::new(
84        (2 * SLOAD_GAS + COPY_GAS).min(gas_limit),
85        out.into(),
86    ))
87}
88
89/// gasEstimateL1Component(address,bool,bytes) → (uint64, uint256, uint256)
90///
91/// Returns: (gasEstimateForL1, baseFee, l1BaseFeeEstimate).
92fn handle_gas_estimate_l1_component(
93    input: &mut PrecompileInput<'_>,
94    ctx: &ArbPrecompileCtx,
95) -> PrecompileResult {
96    let gas_limit = input.gas;
97    load_arbos(input)?;
98
99    let (l1_price, basefee, min_basefee, chain_id, brotli_level) =
100        read_estimate_fields(input, ctx)?;
101    let gas_for_l1 = estimate_l1_gas(
102        input,
103        l1_price,
104        basefee,
105        min_basefee,
106        chain_id,
107        brotli_level,
108    );
109
110    let mut out = Vec::with_capacity(96);
111    out.extend_from_slice(&U256::from(gas_for_l1).to_be_bytes::<32>());
112    out.extend_from_slice(&basefee.to_be_bytes::<32>());
113    out.extend_from_slice(&l1_price.to_be_bytes::<32>());
114
115    Ok(PrecompileOutput::new(
116        (2 * SLOAD_GAS + COPY_GAS).min(gas_limit),
117        out.into(),
118    ))
119}
120
121/// nitroGenesisBlock() → uint64
122fn handle_nitro_genesis_block(
123    input: &mut PrecompileInput<'_>,
124    ctx: &ArbPrecompileCtx,
125) -> PrecompileResult {
126    let gas_limit = input.gas;
127    load_arbos(input)?;
128
129    let internals = input.internals_mut();
130    let arb_state = ctx
131        .block
132        .arbos_state(internals)
133        .map_err(ArbPrecompileError::fatal)?;
134    let genesis_block_num = arb_state
135        .genesis_block_num
136        .get(internals)
137        .map_err(ArbPrecompileError::fatal)?;
138
139    Ok(PrecompileOutput::new(
140        (SLOAD_GAS + COPY_GAS).min(gas_limit),
141        U256::from(genesis_block_num)
142            .to_be_bytes::<32>()
143            .to_vec()
144            .into(),
145    ))
146}
147
148fn handle_block_l1_num(
149    input: &PrecompileInput<'_>,
150    ctx: &ArbPrecompileCtx,
151    block_num: u64,
152) -> PrecompileResult {
153    let l1_block = ctx.block.cached_l1_block_number(block_num).unwrap_or(0);
154    Ok(PrecompileOutput::new(
155        COPY_GAS.min(input.gas),
156        U256::from(l1_block).to_be_bytes::<32>().to_vec().into(),
157    ))
158}
159
160fn handle_zero_u64(input: &PrecompileInput<'_>) -> PrecompileResult {
161    Ok(PrecompileOutput::new(
162        COPY_GAS.min(input.gas),
163        U256::ZERO.to_be_bytes::<32>().to_vec().into(),
164    ))
165}
166
167/// legacyLookupMessageBatchProof returns the 9-value all-zero tuple —
168/// the classic-chain outbox isn't reachable from arbreth.
169///
170/// ABI return:
171///   (bytes32[] proof, uint256 path, address l2Sender, address l1Dest,
172///    uint256 l2Block, uint256 l1Block, uint256 timestamp, uint256 amount,
173///    bytes calldataForL1)
174fn handle_legacy_lookup_empty(input: &PrecompileInput<'_>) -> PrecompileResult {
175    let mut out = vec![0u8; 0x160];
176    U256::from(0x140u64)
177        .to_be_bytes::<32>()
178        .iter()
179        .enumerate()
180        .for_each(|(i, b)| out[i] = *b);
181    U256::from(0x160u64)
182        .to_be_bytes::<32>()
183        .iter()
184        .enumerate()
185        .for_each(|(i, b)| out[0x100 + i] = *b);
186    Ok(PrecompileOutput::new(COPY_GAS.min(input.gas), out.into()))
187}
188
189fn read_estimate_fields(
190    input: &mut PrecompileInput<'_>,
191    ctx: &ArbPrecompileCtx,
192) -> Result<(U256, U256, U256, ChainId, u64), ArbPrecompileError> {
193    let internals = input.internals_mut();
194    let arb_state = ctx
195        .block
196        .arbos_state(internals)
197        .map_err(ArbPrecompileError::fatal)?;
198
199    let l1_price = arb_state
200        .l1_pricing_state
201        .price_per_unit(internals)
202        .map_err(ArbPrecompileError::fatal)?;
203    let basefee = arb_state
204        .l2_pricing_state
205        .base_fee_wei(internals)
206        .map_err(ArbPrecompileError::fatal)?;
207    let min_basefee = arb_state
208        .l2_pricing_state
209        .min_base_fee_wei(internals)
210        .map_err(ArbPrecompileError::fatal)?;
211    let chain_id_u256 = arb_state
212        .chain_id
213        .get(internals)
214        .map_err(ArbPrecompileError::fatal)?;
215    let chain_id: ChainId = chain_id_u256.try_into().unwrap_or(0);
216    let brotli_level = arb_state
217        .brotli_compression_level
218        .get(internals)
219        .map_err(ArbPrecompileError::fatal)?;
220
221    Ok((l1_price, basefee, min_basefee, chain_id, brotli_level))
222}
223
224fn estimate_l1_gas(
225    input: &PrecompileInput<'_>,
226    l1_price: U256,
227    basefee: U256,
228    min_basefee: U256,
229    chain_id: ChainId,
230    brotli_level: u64,
231) -> u64 {
232    let (to_addr, contract_creation, data) = match decode_estimate_args(input.data) {
233        Some(v) => v,
234        None => return 0,
235    };
236    compute_l1_gas_for_estimate(
237        chain_id,
238        to_addr,
239        contract_creation,
240        U256::ZERO,
241        data,
242        l1_price,
243        basefee,
244        min_basefee,
245        brotli_level,
246    )
247}
248
249/// L1 gas estimate: brotli-compress a fake EIP-1559 tx, pad units by
250/// `(units + 256) * 1.01`, multiply by `pricePerUnit`, pad posterCost by
251/// `1.10`, then divide by `max(basefee * 7/8, minBaseFee)`.
252pub fn compute_l1_gas_for_estimate(
253    chain_id: ChainId,
254    to: Address,
255    contract_creation: bool,
256    value: U256,
257    data: Bytes,
258    l1_price: U256,
259    basefee: U256,
260    min_basefee: U256,
261    brotli_level: u64,
262) -> u64 {
263    if basefee.is_zero() || l1_price.is_zero() {
264        return 0;
265    }
266    let tx_bytes = build_fake_tx_bytes(chain_id, to, contract_creation, value, data);
267    let raw_units = arbos::l1_pricing::poster_units_from_bytes(&tx_bytes, brotli_level);
268    let padded_units = raw_units
269        .saturating_add(arbos::l1_pricing::ESTIMATION_PADDING_UNITS)
270        .saturating_mul(10_000 + arbos::l1_pricing::ESTIMATION_PADDING_BASIS_POINTS)
271        / 10_000;
272    let poster_cost = l1_price.saturating_mul(U256::from(padded_units));
273    let posting_padded = poster_cost.saturating_mul(U256::from(11_000u64)) / U256::from(10_000u64);
274    let adjusted = basefee.saturating_mul(U256::from(7u64)) / U256::from(8u64);
275    let gas_price = if adjusted < min_basefee {
276        min_basefee
277    } else {
278        adjusted
279    };
280    if gas_price.is_zero() {
281        return 0;
282    }
283    (posting_padded / gas_price).try_into().unwrap_or(u64::MAX)
284}
285
286/// Decode `gasEstimateComponents(address,bool,bytes)` calldata into
287/// `(to, contractCreation, data)`.
288pub fn decode_estimate_args(data: &[u8]) -> Option<(Address, bool, Bytes)> {
289    if data.len() < 4 + 4 * 32 {
290        return None;
291    }
292    let to = Address::from_slice(&data[16..36]);
293    let creation = data[4 + 32 + 31] != 0;
294    let bytes_offset: usize = U256::from_be_slice(&data[4 + 64..4 + 96]).try_into().ok()?;
295    let bytes_pos = 4usize.checked_add(bytes_offset)?;
296    if data.len() < bytes_pos + 32 {
297        return None;
298    }
299    let bytes_len: usize = U256::from_be_slice(&data[bytes_pos..bytes_pos + 32])
300        .try_into()
301        .ok()?;
302    let data_start = bytes_pos + 32;
303    if data.len() < data_start + bytes_len {
304        return None;
305    }
306    Some((
307        to,
308        creation,
309        Bytes::copy_from_slice(&data[data_start..data_start + bytes_len]),
310    ))
311}
312
313fn hash_prefix_u64(input: &[u8]) -> u64 {
314    let [b0, b1, b2, b3, b4, b5, b6, b7, ..] = keccak256(input).0;
315    u64::from_be_bytes([b0, b1, b2, b3, b4, b5, b6, b7])
316}
317
318fn hash_prefix_u32(input: &[u8]) -> u32 {
319    let [b0, b1, b2, b3, ..] = keccak256(input).0;
320    u32::from_be_bytes([b0, b1, b2, b3])
321}
322
323/// Build the EIP-2718 envelope of a fake EIP-1559 tx used to size the
324/// calldata payload for gas estimation (hard-coded random
325/// nonce/tip/feeCap/gas/sig fields).
326pub fn build_fake_tx_bytes(
327    chain_id: ChainId,
328    to: Address,
329    contract_creation: bool,
330    value: U256,
331    data: Bytes,
332) -> Vec<u8> {
333    let nonce = hash_prefix_u64(b"Nonce");
334    let max_priority = u128::from(hash_prefix_u32(b"GasTipCap"));
335    let max_fee = u128::from(hash_prefix_u32(b"GasFeeCap"));
336    let gas_limit = u64::from(hash_prefix_u32(b"Gas"));
337    let r = U256::from_be_bytes(keccak256(b"R").0);
338    let s = U256::from_be_bytes(keccak256(b"S").0);
339
340    let kind = if contract_creation {
341        revm::primitives::TxKind::Create
342    } else {
343        revm::primitives::TxKind::Call(to)
344    };
345
346    let tx = TxEip1559 {
347        chain_id,
348        nonce,
349        gas_limit,
350        max_fee_per_gas: max_fee,
351        max_priority_fee_per_gas: max_priority,
352        to: kind,
353        value,
354        access_list: Default::default(),
355        input: data,
356    };
357
358    let signature = Signature::new(r, s, false);
359    let signed = tx.into_signed(signature);
360    use alloy_eips::eip2718::Encodable2718;
361    let envelope = TxEnvelope::Eip1559(signed);
362    envelope.encoded_2718()
363}
364
365fn load_arbos(input: &mut PrecompileInput<'_>) -> Result<(), ArbPrecompileError> {
366    input
367        .internals_mut()
368        .load_account(ARBOS_STATE_ADDRESS)
369        .map_err(ArbPrecompileError::fatal)?;
370    Ok(())
371}