arb_rpc/
nodeinterface_rpc.rs

1//! RPC-layer handlers for NodeInterface (0xc8) methods that require
2//! chain-history or call-stack access beyond what a precompile can do.
3//!
4//! Implemented as an `eth_call` override on `ArbEthApi`. Precompile-level
5//! fallbacks return zero / empty (see `arb_precompiles::nodeinterface`)
6//! so callers that don't go through `eth_call` still get a valid response.
7
8use alloy_primitives::{Address, B256, Bytes, U256, address};
9
10/// NodeInterface virtual contract address.
11pub const NODE_INTERFACE_ADDRESS: Address = address!("00000000000000000000000000000000000000c8");
12
13// Function selectors (keccak256("name(arg types)")[0..4]).
14pub const SEL_GAS_ESTIMATE_COMPONENTS: [u8; 4] = [0xc9, 0x4e, 0x6e, 0xeb];
15pub const SEL_GAS_ESTIMATE_L1_COMPONENT: [u8; 4] = [0x77, 0xd4, 0x88, 0xa2];
16pub const SEL_L2_BLOCK_RANGE_FOR_L1: [u8; 4] = [0x48, 0xe7, 0xf8, 0x11];
17pub const SEL_GET_L1_CONFIRMATIONS: [u8; 4] = [0xe5, 0xca, 0x23, 0x8c];
18pub const SEL_FIND_BATCH_CONTAINING_BLOCK: [u8; 4] = [0x81, 0xf1, 0xad, 0xaf];
19pub const SEL_CONSTRUCT_OUTBOX_PROOF: [u8; 4] = [0x42, 0x69, 0x63, 0x50];
20pub const SEL_NITRO_GENESIS_BLOCK: [u8; 4] = [0x93, 0xa2, 0xfe, 0x21];
21pub const SEL_BLOCK_L1_NUM: [u8; 4] = [0x6f, 0x27, 0x5e, 0xf2];
22pub const SEL_LEGACY_LOOKUP_MESSAGE_BATCH_PROOF: [u8; 4] = [0x89, 0x49, 0x62, 0x70];
23
24/// Decode a packed header's mix_hash field to `(sendCount, l1BlockNumber,
25/// arbosVersion)`.
26pub fn unpack_mix_hash(mix: B256) -> (u64, u64, u64) {
27    let b = mix.0;
28    let send_count = u64::from_be_bytes(b[0..8].try_into().unwrap_or_default());
29    let l1_block = u64::from_be_bytes(b[8..16].try_into().unwrap_or_default());
30    let arbos_version = u64::from_be_bytes(b[16..24].try_into().unwrap_or_default());
31    (send_count, l1_block, arbos_version)
32}
33
34/// Extract the `bytes` parameter (data) from an ABI-encoded
35/// `(address, bool, bytes)` gas-estimate call, returning its length.
36///
37/// Calldata layout:
38///   selector(4) + address(32) + bool(32) + offset(32) + length(32) + data…
39pub fn gas_estimate_data_len(input: &[u8]) -> u64 {
40    if input.len() < 4 + 32 * 4 {
41        return 0;
42    }
43    let len_start = 4 + 32 * 3;
44    let len_bytes = &input[len_start..len_start + 32];
45    U256::from_be_slice(len_bytes).try_into().unwrap_or(0u64)
46}
47
48/// ABI-encode the `(uint64, uint64)` result of `l2BlockRangeForL1`.
49pub fn encode_l2_block_range(first: u64, last: u64) -> Bytes {
50    let mut out = vec![0u8; 64];
51    out[24..32].copy_from_slice(&first.to_be_bytes());
52    out[56..64].copy_from_slice(&last.to_be_bytes());
53    Bytes::from(out)
54}
55
56/// ABI-encode a single `uint64` as a right-aligned 32-byte word.
57pub fn encode_u64_word(v: u64) -> Bytes {
58    let mut out = vec![0u8; 32];
59    out[24..32].copy_from_slice(&v.to_be_bytes());
60    Bytes::from(out)
61}
62
63/// ABI-encode `legacyLookupMessageBatchProof`'s all-zero 9-value tuple. The head
64/// is nine words (0x120), so the empty `proof` array begins at 0x120 and the
65/// empty `calldataForL1` at 0x140; both length words live inside the 0x160 buffer.
66pub fn encode_legacy_lookup_empty() -> Bytes {
67    let mut out = vec![0u8; 0x160];
68    out[..32].copy_from_slice(&U256::from(0x120u64).to_be_bytes::<32>());
69    out[0x100..0x120].copy_from_slice(&U256::from(0x140u64).to_be_bytes::<32>());
70    Bytes::from(out)
71}
72
73/// ABI-encode the `(uint64, uint64, uint256, uint256)` result of
74/// `gasEstimateComponents`: `(gasEstimate, gasEstimateForL1, baseFee,
75/// l1BaseFeeEstimate)`.
76pub fn encode_gas_estimate_components(
77    gas_total: u64,
78    gas_for_l1: u64,
79    basefee: U256,
80    l1_base_fee: U256,
81) -> Bytes {
82    let mut out = vec![0u8; 128];
83    out[24..32].copy_from_slice(&gas_total.to_be_bytes());
84    out[56..64].copy_from_slice(&gas_for_l1.to_be_bytes());
85    out[64..96].copy_from_slice(&basefee.to_be_bytes::<32>());
86    out[96..128].copy_from_slice(&l1_base_fee.to_be_bytes::<32>());
87    Bytes::from(out)
88}
89
90/// Decode the `uint64` argument from the selector `blockL1Num(uint64)` /
91/// `l2BlockRangeForL1(uint64)` / `findBatchContainingBlock(uint64)` /
92/// `nitroGenesisBlock()` (no arg, returns 0).
93pub fn decode_single_u64_arg(input: &[u8]) -> Option<u64> {
94    if input.len() < 4 + 32 {
95        return None;
96    }
97    U256::from_be_slice(&input[4..36]).try_into().ok()
98}
99
100/// Binary-search headers to find the block-range that was emitted against
101/// the given L1 block. The predicate on each header is
102/// `l1_block_number_from_mix_hash(header.mix_hash)`.
103///
104/// Returns `(first_block, last_block)` inclusive. If no L2 block maps to
105/// `target_l1_block`, returns `None`.
106pub fn find_l2_block_range<F>(target_l1_block: u64, best: u64, mix_hash_of: F) -> Option<(u64, u64)>
107where
108    F: Fn(u64) -> Option<B256>,
109{
110    if best == 0 {
111        return None;
112    }
113
114    // Lower bound: smallest L2 block N such that l1BlockNumber(N) >= target.
115    let mut lo = 0u64;
116    let mut hi = best;
117    while lo < hi {
118        let mid = lo + (hi - lo) / 2;
119        let mix = mix_hash_of(mid)?;
120        let (_, l1_bn, _) = unpack_mix_hash(mix);
121        if l1_bn < target_l1_block {
122            lo = mid + 1;
123        } else {
124            hi = mid;
125        }
126    }
127    let first = lo;
128    // Check we actually matched (may have overshot into a different L1 block).
129    let first_mix = mix_hash_of(first)?;
130    let (_, first_l1, _) = unpack_mix_hash(first_mix);
131    if first_l1 != target_l1_block {
132        return None;
133    }
134
135    // Upper bound: largest L2 block N such that l1BlockNumber(N) <= target.
136    let mut lo = first;
137    let mut hi = best;
138    while lo < hi {
139        let mid = lo + (hi - lo).div_ceil(2);
140        let mix = mix_hash_of(mid)?;
141        let (_, l1_bn, _) = unpack_mix_hash(mix);
142        if l1_bn > target_l1_block {
143            hi = mid - 1;
144        } else {
145            lo = mid;
146        }
147    }
148    Some((first, lo))
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn unpack_mix_hash_layout() {
157        let mut mix = [0u8; 32];
158        mix[0..8].copy_from_slice(&42u64.to_be_bytes());
159        mix[8..16].copy_from_slice(&100u64.to_be_bytes());
160        mix[16..24].copy_from_slice(&30u64.to_be_bytes());
161        let (sc, l1, v) = unpack_mix_hash(B256::from(mix));
162        assert_eq!(sc, 42);
163        assert_eq!(l1, 100);
164        assert_eq!(v, 30);
165    }
166
167    #[test]
168    fn gas_estimate_data_len_parses_abi_length() {
169        let mut input = vec![0u8; 4 + 32 * 4 + 10];
170        input[0..4].copy_from_slice(&SEL_GAS_ESTIMATE_COMPONENTS);
171        // length is at offset 4 + 96
172        let len_start = 4 + 96;
173        input[len_start + 24..len_start + 32].copy_from_slice(&10u64.to_be_bytes());
174        assert_eq!(gas_estimate_data_len(&input), 10);
175    }
176
177    #[test]
178    fn gas_estimate_data_len_short_input_zero() {
179        assert_eq!(gas_estimate_data_len(&[]), 0);
180        assert_eq!(gas_estimate_data_len(&[0u8; 100]), 0);
181    }
182
183    #[test]
184    fn encode_l2_block_range_pads_correctly() {
185        let out = encode_l2_block_range(5, 10);
186        assert_eq!(out.len(), 64);
187        assert_eq!(U256::from_be_slice(&out[0..32]), U256::from(5u64));
188        assert_eq!(U256::from_be_slice(&out[32..64]), U256::from(10u64));
189    }
190
191    #[test]
192    fn encode_gas_estimate_components_layout() {
193        let out = encode_gas_estimate_components(
194            100_000,
195            5_000,
196            U256::from(1_000_000u64),
197            U256::from(50_000_000u64),
198        );
199        assert_eq!(out.len(), 128);
200        assert_eq!(U256::from_be_slice(&out[0..32]), U256::from(100_000u64));
201        assert_eq!(U256::from_be_slice(&out[32..64]), U256::from(5_000u64));
202        assert_eq!(U256::from_be_slice(&out[64..96]), U256::from(1_000_000u64));
203        assert_eq!(
204            U256::from_be_slice(&out[96..128]),
205            U256::from(50_000_000u64)
206        );
207    }
208
209    #[test]
210    fn decode_single_u64_arg_reads_last_8_bytes() {
211        let mut input = vec![0u8; 4 + 32];
212        input[4 + 24..4 + 32].copy_from_slice(&12345u64.to_be_bytes());
213        assert_eq!(decode_single_u64_arg(&input), Some(12345));
214    }
215
216    #[test]
217    fn decode_single_u64_arg_rejects_short_input() {
218        assert_eq!(decode_single_u64_arg(&[0u8; 10]), None);
219    }
220
221    fn mix_with_l1_block(l1: u64) -> B256 {
222        let mut m = [0u8; 32];
223        m[8..16].copy_from_slice(&l1.to_be_bytes());
224        B256::from(m)
225    }
226
227    #[test]
228    fn find_l2_block_range_hits_exact_l1() {
229        // L2 blocks 0..10, each maps to L1 block = 1000 + (L2 / 3).
230        let mix_hash_of = |l2: u64| Some(mix_with_l1_block(1000 + l2 / 3));
231        let range = find_l2_block_range(1001, 10, mix_hash_of);
232        // L1 block 1001 = L2 blocks 3..=5
233        assert_eq!(range, Some((3, 5)));
234    }
235
236    #[test]
237    fn find_l2_block_range_miss_returns_none() {
238        let mix_hash_of = |l2: u64| Some(mix_with_l1_block(1000 + l2 / 3));
239        // Query a higher L1 block than any recorded.
240        assert_eq!(find_l2_block_range(9999, 10, mix_hash_of), None);
241    }
242
243    #[test]
244    fn find_l2_block_range_empty_chain() {
245        assert_eq!(find_l2_block_range(1, 0, |_| None), None);
246    }
247
248    #[test]
249    fn find_l2_block_range_first_block_match() {
250        let mix_hash_of = |l2: u64| Some(mix_with_l1_block(1000 + l2));
251        assert_eq!(find_l2_block_range(1000, 5, mix_hash_of), Some((0, 0)));
252    }
253
254    #[test]
255    fn find_l2_block_range_all_same_l1() {
256        let mix_hash_of = |_: u64| Some(mix_with_l1_block(42));
257        assert_eq!(find_l2_block_range(42, 5, mix_hash_of), Some((0, 5)));
258    }
259
260    #[test]
261    fn legacy_lookup_empty_offsets_in_bounds() {
262        let out = encode_legacy_lookup_empty();
263        assert_eq!(out.len(), 0x160);
264        // proof is return value 0 (head word at 0x00); calldataForL1 is value 8
265        // (head word at 0x100). Each dynamic offset must point at an in-bounds,
266        // zero-length word.
267        for head_off in [0x00usize, 0x100] {
268            let off = U256::from_be_slice(&out[head_off..head_off + 32]).to::<usize>();
269            assert!(
270                off + 32 <= out.len(),
271                "dynamic offset {off:#x} out of bounds"
272            );
273            let len = U256::from_be_slice(&out[off..off + 32]);
274            assert_eq!(len, U256::ZERO, "empty dynamic field must have zero length");
275        }
276    }
277}