arb_precompiles/
arbaddresstable.rs

1use alloy_evm::precompiles::{DynPrecompile, PrecompileInput};
2use alloy_primitives::{Address, U256};
3use revm::precompile::{PrecompileError, PrecompileId, PrecompileOutput, PrecompileResult};
4
5use crate::storage_slot::{
6    derive_subspace_key, map_slot, map_slot_b256, ADDRESS_TABLE_SUBSPACE, ARBOS_STATE_ADDRESS,
7    ROOT_STORAGE_KEY,
8};
9
10/// ArbAddressTable precompile address (0x66).
11pub const ARBADDRESSTABLE_ADDRESS: Address = Address::new([
12    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
13    0x00, 0x00, 0x00, 0x66,
14]);
15
16// Function selectors.
17const ADDRESS_EXISTS: [u8; 4] = [0xa5, 0x02, 0x52, 0x22];
18const COMPRESS: [u8; 4] = [0xf6, 0xa4, 0x55, 0xa2];
19const DECOMPRESS: [u8; 4] = [0x31, 0x86, 0x2a, 0xda];
20const LOOKUP: [u8; 4] = [0xd4, 0xb6, 0xb5, 0xda];
21const LOOKUP_INDEX: [u8; 4] = [0x8a, 0x18, 0x67, 0x88];
22const REGISTER: [u8; 4] = [0x44, 0x20, 0xe4, 0x86];
23const SIZE: [u8; 4] = [0x94, 0x9d, 0x22, 0x5d];
24
25const SLOAD_GAS: u64 = 800;
26const SSTORE_GAS: u64 = 20_000;
27const COPY_GAS: u64 = 3;
28
29pub fn create_arbaddresstable_precompile() -> DynPrecompile {
30    DynPrecompile::new_stateful(PrecompileId::custom("arbaddresstable"), handler)
31}
32
33fn handler(mut input: PrecompileInput<'_>) -> PrecompileResult {
34    let gas_limit = input.gas;
35    let data = input.data;
36    if data.len() < 4 {
37        return Err(PrecompileError::other("input too short"));
38    }
39
40    let selector: [u8; 4] = [data[0], data[1], data[2], data[3]];
41
42    let result = match selector {
43        SIZE => handle_size(&mut input),
44        ADDRESS_EXISTS => handle_address_exists(&mut input),
45        LOOKUP => handle_lookup(&mut input),
46        LOOKUP_INDEX => handle_lookup_index(&mut input),
47        REGISTER => handle_register(&mut input),
48        COMPRESS => handle_compress(&mut input),
49        DECOMPRESS => handle_decompress(&mut input),
50        _ => Err(PrecompileError::other("unknown ArbAddressTable selector")),
51    };
52    crate::gas_check(gas_limit, result)
53}
54
55// ── helpers ──────────────────────────────────────────────────────────
56
57fn load_arbos(input: &mut PrecompileInput<'_>) -> Result<(), PrecompileError> {
58    input
59        .internals_mut()
60        .load_account(ARBOS_STATE_ADDRESS)
61        .map_err(|e| PrecompileError::other(format!("load_account: {e:?}")))?;
62    Ok(())
63}
64
65fn sload_field(input: &mut PrecompileInput<'_>, slot: U256) -> Result<U256, PrecompileError> {
66    let val = input
67        .internals_mut()
68        .sload(ARBOS_STATE_ADDRESS, slot)
69        .map_err(|_| PrecompileError::other("sload failed"))?;
70    Ok(val.data)
71}
72
73fn sstore_field(
74    input: &mut PrecompileInput<'_>,
75    slot: U256,
76    value: U256,
77) -> Result<(), PrecompileError> {
78    input
79        .internals_mut()
80        .sstore(ARBOS_STATE_ADDRESS, slot, value)
81        .map_err(|_| PrecompileError::other("sstore failed"))?;
82    Ok(())
83}
84
85/// AddressTable numItems is stored at offset 0 in the table's subspace storage.
86fn handle_size(input: &mut PrecompileInput<'_>) -> PrecompileResult {
87    let gas_limit = input.gas;
88    load_arbos(input)?;
89
90    let table_key = derive_subspace_key(ROOT_STORAGE_KEY, ADDRESS_TABLE_SUBSPACE);
91    let size_slot = map_slot(table_key.as_slice(), 0);
92    let size = sload_field(input, size_slot)?;
93
94    Ok(PrecompileOutput::new(
95        (2 * SLOAD_GAS + COPY_GAS).min(gas_limit),
96        size.to_be_bytes::<32>().to_vec().into(),
97    ))
98}
99
100/// Check if an address exists in the table by looking up the byAddress sub-storage.
101fn handle_address_exists(input: &mut PrecompileInput<'_>) -> PrecompileResult {
102    let data = input.data;
103    if data.len() < 36 {
104        return Err(PrecompileError::other("input too short"));
105    }
106
107    let gas_limit = input.gas;
108    let addr = Address::from_slice(&data[16..36]);
109    load_arbos(input)?;
110
111    // byAddress = OpenSubStorage([]byte{}) — sub-storage with empty key.
112    let table_key = derive_subspace_key(ROOT_STORAGE_KEY, ADDRESS_TABLE_SUBSPACE);
113    let by_address_key = derive_subspace_key(table_key.as_slice(), &[]);
114
115    let addr_as_b256 = alloy_primitives::B256::left_padding_from(addr.as_slice());
116    let member_slot = map_slot_b256(by_address_key.as_slice(), &addr_as_b256);
117
118    let value = sload_field(input, member_slot)?;
119    let exists = if value != U256::ZERO {
120        U256::from(1u64)
121    } else {
122        U256::ZERO
123    };
124
125    // OAS(1) + byAddress.Get(1) + argsCost(3) + resultCost(3).
126    Ok(PrecompileOutput::new(
127        (2 * SLOAD_GAS + 2 * COPY_GAS).min(gas_limit),
128        exists.to_be_bytes::<32>().to_vec().into(),
129    ))
130}
131
132/// Lookup the index of an address in the table.
133fn handle_lookup(input: &mut PrecompileInput<'_>) -> PrecompileResult {
134    let data = input.data;
135    if data.len() < 36 {
136        return Err(PrecompileError::other("input too short"));
137    }
138
139    let gas_limit = input.gas;
140    let addr = Address::from_slice(&data[16..36]);
141    load_arbos(input)?;
142
143    let table_key = derive_subspace_key(ROOT_STORAGE_KEY, ADDRESS_TABLE_SUBSPACE);
144    let by_address_key = derive_subspace_key(table_key.as_slice(), &[]);
145
146    let addr_as_b256 = alloy_primitives::B256::left_padding_from(addr.as_slice());
147    let member_slot = map_slot_b256(by_address_key.as_slice(), &addr_as_b256);
148
149    let value = sload_field(input, member_slot)?;
150    if value == U256::ZERO {
151        return Err(PrecompileError::other(
152            "address does not exist in AddressTable",
153        ));
154    }
155
156    // Stored value is the 1-based index, so subtract 1.
157    let index = value.wrapping_sub(U256::from(1u64));
158    // OAS(1) + byAddress.Get(1) + argsCost(3) + resultCost(3).
159    Ok(PrecompileOutput::new(
160        (2 * SLOAD_GAS + 2 * COPY_GAS).min(gas_limit),
161        index.to_be_bytes::<32>().to_vec().into(),
162    ))
163}
164
165/// Lookup an address by index in the table.
166/// Reverse entries are stored at offset (index + 1) in the table's backing storage.
167fn handle_lookup_index(input: &mut PrecompileInput<'_>) -> PrecompileResult {
168    let data = input.data;
169    if data.len() < 36 {
170        return Err(PrecompileError::other("input too short"));
171    }
172
173    let gas_limit = input.gas;
174    let index: u64 = U256::from_be_slice(&data[4..36])
175        .try_into()
176        .map_err(|_| PrecompileError::other("index too large"))?;
177    load_arbos(input)?;
178
179    let table_key = derive_subspace_key(ROOT_STORAGE_KEY, ADDRESS_TABLE_SUBSPACE);
180    // Reverse lookup is at offset (index + 1) — 1-indexed.
181    let entry_slot = map_slot(table_key.as_slice(), index + 1);
182    let value = sload_field(input, entry_slot)?;
183
184    if value == U256::ZERO {
185        return Err(PrecompileError::other(
186            "index does not exist in AddressTable",
187        ));
188    }
189
190    // OAS(1) + numItems(1) + backing(1) + argsCost(3) + resultCost(3).
191    Ok(PrecompileOutput::new(
192        (3 * SLOAD_GAS + 2 * COPY_GAS).min(gas_limit),
193        value.to_be_bytes::<32>().to_vec().into(),
194    ))
195}
196
197/// Register an address in the table. If it already exists, returns its index.
198/// Otherwise, adds it and returns the new 0-based index.
199///
200/// Storage layout:
201/// - numItems at offset 0 in table subspace
202/// - byAddress: sub-storage with empty key, maps addr_hash → 1-based index
203/// - backing: maps (index + 1) → addr_hash (reverse lookup)
204fn handle_register(input: &mut PrecompileInput<'_>) -> PrecompileResult {
205    let data = input.data;
206    if data.len() < 36 {
207        return Err(PrecompileError::other("input too short"));
208    }
209
210    let gas_limit = input.gas;
211    let addr = Address::from_slice(&data[16..36]);
212    load_arbos(input)?;
213
214    let table_key = derive_subspace_key(ROOT_STORAGE_KEY, ADDRESS_TABLE_SUBSPACE);
215    let by_address_key = derive_subspace_key(table_key.as_slice(), &[]);
216    let addr_as_b256 = alloy_primitives::B256::left_padding_from(addr.as_slice());
217
218    // Check if address already exists in byAddress mapping.
219    let member_slot = map_slot_b256(by_address_key.as_slice(), &addr_as_b256);
220    let existing = sload_field(input, member_slot)?;
221
222    if existing != U256::ZERO {
223        // Already registered — return 0-based index.
224        let index = existing.wrapping_sub(U256::from(1u64));
225        // OAS(1) + byAddress.Get(1) + argsCost(3) + resultCost(3).
226        return Ok(PrecompileOutput::new(
227            (2 * SLOAD_GAS + 2 * COPY_GAS).min(gas_limit),
228            index.to_be_bytes::<32>().to_vec().into(),
229        ));
230    }
231
232    // Not yet registered — add it.
233    // Read numItems and increment it.
234    let num_items_slot = map_slot(table_key.as_slice(), 0);
235    let num_items = sload_field(input, num_items_slot)?;
236    let num_items_u64: u64 = num_items
237        .try_into()
238        .map_err(|_| PrecompileError::other("invalid numItems"))?;
239    let new_num_items = num_items_u64 + 1;
240    sstore_field(input, num_items_slot, U256::from(new_num_items))?;
241
242    // Store reverse mapping: backingStorage[newNumItems] = addr_hash.
243    let reverse_slot = map_slot(table_key.as_slice(), new_num_items);
244    sstore_field(input, reverse_slot, U256::from_be_bytes(addr_as_b256.0))?;
245
246    // Store byAddress mapping: byAddress[addr_hash] = newNumItems (1-based).
247    sstore_field(input, member_slot, U256::from(new_num_items))?;
248
249    // Return 0-based index.
250    let index = new_num_items - 1;
251
252    // OAS(1) + byAddress.Get(1) + numItems.Get(1) + 3 sstores + argsCost(3) + resultCost(3).
253    let gas_used = 3 * SLOAD_GAS + 3 * SSTORE_GAS + 2 * COPY_GAS;
254
255    Ok(PrecompileOutput::new(
256        gas_used.min(gas_limit),
257        U256::from(index).to_be_bytes::<32>().to_vec().into(),
258    ))
259}
260
261/// Compress: if address is in table, RLP-encode its index; else RLP-encode the 20-byte address.
262/// Returns ABI-encoded bytes.
263fn handle_compress(input: &mut PrecompileInput<'_>) -> PrecompileResult {
264    let data = input.data;
265    if data.len() < 36 {
266        return Err(PrecompileError::other("input too short"));
267    }
268
269    let gas_limit = input.gas;
270    let addr = Address::from_slice(&data[16..36]);
271    load_arbos(input)?;
272
273    let table_key = derive_subspace_key(ROOT_STORAGE_KEY, ADDRESS_TABLE_SUBSPACE);
274    let by_address_key = derive_subspace_key(table_key.as_slice(), &[]);
275    let addr_as_b256 = alloy_primitives::B256::left_padding_from(addr.as_slice());
276    let member_slot = map_slot_b256(by_address_key.as_slice(), &addr_as_b256);
277    let value = sload_field(input, member_slot)?;
278
279    let rlp_bytes = if value != U256::ZERO {
280        // Address exists — RLP-encode the 0-based index.
281        let index = value.wrapping_sub(U256::from(1u64)).to::<u64>();
282        rlp_encode_u64(index)
283    } else {
284        // Not in table — RLP-encode the raw 20-byte address.
285        rlp_encode_bytes(addr.as_slice())
286    };
287
288    // ABI-encode as `bytes`: offset (32) + length (32) + padded data.
289    let mut output = Vec::with_capacity(96);
290    output.extend_from_slice(&U256::from(32u64).to_be_bytes::<32>());
291    output.extend_from_slice(&U256::from(rlp_bytes.len() as u64).to_be_bytes::<32>());
292    output.extend_from_slice(&rlp_bytes);
293    // Pad to 32-byte boundary.
294    let pad = (32 - rlp_bytes.len() % 32) % 32;
295    output.extend(std::iter::repeat_n(0u8, pad));
296
297    // OAS(1) + byAddress.Get(1) + argsCost(3) + resultCost(3 words for bytes encoding).
298    let result_words = (output.len() as u64).div_ceil(32);
299    Ok(PrecompileOutput::new(
300        (2 * SLOAD_GAS + COPY_GAS + result_words * COPY_GAS).min(gas_limit),
301        output.into(),
302    ))
303}
304
305/// Decompress: RLP-decode from buf[offset..]. If 20 bytes, it's a raw address.
306/// Otherwise decode as u64 index and look up in the table.
307/// Returns ABI-encoded (address, uint256 bytesRead).
308fn handle_decompress(input: &mut PrecompileInput<'_>) -> PrecompileResult {
309    let data = input.data;
310    // ABI: decompress(bytes buf, uint256 offset)
311    // data[4..36] = offset to bytes data
312    // data[36..68] = offset value
313    if data.len() < 68 {
314        return Err(PrecompileError::other("input too short"));
315    }
316
317    let gas_limit = input.gas;
318
319    // Decode ABI bytes parameter.
320    let bytes_offset = U256::from_be_slice(&data[4..36]).to::<usize>();
321    let ioffset = U256::from_be_slice(&data[36..68]).to::<usize>();
322
323    // bytes data starts at 4 + bytes_offset (skip selector).
324    let bytes_start = 4 + bytes_offset;
325    if data.len() < bytes_start + 32 {
326        return Err(PrecompileError::other("invalid bytes offset"));
327    }
328    let bytes_len = U256::from_be_slice(&data[bytes_start..bytes_start + 32]).to::<usize>();
329    let bytes_data_start = bytes_start + 32;
330    if data.len() < bytes_data_start + bytes_len {
331        return Err(PrecompileError::other("bytes data truncated"));
332    }
333    let buf = &data[bytes_data_start..bytes_data_start + bytes_len];
334
335    if ioffset >= buf.len() {
336        return Err(PrecompileError::other("offset out of bounds"));
337    }
338    let slice = &buf[ioffset..];
339
340    load_arbos(input)?;
341
342    // Try to RLP-decode as byte string first.
343    let (decoded, bytes_read) =
344        rlp_decode_bytes(slice).map_err(|_| PrecompileError::other("RLP decode failed"))?;
345
346    let (addr, final_bytes_read) = if decoded.len() == 20 {
347        // Raw 20-byte address.
348        (Address::from_slice(&decoded), bytes_read)
349    } else {
350        // Re-decode as u64 index.
351        let (index, idx_bytes_read) =
352            rlp_decode_u64(slice).map_err(|_| PrecompileError::other("RLP decode index failed"))?;
353
354        let table_key = derive_subspace_key(ROOT_STORAGE_KEY, ADDRESS_TABLE_SUBSPACE);
355
356        // Bounds check: index < numItems.
357        let num_items_slot = map_slot(table_key.as_slice(), 0);
358        let num_items = sload_field(input, num_items_slot)?;
359        if U256::from(index) >= num_items {
360            return Err(PrecompileError::other(
361                "index does not exist in AddressTable",
362            ));
363        }
364
365        let entry_slot = map_slot(table_key.as_slice(), index + 1);
366        let value = sload_field(input, entry_slot)?;
367
368        // Extract 20-byte address from the 32-byte stored value.
369        let value_bytes = value.to_be_bytes::<32>();
370        let result_addr = Address::from_slice(&value_bytes[12..32]);
371        (result_addr, idx_bytes_read)
372    };
373
374    // ABI-encode (address, uint256).
375    let mut output = Vec::with_capacity(64);
376    output.extend_from_slice(&alloy_primitives::B256::left_padding_from(addr.as_slice()).0);
377    output.extend_from_slice(&U256::from(final_bytes_read as u64).to_be_bytes::<32>());
378
379    // Variable body sloads + argsCost(dynamic) + resultCost = 2 words × 3 = 6.
380    // Body: OAS(1) + 0 (raw addr) or OAS(1) + numItems(1) + backing(1) (index).
381    let body_sloads: u64 = if decoded.len() == 20 { 1 } else { 3 };
382    let arg_words = (input.data.len() as u64).saturating_sub(4).div_ceil(32);
383    Ok(PrecompileOutput::new(
384        (body_sloads * SLOAD_GAS + (arg_words + 2) * COPY_GAS).min(gas_limit),
385        output.into(),
386    ))
387}
388
389// ── Minimal RLP helpers ─────────────────────────────────────────────
390
391/// RLP-encode a u64 value.
392fn rlp_encode_u64(val: u64) -> Vec<u8> {
393    if val == 0 {
394        return vec![0x80];
395    }
396    if val < 128 {
397        return vec![val as u8];
398    }
399    let bytes = val.to_be_bytes();
400    let start = bytes.iter().position(|&b| b != 0).unwrap_or(7);
401    let len = 8 - start;
402    let mut out = Vec::with_capacity(1 + len);
403    out.push(0x80 + len as u8);
404    out.extend_from_slice(&bytes[start..]);
405    out
406}
407
408/// RLP-encode a byte slice as an RLP string.
409fn rlp_encode_bytes(data: &[u8]) -> Vec<u8> {
410    if data.len() == 1 && data[0] < 128 {
411        return vec![data[0]];
412    }
413    if data.len() < 56 {
414        let mut out = Vec::with_capacity(1 + data.len());
415        out.push(0x80 + data.len() as u8);
416        out.extend_from_slice(data);
417        return out;
418    }
419    let len_bytes = {
420        let l = data.len() as u64;
421        let bytes = l.to_be_bytes();
422        let start = bytes.iter().position(|&b| b != 0).unwrap_or(7);
423        bytes[start..].to_vec()
424    };
425    let mut out = Vec::with_capacity(1 + len_bytes.len() + data.len());
426    out.push(0xb7 + len_bytes.len() as u8);
427    out.extend_from_slice(&len_bytes);
428    out.extend_from_slice(data);
429    out
430}
431
432/// RLP-decode a byte string from a slice, returning (decoded_bytes, total_bytes_consumed).
433fn rlp_decode_bytes(data: &[u8]) -> Result<(Vec<u8>, usize), &'static str> {
434    if data.is_empty() {
435        return Err("empty input");
436    }
437    let prefix = data[0];
438    if prefix < 0x80 {
439        // Single byte.
440        Ok((vec![prefix], 1))
441    } else if prefix <= 0xb7 {
442        // Short string (0-55 bytes).
443        let len = (prefix - 0x80) as usize;
444        if data.len() < 1 + len {
445            return Err("truncated short string");
446        }
447        Ok((data[1..1 + len].to_vec(), 1 + len))
448    } else if prefix <= 0xbf {
449        // Long string.
450        let len_of_len = (prefix - 0xb7) as usize;
451        if data.len() < 1 + len_of_len {
452            return Err("truncated long string length");
453        }
454        let mut len_bytes = [0u8; 8];
455        len_bytes[8 - len_of_len..].copy_from_slice(&data[1..1 + len_of_len]);
456        let len = u64::from_be_bytes(len_bytes) as usize;
457        let total = 1 + len_of_len + len;
458        if data.len() < total {
459            return Err("truncated long string data");
460        }
461        Ok((data[1 + len_of_len..total].to_vec(), total))
462    } else {
463        Err("unexpected list prefix")
464    }
465}
466
467/// RLP-decode a u64 from a slice.
468fn rlp_decode_u64(data: &[u8]) -> Result<(u64, usize), &'static str> {
469    if data.is_empty() {
470        return Err("empty input");
471    }
472    let prefix = data[0];
473    if prefix == 0x80 {
474        // Zero.
475        Ok((0, 1))
476    } else if prefix < 0x80 {
477        // Single byte value.
478        Ok((prefix as u64, 1))
479    } else if prefix <= 0x88 {
480        // Short string encoding for integers (up to 8 bytes).
481        let len = (prefix - 0x80) as usize;
482        if len > 8 || data.len() < 1 + len {
483            return Err("invalid u64 encoding");
484        }
485        let mut bytes = [0u8; 8];
486        bytes[8 - len..].copy_from_slice(&data[1..1 + len]);
487        Ok((u64::from_be_bytes(bytes), 1 + len))
488    } else {
489        Err("value too large for u64")
490    }
491}