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