1use alloy_primitives::{Address, B256, Bytes, U256, address};
9
10pub const NODE_INTERFACE_ADDRESS: Address = address!("00000000000000000000000000000000000000c8");
12
13pub 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
24pub 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
34pub 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
48pub 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
56pub 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
63pub 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
73pub 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
90pub 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
100pub 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 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 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 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 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 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 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 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 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}