1use alloy_consensus::{SignableTransaction, TxEip1559, TxEnvelope};
2use alloy_evm::precompiles::{DynPrecompile, PrecompileInput};
3use alloy_primitives::{keccak256, Address, Bytes, ChainId, Signature, U256};
4use alloy_sol_types::SolInterface;
5use revm::precompile::{PrecompileError, PrecompileId, PrecompileOutput, PrecompileResult};
6
7use crate::{
8 arbsys::get_cached_l1_block_number,
9 interfaces::INodeInterface,
10 storage_slot::{
11 root_slot, subspace_slot, ARBOS_STATE_ADDRESS, GENESIS_BLOCK_NUM_OFFSET,
12 L1_PRICING_SUBSPACE, L2_PRICING_SUBSPACE,
13 },
14};
15
16pub const NODE_INTERFACE_ADDRESS: Address = Address::new([
18 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
19 0x00, 0x00, 0x00, 0xc8,
20]);
21
22const L1_PRICE_PER_UNIT: u64 = 7;
24
25const L2_BASE_FEE: u64 = 2;
27const L2_MIN_BASE_FEE: u64 = 3;
28
29const SLOAD_GAS: u64 = 800;
31const COPY_GAS: u64 = 3;
32
33pub fn create_nodeinterface_precompile() -> DynPrecompile {
34 DynPrecompile::new_stateful(PrecompileId::custom("nodeinterface"), handler)
35}
36
37fn handler(mut input: PrecompileInput<'_>) -> PrecompileResult {
38 let gas_limit = input.gas;
39 crate::init_precompile_gas(input.data.len());
40
41 let call = match INodeInterface::NodeInterfaceCalls::abi_decode(input.data) {
42 Ok(c) => c,
43 Err(_) => return crate::burn_all_revert(gas_limit),
44 };
45
46 use INodeInterface::NodeInterfaceCalls as Calls;
47 let result = match call {
48 Calls::gasEstimateComponents(_) => handle_gas_estimate_components(&mut input),
49 Calls::gasEstimateL1Component(_) => handle_gas_estimate_l1_component(&mut input),
50 Calls::nitroGenesisBlock(_) => handle_nitro_genesis_block(&mut input),
51 Calls::blockL1Num(c) => handle_block_l1_num(&input, c.l2BlockNum),
52 Calls::getL1Confirmations(_) => handle_zero_u64(&input),
56 Calls::findBatchContainingBlock(_) => handle_zero_u64(&input),
57 Calls::legacyLookupMessageBatchProof(_) => handle_legacy_lookup_empty(&input),
58 Calls::l2BlockRangeForL1(_)
59 | Calls::estimateRetryableTicket(_)
60 | Calls::constructOutboxProof(_) => {
61 Err(PrecompileError::other("method only available via RPC"))
62 }
63 };
64 crate::gas_check(gas_limit, result)
65}
66
67fn handle_gas_estimate_components(input: &mut PrecompileInput<'_>) -> PrecompileResult {
76 let gas_limit = input.gas;
77 load_arbos(input)?;
78
79 let l1_price = sload_field(input, subspace_slot(L1_PRICING_SUBSPACE, L1_PRICE_PER_UNIT))?;
80 let basefee = sload_field(input, subspace_slot(L2_PRICING_SUBSPACE, L2_BASE_FEE))?;
81 let min_basefee = sload_field(input, subspace_slot(L2_PRICING_SUBSPACE, L2_MIN_BASE_FEE))?;
82 let chain_id_u256 = sload_field(input, root_slot(crate::storage_slot::CHAIN_ID_OFFSET))?;
83 let chain_id: ChainId = chain_id_u256.try_into().unwrap_or(0);
84 let brotli_level = sload_field(
85 input,
86 root_slot(crate::storage_slot::BROTLI_COMPRESSION_LEVEL_OFFSET),
87 )?
88 .try_into()
89 .unwrap_or(0u64);
90
91 let gas_for_l1 = estimate_l1_gas(
92 input,
93 l1_price,
94 basefee,
95 min_basefee,
96 chain_id,
97 brotli_level,
98 );
99
100 let mut out = Vec::with_capacity(128);
101 out.extend_from_slice(&U256::ZERO.to_be_bytes::<32>());
103 out.extend_from_slice(&U256::from(gas_for_l1).to_be_bytes::<32>());
105 out.extend_from_slice(&basefee.to_be_bytes::<32>());
107 out.extend_from_slice(&l1_price.to_be_bytes::<32>());
109
110 Ok(PrecompileOutput::new(
111 (2 * SLOAD_GAS + COPY_GAS).min(gas_limit),
112 out.into(),
113 ))
114}
115
116fn handle_gas_estimate_l1_component(input: &mut PrecompileInput<'_>) -> PrecompileResult {
120 let gas_limit = input.gas;
121 load_arbos(input)?;
122
123 let l1_price = sload_field(input, subspace_slot(L1_PRICING_SUBSPACE, L1_PRICE_PER_UNIT))?;
124 let basefee = sload_field(input, subspace_slot(L2_PRICING_SUBSPACE, L2_BASE_FEE))?;
125 let min_basefee = sload_field(input, subspace_slot(L2_PRICING_SUBSPACE, L2_MIN_BASE_FEE))?;
126 let chain_id_u256 = sload_field(input, root_slot(crate::storage_slot::CHAIN_ID_OFFSET))?;
127 let chain_id: ChainId = chain_id_u256.try_into().unwrap_or(0);
128 let brotli_level = sload_field(
129 input,
130 root_slot(crate::storage_slot::BROTLI_COMPRESSION_LEVEL_OFFSET),
131 )?
132 .try_into()
133 .unwrap_or(0u64);
134
135 let gas_for_l1 = estimate_l1_gas(
136 input,
137 l1_price,
138 basefee,
139 min_basefee,
140 chain_id,
141 brotli_level,
142 );
143
144 let mut out = Vec::with_capacity(96);
145 out.extend_from_slice(&U256::from(gas_for_l1).to_be_bytes::<32>());
147 out.extend_from_slice(&basefee.to_be_bytes::<32>());
149 out.extend_from_slice(&l1_price.to_be_bytes::<32>());
151
152 Ok(PrecompileOutput::new(
153 (2 * SLOAD_GAS + COPY_GAS).min(gas_limit),
154 out.into(),
155 ))
156}
157
158fn handle_nitro_genesis_block(input: &mut PrecompileInput<'_>) -> PrecompileResult {
160 let gas_limit = input.gas;
161 load_arbos(input)?;
162
163 let genesis_block_num = sload_field(input, root_slot(GENESIS_BLOCK_NUM_OFFSET))?;
164
165 Ok(PrecompileOutput::new(
166 (SLOAD_GAS + COPY_GAS).min(gas_limit),
167 genesis_block_num.to_be_bytes::<32>().to_vec().into(),
168 ))
169}
170
171fn handle_block_l1_num(input: &PrecompileInput<'_>, block_num: u64) -> PrecompileResult {
172 let l1_block = get_cached_l1_block_number(block_num).unwrap_or(0);
173 Ok(PrecompileOutput::new(
174 COPY_GAS.min(input.gas),
175 U256::from(l1_block).to_be_bytes::<32>().to_vec().into(),
176 ))
177}
178
179fn handle_zero_u64(input: &PrecompileInput<'_>) -> PrecompileResult {
182 Ok(PrecompileOutput::new(
183 COPY_GAS.min(input.gas),
184 U256::ZERO.to_be_bytes::<32>().to_vec().into(),
185 ))
186}
187
188fn handle_legacy_lookup_empty(input: &PrecompileInput<'_>) -> PrecompileResult {
196 let mut out = vec![0u8; 0x160];
211 U256::from(0x140u64)
213 .to_be_bytes::<32>()
214 .iter()
215 .enumerate()
216 .for_each(|(i, b)| out[i] = *b);
217 U256::from(0x160u64)
219 .to_be_bytes::<32>()
220 .iter()
221 .enumerate()
222 .for_each(|(i, b)| out[0x100 + i] = *b);
223 Ok(PrecompileOutput::new(COPY_GAS.min(input.gas), out.into()))
224}
225
226fn estimate_l1_gas(
227 input: &PrecompileInput<'_>,
228 l1_price: U256,
229 basefee: U256,
230 min_basefee: U256,
231 chain_id: ChainId,
232 brotli_level: u64,
233) -> u64 {
234 let (to_addr, contract_creation, data) = match decode_estimate_args(input.data) {
235 Some(v) => v,
236 None => return 0,
237 };
238 compute_l1_gas_for_estimate(
239 chain_id,
240 to_addr,
241 contract_creation,
242 U256::ZERO,
243 data,
244 l1_price,
245 basefee,
246 min_basefee,
247 brotli_level,
248 )
249}
250
251pub fn compute_l1_gas_for_estimate(
255 chain_id: ChainId,
256 to: Address,
257 contract_creation: bool,
258 value: U256,
259 data: Bytes,
260 l1_price: U256,
261 basefee: U256,
262 min_basefee: U256,
263 brotli_level: u64,
264) -> u64 {
265 if basefee.is_zero() || l1_price.is_zero() {
266 return 0;
267 }
268 let tx_bytes = build_fake_tx_bytes(chain_id, to, contract_creation, value, data);
269 let raw_units = arbos::l1_pricing::poster_units_from_bytes(&tx_bytes, brotli_level);
270 let padded_units = raw_units
271 .saturating_add(arbos::l1_pricing::ESTIMATION_PADDING_UNITS)
272 .saturating_mul(10_000 + arbos::l1_pricing::ESTIMATION_PADDING_BASIS_POINTS)
273 / 10_000;
274 let poster_cost = l1_price.saturating_mul(U256::from(padded_units));
275 let posting_padded = poster_cost.saturating_mul(U256::from(11_000u64)) / U256::from(10_000u64);
276 let adjusted = basefee.saturating_mul(U256::from(7u64)) / U256::from(8u64);
277 let gas_price = if adjusted < min_basefee {
278 min_basefee
279 } else {
280 adjusted
281 };
282 if gas_price.is_zero() {
283 return 0;
284 }
285 (posting_padded / gas_price).try_into().unwrap_or(u64::MAX)
286}
287
288pub fn decode_estimate_args(data: &[u8]) -> Option<(Address, bool, Bytes)> {
291 if data.len() < 4 + 4 * 32 {
292 return None;
293 }
294 let to = Address::from_slice(&data[16..36]);
295 let creation = data[4 + 32 + 31] != 0;
296 let bytes_offset: usize = U256::from_be_slice(&data[4 + 64..4 + 96]).try_into().ok()?;
297 let bytes_pos = 4usize.checked_add(bytes_offset)?;
298 if data.len() < bytes_pos + 32 {
299 return None;
300 }
301 let bytes_len: usize = U256::from_be_slice(&data[bytes_pos..bytes_pos + 32])
302 .try_into()
303 .ok()?;
304 let data_start = bytes_pos + 32;
305 if data.len() < data_start + bytes_len {
306 return None;
307 }
308 Some((
309 to,
310 creation,
311 Bytes::copy_from_slice(&data[data_start..data_start + bytes_len]),
312 ))
313}
314
315pub fn build_fake_tx_bytes(
319 chain_id: ChainId,
320 to: Address,
321 contract_creation: bool,
322 value: U256,
323 data: Bytes,
324) -> Vec<u8> {
325 let nonce = u64::from_be_bytes(keccak256(b"Nonce")[..8].try_into().unwrap());
326 let max_priority = u128::from(u32::from_be_bytes(
327 keccak256(b"GasTipCap")[..4].try_into().unwrap(),
328 ));
329 let max_fee = u128::from(u32::from_be_bytes(
330 keccak256(b"GasFeeCap")[..4].try_into().unwrap(),
331 ));
332 let gas_limit = u64::from(u32::from_be_bytes(
333 keccak256(b"Gas")[..4].try_into().unwrap(),
334 ));
335 let r = U256::from_be_bytes(keccak256(b"R").0);
336 let s = U256::from_be_bytes(keccak256(b"S").0);
337
338 let kind = if contract_creation {
339 revm::primitives::TxKind::Create
340 } else {
341 revm::primitives::TxKind::Call(to)
342 };
343
344 let tx = TxEip1559 {
345 chain_id,
346 nonce,
347 gas_limit,
348 max_fee_per_gas: max_fee,
349 max_priority_fee_per_gas: max_priority,
350 to: kind,
351 value,
352 access_list: Default::default(),
353 input: data,
354 };
355
356 let signature = Signature::new(r, s, false);
357 let signed = tx.into_signed(signature);
358 use alloy_eips::eip2718::Encodable2718;
359 let envelope = TxEnvelope::Eip1559(signed);
360 envelope.encoded_2718()
361}
362
363fn load_arbos(input: &mut PrecompileInput<'_>) -> Result<(), PrecompileError> {
364 input
365 .internals_mut()
366 .load_account(ARBOS_STATE_ADDRESS)
367 .map_err(|e| PrecompileError::other(format!("load_account: {e:?}")))?;
368 Ok(())
369}
370
371fn sload_field(input: &mut PrecompileInput<'_>, slot: U256) -> Result<U256, PrecompileError> {
372 let val = input
373 .internals_mut()
374 .sload(ARBOS_STATE_ADDRESS, slot)
375 .map_err(|_| PrecompileError::other("sload failed"))?;
376 crate::charge_precompile_gas(SLOAD_GAS);
377 Ok(val.data)
378}