arb_precompiles/
arbsys.rs

1use alloy_evm::precompiles::{DynPrecompile, PrecompileInput};
2use alloy_primitives::{keccak256, Address, Log, B256, U256};
3use revm::precompile::{PrecompileError, PrecompileId, PrecompileOutput, PrecompileResult};
4
5use std::{cell::RefCell, collections::HashMap, sync::Mutex};
6
7use crate::storage_slot::{
8    derive_subspace_key, map_slot, root_slot, ARBOS_STATE_ADDRESS, NATIVE_TOKEN_SUBSPACE,
9    ROOT_STORAGE_KEY, SEND_MERKLE_SUBSPACE,
10};
11
12/// ArbSys precompile address (0x64).
13pub const ARBSYS_ADDRESS: Address = Address::new([
14    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
15    0x00, 0x00, 0x00, 0x64,
16]);
17
18// Function selectors (keccak256 of canonical signature, first 4 bytes).
19const WITHDRAW_ETH: [u8; 4] = [0x25, 0xe1, 0x60, 0x63]; // withdrawEth(address)
20const SEND_TX_TO_L1: [u8; 4] = [0x92, 0x8c, 0x16, 0x9a]; // sendTxToL1(address,bytes)
21const ARB_BLOCK_NUMBER: [u8; 4] = [0xa3, 0xb1, 0xb3, 0x1d]; // arbBlockNumber()
22const ARB_BLOCK_HASH: [u8; 4] = [0x2b, 0x40, 0x7a, 0x82]; // arbBlockHash(uint256)
23const ARB_CHAIN_ID: [u8; 4] = [0xd1, 0x27, 0xf5, 0x4a]; // arbChainID()
24const ARB_OS_VERSION: [u8; 4] = [0x05, 0x10, 0x38, 0xf2]; // arbOSVersion()
25const GET_STORAGE_GAS_AVAILABLE: [u8; 4] = [0xa9, 0x45, 0x97, 0xff]; // getStorageGasAvailable()
26const IS_TOP_LEVEL_CALL: [u8; 4] = [0x08, 0xbd, 0x62, 0x4c]; // isTopLevelCall()
27const MAP_L1_SENDER: [u8; 4] = [0x4d, 0xbb, 0xd5, 0x06]; // mapL1SenderContractAddressToL2Alias(address,address)
28const WAS_ALIASED: [u8; 4] = [0x17, 0x5a, 0x26, 0x0b]; // wasMyCallersAddressAliased()
29const CALLER_WITHOUT_ALIAS: [u8; 4] = [0xd7, 0x45, 0x23, 0xb3]; // myCallersAddressWithoutAliasing()
30const SEND_MERKLE_TREE_STATE: [u8; 4] = [0x7a, 0xee, 0xcd, 0x2a]; // sendMerkleTreeState()
31
32// L1 alias offset: 0x1111000000000000000000000000000000001111
33const L1_ALIAS_OFFSET: Address = Address::new([
34    0x11, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
35    0x00, 0x00, 0x11, 0x11,
36]);
37
38// MerkleAccumulator: size at offset 0, partials at offset (2 + level).
39
40// Gas costs from the precompile framework (params package).
41const COPY_GAS: u64 = 3; // per 32-byte word
42const LOG_GAS: u64 = 375;
43const LOG_TOPIC_GAS: u64 = 375;
44const LOG_DATA_GAS: u64 = 8; // per byte
45
46// Storage gas costs from ArbOS storage accounting.
47const STORAGE_READ_COST: u64 = 800; // params.SloadGasEIP2200
48const STORAGE_WRITE_COST: u64 = 20_000; // params.SstoreSetGasEIP2200
49const STORAGE_WRITE_ZERO_COST: u64 = 5_000; // params.SstoreResetGasEIP2200
50
51fn storage_write_cost(value: U256) -> u64 {
52    if value.is_zero() {
53        STORAGE_WRITE_ZERO_COST
54    } else {
55        STORAGE_WRITE_COST
56    }
57}
58
59fn words_for_bytes(n: u64) -> u64 {
60    n.div_ceil(32)
61}
62
63/// Keccak gas from the storage burner: 30 + 6*words.
64fn keccak_gas(byte_count: u64) -> u64 {
65    30 + 6 * words_for_bytes(byte_count)
66}
67
68// Event topics.
69fn l2_to_l1_tx_topic() -> B256 {
70    keccak256(b"L2ToL1Tx(address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes)")
71}
72
73fn send_merkle_update_topic() -> B256 {
74    keccak256(b"SendMerkleUpdate(uint256,bytes32,uint256)")
75}
76
77/// State changes from an ArbSys call for post-execution application.
78#[derive(Debug, Clone, Default)]
79pub struct ArbSysMerkleState {
80    pub new_size: u64,
81    pub partials: Vec<(u64, B256)>,
82    pub send_hash: B256,
83    pub leaf_num: u64,
84    pub value_to_burn: U256,
85    pub block_number: u64,
86}
87
88thread_local! {
89    static ARBSYS_STATE: RefCell<Option<ArbSysMerkleState>> = const { RefCell::new(None) };
90    /// Set to `true` when the current transaction is an aliasing type
91    /// (unsigned, contract, or retryable L1→L2 message).
92    static TX_IS_ALIASED: RefCell<bool> = const { RefCell::new(false) };
93}
94
95static L1_BLOCK_CACHE: Mutex<Option<HashMap<u64, u64>>> = Mutex::new(None);
96static CURRENT_L2_BLOCK: Mutex<u64> = Mutex::new(0);
97
98/// Store ArbSys state changes for post-execution application.
99pub fn store_arbsys_state(state: ArbSysMerkleState) {
100    ARBSYS_STATE.with(|cell| *cell.borrow_mut() = Some(state));
101}
102
103/// Take the stored ArbSys state (clears it).
104pub fn take_arbsys_state() -> Option<ArbSysMerkleState> {
105    ARBSYS_STATE.with(|cell| cell.borrow_mut().take())
106}
107
108/// Mark the current transaction as an aliased L1→L2 type.
109pub fn set_tx_is_aliased(aliased: bool) {
110    TX_IS_ALIASED.with(|cell| *cell.borrow_mut() = aliased);
111}
112
113/// Check whether the current transaction uses address aliasing.
114pub fn get_tx_is_aliased() -> bool {
115    TX_IS_ALIASED.with(|cell| *cell.borrow())
116}
117
118/// Set the cached L1 block number for a given L2 block.
119pub fn set_cached_l1_block_number(l2_block: u64, l1_block: u64) {
120    let mut cache = L1_BLOCK_CACHE.lock().expect("L1 block cache lock poisoned");
121    let map = cache.get_or_insert_with(HashMap::new);
122    map.insert(l2_block, l1_block);
123    if l2_block > 100 {
124        map.retain(|&k, _| k >= l2_block - 100);
125    }
126}
127
128/// Get the cached L1 block number for a given L2 block.
129pub fn get_cached_l1_block_number(l2_block: u64) -> Option<u64> {
130    let cache = L1_BLOCK_CACHE.lock().expect("L1 block cache lock poisoned");
131    cache.as_ref().and_then(|m| m.get(&l2_block).copied())
132}
133
134/// Set the current L2 block number for precompile use.
135/// In Arbitrum, block_env.number holds the L1 block number (for the NUMBER opcode),
136/// so precompiles that need the L2 block number read it from here.
137pub fn set_current_l2_block(l2_block: u64) {
138    *CURRENT_L2_BLOCK.lock().expect("L2 block lock poisoned") = l2_block;
139}
140
141/// Get the current L2 block number.
142pub fn get_current_l2_block() -> u64 {
143    *CURRENT_L2_BLOCK.lock().expect("L2 block lock poisoned")
144}
145
146pub fn create_arbsys_precompile() -> DynPrecompile {
147    DynPrecompile::new_stateful(PrecompileId::custom("arbsys"), handler)
148}
149
150fn handler(mut input: PrecompileInput<'_>) -> PrecompileResult {
151    let gas_limit = input.gas;
152    let data = input.data;
153    if data.len() < 4 {
154        return Err(PrecompileError::other("input too short"));
155    }
156
157    let selector: [u8; 4] = [data[0], data[1], data[2], data[3]];
158
159    let result = match selector {
160        ARB_BLOCK_NUMBER => handle_arb_block_number(&mut input),
161        ARB_BLOCK_HASH => handle_arb_block_hash(&mut input),
162        ARB_CHAIN_ID => handle_arb_chain_id(&mut input),
163        ARB_OS_VERSION => handle_arbos_version(&mut input),
164        IS_TOP_LEVEL_CALL => handle_is_top_level_call(&mut input),
165        WAS_ALIASED => handle_was_aliased(&mut input),
166        CALLER_WITHOUT_ALIAS => handle_caller_without_alias(&mut input),
167        MAP_L1_SENDER => handle_map_l1_sender(&mut input),
168        GET_STORAGE_GAS_AVAILABLE => handle_get_storage_gas(&mut input),
169        WITHDRAW_ETH => handle_withdraw_eth(&mut input),
170        SEND_TX_TO_L1 => handle_send_tx_to_l1(&mut input),
171        SEND_MERKLE_TREE_STATE => handle_send_merkle_tree_state(&mut input),
172        _ => Err(PrecompileError::other("unknown ArbSys selector")),
173    };
174    crate::gas_check(gas_limit, result)
175}
176
177// ── view functions ───────────────────────────────────────────────────
178
179fn handle_arb_block_number(input: &mut PrecompileInput<'_>) -> PrecompileResult {
180    let block_num = U256::from(get_current_l2_block());
181    let args_cost = COPY_GAS * words_for_bytes(input.data.len().saturating_sub(4) as u64);
182    let result_cost = COPY_GAS * words_for_bytes(32);
183    Ok(PrecompileOutput::new(
184        STORAGE_READ_COST + args_cost + result_cost,
185        block_num.to_be_bytes::<32>().to_vec().into(),
186    ))
187}
188
189fn handle_arb_block_hash(input: &mut PrecompileInput<'_>) -> PrecompileResult {
190    let data = input.data;
191    if data.len() < 4 + 32 {
192        return Err(PrecompileError::other("input too short"));
193    }
194
195    let requested: u64 = U256::from_be_slice(&data[4..36])
196        .try_into()
197        .unwrap_or(u64::MAX);
198    let current = get_current_l2_block();
199
200    // Must be strictly less than current and within 256 blocks.
201    if requested >= current || requested + 256 < current {
202        return Err(PrecompileError::other("invalid block number"));
203    }
204
205    // Read from the L2 block hash cache (populated from the header chain).
206    // Do NOT use db.block_hash() which reads from the journal's block_hashes
207    // map — that map is pre-populated with L1 hashes (for the BLOCKHASH opcode)
208    // and would return wrong values for L2 block numbers.
209    let hash = crate::get_l2_block_hash(requested).unwrap_or(alloy_primitives::B256::ZERO);
210
211    let args_cost = COPY_GAS * words_for_bytes(input.data.len().saturating_sub(4) as u64);
212    let result_cost = COPY_GAS * words_for_bytes(32);
213    Ok(PrecompileOutput::new(
214        STORAGE_READ_COST + args_cost + result_cost,
215        hash.0.to_vec().into(),
216    ))
217}
218
219fn handle_arb_chain_id(input: &mut PrecompileInput<'_>) -> PrecompileResult {
220    let chain_id = input.internals().chain_id();
221    let args_cost = COPY_GAS * words_for_bytes(input.data.len().saturating_sub(4) as u64);
222    let result_cost = COPY_GAS * words_for_bytes(32);
223    Ok(PrecompileOutput::new(
224        STORAGE_READ_COST + args_cost + result_cost,
225        U256::from(chain_id).to_be_bytes::<32>().to_vec().into(),
226    ))
227}
228
229fn handle_arbos_version(input: &mut PrecompileInput<'_>) -> PrecompileResult {
230    let internals = input.internals_mut();
231
232    internals
233        .load_account(ARBOS_STATE_ADDRESS)
234        .map_err(|e| PrecompileError::other(format!("load_account: {e:?}")))?;
235
236    // ArbOS version is at root offset 0. Add 55 because the storage value is 0-based (v56 = stored
237    // 1).
238    let raw_version = internals
239        .sload(ARBOS_STATE_ADDRESS, root_slot(0))
240        .map_err(|_| PrecompileError::other("sload failed"))?;
241    let version = raw_version.data + U256::from(55);
242
243    let args_cost = COPY_GAS * words_for_bytes(input.data.len().saturating_sub(4) as u64);
244    let result_cost = COPY_GAS * words_for_bytes(32);
245    Ok(PrecompileOutput::new(
246        STORAGE_READ_COST + args_cost + result_cost,
247        version.to_be_bytes::<32>().to_vec().into(),
248    ))
249}
250
251fn handle_is_top_level_call(input: &mut PrecompileInput<'_>) -> PrecompileResult {
252    // Returns `depth <= 2`.
253    // Depth 1 = direct precompile call from tx, depth 2 = one intermediate contract.
254    let depth = crate::get_evm_depth();
255    let is_top = depth <= 2;
256    let val = if is_top { U256::from(1) } else { U256::ZERO };
257    let args_cost = COPY_GAS * words_for_bytes(input.data.len().saturating_sub(4) as u64);
258    let result_cost = COPY_GAS * words_for_bytes(32);
259    Ok(PrecompileOutput::new(
260        STORAGE_READ_COST + args_cost + result_cost,
261        val.to_be_bytes::<32>().to_vec().into(),
262    ))
263}
264
265fn handle_was_aliased(input: &mut PrecompileInput<'_>) -> PrecompileResult {
266    let tx_origin = input.internals().tx_origin();
267    let caller = input.caller;
268
269    // Read ArbOS version for version-gated behavior.
270    let internals = input.internals_mut();
271    internals
272        .load_account(ARBOS_STATE_ADDRESS)
273        .map_err(|e| PrecompileError::other(format!("load_account: {e:?}")))?;
274    let raw_version = internals
275        .sload(ARBOS_STATE_ADDRESS, root_slot(0))
276        .map_err(|_| PrecompileError::other("sload failed"))?
277        .data;
278    let arbos_version: u64 = raw_version.try_into().unwrap_or(0);
279
280    // topLevel = isTopLevel(depth < 2 || origin == Contracts[depth-2].Caller())
281    // ArbOS < 6: topLevel = depth == 2
282    // aliased = topLevel && DoesTxTypeAlias(TopTxType)
283    let depth = crate::get_evm_depth();
284    let is_top_level = if arbos_version < 6 {
285        depth == 2
286    } else {
287        depth <= 2 || tx_origin == caller
288    };
289
290    let aliased = is_top_level && get_tx_is_aliased();
291    let val = if aliased { U256::from(1) } else { U256::ZERO };
292    let args_cost = COPY_GAS * words_for_bytes(input.data.len().saturating_sub(4) as u64);
293    let result_cost = COPY_GAS * words_for_bytes(32);
294    Ok(PrecompileOutput::new(
295        STORAGE_READ_COST + args_cost + result_cost,
296        val.to_be_bytes::<32>().to_vec().into(),
297    ))
298}
299
300fn handle_caller_without_alias(input: &mut PrecompileInput<'_>) -> PrecompileResult {
301    // Returns Contracts[depth-2].Caller() (potentially unaliased).
302    // At depth 2 (common case): Contracts[0].Caller() == tx_origin.
303    // For deeper calls we'd need the call stack, which isn't available
304    // through PrecompileInput. tx_origin is correct at depth <= 2.
305    let tx_origin = input.internals().tx_origin();
306    let address = tx_origin;
307
308    let result_addr = if get_tx_is_aliased() {
309        undo_l1_alias(address)
310    } else {
311        address
312    };
313
314    let args_cost = COPY_GAS * words_for_bytes(input.data.len().saturating_sub(4) as u64);
315    let result_cost = COPY_GAS * words_for_bytes(32);
316    let mut out = [0u8; 32];
317    out[12..32].copy_from_slice(result_addr.as_slice());
318    Ok(PrecompileOutput::new(
319        STORAGE_READ_COST + args_cost + result_cost,
320        out.to_vec().into(),
321    ))
322}
323
324fn handle_map_l1_sender(input: &mut PrecompileInput<'_>) -> PrecompileResult {
325    let data = input.data;
326    if data.len() < 4 + 64 {
327        return Err(PrecompileError::other("input too short"));
328    }
329    // mapL1SenderContractAddressToL2Alias(address l1_addr, address _unused)
330    let l1_addr = Address::from_slice(&data[16..36]);
331    let aliased = apply_l1_alias(l1_addr);
332    let args_cost = COPY_GAS * words_for_bytes(input.data.len().saturating_sub(4) as u64);
333    let result_cost = COPY_GAS * words_for_bytes(32);
334    let mut out = [0u8; 32];
335    out[12..32].copy_from_slice(aliased.as_slice());
336    Ok(PrecompileOutput::new(
337        args_cost + result_cost,
338        out.to_vec().into(),
339    ))
340}
341
342fn handle_get_storage_gas(input: &mut PrecompileInput<'_>) -> PrecompileResult {
343    // Returns 0 — ArbOS has no concept of storage gas.
344    let args_cost = COPY_GAS * words_for_bytes(input.data.len().saturating_sub(4) as u64);
345    let result_cost = COPY_GAS * words_for_bytes(32);
346    Ok(PrecompileOutput::new(
347        STORAGE_READ_COST + args_cost + result_cost,
348        U256::ZERO.to_be_bytes::<32>().to_vec().into(),
349    ))
350}
351
352// ── L2→L1 messaging ─────────────────────────────────────────────────
353
354fn handle_withdraw_eth(input: &mut PrecompileInput<'_>) -> PrecompileResult {
355    if input.is_static {
356        return Err(PrecompileError::other(
357            "cannot call withdrawEth in static context",
358        ));
359    }
360
361    let data = input.data;
362    if data.len() < 4 + 32 {
363        return Err(PrecompileError::other("input too short"));
364    }
365
366    let destination = Address::from_slice(&data[16..36]);
367    // WithdrawEth calls SendTxToL1 with the destination and empty calldata.
368    do_send_tx_to_l1(input, destination, &[])
369}
370
371fn handle_send_tx_to_l1(input: &mut PrecompileInput<'_>) -> PrecompileResult {
372    if input.is_static {
373        return Err(PrecompileError::other(
374            "cannot call sendTxToL1 in static context",
375        ));
376    }
377
378    let data = input.data;
379    if data.len() < 4 + 64 {
380        return Err(PrecompileError::other("input too short"));
381    }
382
383    // sendTxToL1(address destination, bytes calldata)
384    let destination = Address::from_slice(&data[16..36]);
385
386    // Decode the dynamic bytes parameter.
387    let offset = U256::from_be_slice(&data[36..68])
388        .try_into()
389        .unwrap_or(0usize);
390    let abs_offset = 4 + offset;
391    if abs_offset + 32 > data.len() {
392        return Err(PrecompileError::other("calldata offset out of bounds"));
393    }
394    let length: usize = U256::from_be_slice(&data[abs_offset..abs_offset + 32])
395        .try_into()
396        .unwrap_or(0);
397    let calldata_start = abs_offset + 32;
398    let calldata_end = calldata_start + length;
399    if calldata_end > data.len() {
400        return Err(PrecompileError::other("calldata length out of bounds"));
401    }
402    let calldata = &data[calldata_start..calldata_end];
403
404    do_send_tx_to_l1(input, destination, calldata)
405}
406
407fn do_send_tx_to_l1(
408    input: &mut PrecompileInput<'_>,
409    destination: Address,
410    calldata: &[u8],
411) -> PrecompileResult {
412    let caller = input.caller;
413    let value = input.value;
414    // Use the L1 block number from ArbOS state (set by StartBlock), not from
415    // block_env.number (mix_hash). These can differ — the mix_hash value is the
416    // header's L1 block number, while ArbOS state holds the value updated during
417    // StartBlock which is what Nitro's evm.Context.BlockNumber returns.
418    let l1_block_number = U256::from(crate::get_l1_block_number_for_evm());
419    let l2_block_number = U256::from(get_current_l2_block());
420    let timestamp = input.internals().block_timestamp();
421
422    // Gas tracking: match the precompile framework burn pattern.
423    let mut gas_used = 0u64;
424    // Argument copy cost.
425    gas_used += COPY_GAS * words_for_bytes(input.data.len().saturating_sub(4) as u64);
426    // OpenArbosState overhead: makeContext reads version (800 gas) for all non-pure methods.
427    gas_used += STORAGE_READ_COST;
428
429    let internals = input.internals_mut();
430
431    // Load the ArbOS state account.
432    internals
433        .load_account(ARBOS_STATE_ADDRESS)
434        .map_err(|e| PrecompileError::other(format!("load_account: {e:?}")))?;
435
436    // ArbOS v41+: prevent sending value when native token owners exist.
437    if !value.is_zero() {
438        // Version read gas already covered by OpenArbosState overhead above.
439        let raw_version = internals
440            .sload(ARBOS_STATE_ADDRESS, root_slot(0))
441            .map_err(|_| PrecompileError::other("sload failed"))?
442            .data;
443        let arbos_version: u64 = raw_version.try_into().unwrap_or(0);
444        if arbos_version >= 41 {
445            let nt_key = derive_subspace_key(ROOT_STORAGE_KEY, NATIVE_TOKEN_SUBSPACE);
446            let nt_size_slot = map_slot(nt_key.as_slice(), 0);
447            gas_used += STORAGE_READ_COST;
448            let num_owners = internals
449                .sload(ARBOS_STATE_ADDRESS, nt_size_slot)
450                .map_err(|_| PrecompileError::other("sload failed"))?
451                .data;
452            if !num_owners.is_zero() {
453                return Err(PrecompileError::other(
454                    "not allowed to send value when native token owners exist",
455                ));
456            }
457        }
458    }
459
460    // Read current Merkle accumulator size.
461    let merkle_key = derive_subspace_key(ROOT_STORAGE_KEY, SEND_MERKLE_SUBSPACE);
462    let size_slot = map_slot(merkle_key.as_slice(), 0);
463    gas_used += STORAGE_READ_COST;
464    let current_size = internals
465        .sload(ARBOS_STATE_ADDRESS, size_slot)
466        .map_err(|_| PrecompileError::other("sload failed"))?
467        .data;
468    let old_size: u64 = current_size.try_into().unwrap_or(0);
469
470    // Compute the send hash (arbosState.KeccakHash charges gas via burner).
471    // Preimage: caller(20) + dest(20) + blockNum(32) + l1BlockNum(32) + time(32) + value(32) +
472    // calldata
473    let send_hash_input_len = 20 + 20 + 32 * 4 + calldata.len() as u64;
474    gas_used += keccak_gas(send_hash_input_len);
475    let send_hash = compute_send_hash(
476        caller,
477        destination,
478        l2_block_number,
479        l1_block_number,
480        timestamp,
481        value,
482        calldata,
483    );
484
485    // Update Merkle accumulator: insert leaf and collect intermediate node events.
486    let (new_size, merkle_events, partials) =
487        update_merkle_accumulator(internals, &merkle_key, send_hash, old_size, &mut gas_used)?;
488
489    // merkleAcc.Size() after Append does another storage read.
490    gas_used += STORAGE_READ_COST;
491
492    // Write new size.
493    let new_size_val = U256::from(new_size);
494    gas_used += storage_write_cost(new_size_val);
495    internals
496        .sstore(ARBOS_STATE_ADDRESS, size_slot, new_size_val)
497        .map_err(|_| PrecompileError::other("sstore failed"))?;
498
499    // Emit SendMerkleUpdate events (one per intermediate node, all topics, empty data).
500    let update_topic = send_merkle_update_topic();
501    for evt in &merkle_events {
502        // position = (level << 192) + numLeaves
503        let position: U256 = (U256::from(evt.level) << 192) | U256::from(evt.num_leaves);
504        internals.log(Log::new_unchecked(
505            ARBSYS_ADDRESS,
506            vec![
507                update_topic,
508                B256::from(U256::ZERO.to_be_bytes::<32>()), // reserved = 0
509                B256::from(evt.hash.to_be_bytes::<32>()),   // hash
510                B256::from(position.to_be_bytes::<32>()),   // position
511            ],
512            Default::default(), // empty data (all fields indexed)
513        ));
514        // Gas: 4 topics (event_id + 3 indexed), 0 data bytes.
515        gas_used += LOG_GAS + LOG_TOPIC_GAS * 4;
516    }
517
518    let leaf_num = new_size - 1;
519
520    // Emit L2ToL1Tx event.
521    // Topics: [event_id, destination (indexed), hash (indexed), position (indexed)]
522    // Data: ABI-encoded [caller, arbBlockNum, ethBlockNum, timestamp, callvalue, bytes]
523    let l2l1_topic = l2_to_l1_tx_topic();
524    let dest_topic = B256::left_padding_from(destination.as_slice());
525    let hash_topic = B256::from(U256::from_be_bytes(send_hash.0).to_be_bytes::<32>());
526    let position_topic = B256::from(U256::from(leaf_num).to_be_bytes::<32>());
527
528    let mut event_data = Vec::with_capacity(256);
529    // address caller (left-padded to 32 bytes)
530    let mut caller_padded = [0u8; 32];
531    caller_padded[12..32].copy_from_slice(caller.as_slice());
532    event_data.extend_from_slice(&caller_padded);
533    // uint256 arbBlockNum (L2 block number)
534    event_data.extend_from_slice(&l2_block_number.to_be_bytes::<32>());
535    // uint256 ethBlockNum (L1 block number)
536    event_data.extend_from_slice(&l1_block_number.to_be_bytes::<32>());
537    // uint256 timestamp
538    event_data.extend_from_slice(&timestamp.to_be_bytes::<32>());
539    // uint256 callvalue
540    event_data.extend_from_slice(&value.to_be_bytes::<32>());
541    // bytes data (ABI dynamic type: offset, then length, then data, then padding)
542    event_data.extend_from_slice(&U256::from(6 * 32).to_be_bytes::<32>()); // offset = 6 words
543    event_data.extend_from_slice(&U256::from(calldata.len()).to_be_bytes::<32>());
544    event_data.extend_from_slice(calldata);
545    // Pad to 32-byte boundary.
546    let pad = (32 - calldata.len() % 32) % 32;
547    event_data.extend(std::iter::repeat_n(0u8, pad));
548
549    let l2l1_data_len = event_data.len() as u64;
550    internals.log(Log::new_unchecked(
551        ARBSYS_ADDRESS,
552        vec![l2l1_topic, dest_topic, hash_topic, position_topic],
553        event_data.into(),
554    ));
555    // Gas: 4 topics (event_id + 3 indexed), data = ABI-encoded non-indexed fields.
556    gas_used += LOG_GAS + LOG_TOPIC_GAS * 4 + LOG_DATA_GAS * l2l1_data_len;
557
558    // Store state for post-execution (value burn, etc.)
559    store_arbsys_state(ArbSysMerkleState {
560        new_size,
561        partials: partials
562            .iter()
563            .enumerate()
564            .map(|(i, h)| (i as u64, *h))
565            .collect(),
566        send_hash,
567        leaf_num,
568        value_to_burn: value,
569        block_number: l2_block_number.try_into().unwrap_or(0),
570    });
571
572    // Read ArbOS version for return value versioning (no gas — uses cached value).
573    let raw_version = internals
574        .sload(ARBOS_STATE_ADDRESS, root_slot(0))
575        .map_err(|_| PrecompileError::other("sload failed"))?
576        .data;
577    let arbos_version: u64 = raw_version.try_into().unwrap_or(0);
578
579    // ArbOS >= 4: return leafNum; older versions return sendHash.
580    let return_val = if arbos_version >= 4 {
581        U256::from(leaf_num)
582    } else {
583        U256::from_be_bytes(send_hash.0)
584    };
585
586    // Result copy cost.
587    let output = return_val.to_be_bytes::<32>().to_vec();
588    gas_used += COPY_GAS * words_for_bytes(output.len() as u64);
589
590    Ok(PrecompileOutput::new(gas_used, output.into()))
591}
592
593fn handle_send_merkle_tree_state(input: &mut PrecompileInput<'_>) -> PrecompileResult {
594    // Only callable by address zero (for state export).
595    if input.caller != Address::ZERO {
596        return Err(PrecompileError::other(
597            "method can only be called by address zero",
598        ));
599    }
600    let mut gas_used = 0u64;
601    let internals = input.internals_mut();
602
603    internals
604        .load_account(ARBOS_STATE_ADDRESS)
605        .map_err(|e| PrecompileError::other(format!("load_account: {e:?}")))?;
606
607    let merkle_key = derive_subspace_key(ROOT_STORAGE_KEY, SEND_MERKLE_SUBSPACE);
608    let size_slot = map_slot(merkle_key.as_slice(), 0);
609    gas_used += STORAGE_READ_COST;
610    let size = internals
611        .sload(ARBOS_STATE_ADDRESS, size_slot)
612        .map_err(|_| PrecompileError::other("sload failed"))?
613        .data;
614
615    let size_u64: u64 = size.try_into().unwrap_or(0);
616
617    // Read partials — stored at offset (2 + level) in the accumulator storage.
618    let num_partials = calc_num_partials(size_u64);
619    let mut partials = Vec::new();
620    for i in 0..num_partials {
621        let slot = map_slot(merkle_key.as_slice(), 2 + i);
622        gas_used += STORAGE_READ_COST;
623        let val = internals
624            .sload(ARBOS_STATE_ADDRESS, slot)
625            .map_err(|_| PrecompileError::other("sload failed"))?
626            .data;
627        partials.push(val);
628    }
629
630    let b256_partials: Vec<B256> = partials
631        .iter()
632        .map(|p| B256::from(p.to_be_bytes::<32>()))
633        .collect();
634    let root = compute_merkle_root(&b256_partials, size_u64);
635
636    // Return (size, root, partials...)
637    // ABI: uint256 size, bytes32 root, bytes32[] partials
638    let num_partials = partials.len();
639    let mut out = Vec::with_capacity(96 + num_partials * 32);
640    out.extend_from_slice(&size.to_be_bytes::<32>());
641    out.extend_from_slice(&root.0);
642    // Dynamic array: offset, length, elements
643    out.extend_from_slice(&U256::from(96u64).to_be_bytes::<32>());
644    out.extend_from_slice(&U256::from(num_partials).to_be_bytes::<32>());
645    for p in &partials {
646        out.extend_from_slice(&p.to_be_bytes::<32>());
647    }
648
649    let args_cost = COPY_GAS * words_for_bytes(input.data.len().saturating_sub(4) as u64);
650    let result_cost = COPY_GAS * words_for_bytes(out.len() as u64);
651    Ok(PrecompileOutput::new(
652        gas_used + args_cost + result_cost,
653        out.into(),
654    ))
655}
656
657// ── Merkle helpers ───────────────────────────────────────────────────
658
659fn compute_send_hash(
660    sender: Address,
661    dest: Address,
662    arb_block_num: U256,
663    eth_block_num: U256,
664    timestamp: U256,
665    value: U256,
666    data: &[u8],
667) -> B256 {
668    // Uses raw 20-byte addresses (no left-padding to 32 bytes).
669    let mut preimage = Vec::with_capacity(200 + data.len());
670    preimage.extend_from_slice(sender.as_slice()); // 20 bytes
671    preimage.extend_from_slice(dest.as_slice()); // 20 bytes
672    preimage.extend_from_slice(&arb_block_num.to_be_bytes::<32>());
673    preimage.extend_from_slice(&eth_block_num.to_be_bytes::<32>());
674    preimage.extend_from_slice(&timestamp.to_be_bytes::<32>());
675    preimage.extend_from_slice(&value.to_be_bytes::<32>());
676    preimage.extend_from_slice(data);
677    keccak256(&preimage)
678}
679
680/// Intermediate node event from merkle accumulator append.
681struct MerkleTreeNodeEvent {
682    level: u64,
683    num_leaves: u64,
684    hash: U256,
685}
686
687/// Append a leaf to the merkle accumulator (MerkleAccumulator.Append).
688///
689/// Returns (new_size, events, partials_for_root_computation).
690fn update_merkle_accumulator(
691    internals: &mut alloy_evm::EvmInternals<'_>,
692    merkle_key: &B256,
693    item_hash: B256,
694    old_size: u64,
695    gas_used: &mut u64,
696) -> Result<(u64, Vec<MerkleTreeNodeEvent>, Vec<B256>), PrecompileError> {
697    let new_size = old_size + 1;
698    let mut events = Vec::new();
699
700    // Hash the leaf before insertion: soFar = keccak256(itemHash).
701    let mut so_far = keccak256(item_hash.as_slice()).to_vec();
702
703    let num_partials_old = calc_num_partials(old_size);
704    let mut level = 0u64;
705
706    loop {
707        if level == num_partials_old {
708            // Store at new top level.
709            let h = U256::from_be_slice(&so_far);
710            let slot = map_slot(merkle_key.as_slice(), 2 + level);
711            *gas_used += storage_write_cost(h);
712            internals
713                .sstore(ARBOS_STATE_ADDRESS, slot, h)
714                .map_err(|_| PrecompileError::other("sstore failed"))?;
715            break;
716        }
717
718        // Read partial at this level.
719        let slot = map_slot(merkle_key.as_slice(), 2 + level);
720        *gas_used += STORAGE_READ_COST;
721        let this_level = internals
722            .sload(ARBOS_STATE_ADDRESS, slot)
723            .map_err(|_| PrecompileError::other("sload failed"))?
724            .data;
725
726        if this_level.is_zero() {
727            // Empty slot: store and stop.
728            let h = U256::from_be_slice(&so_far);
729            *gas_used += storage_write_cost(h);
730            internals
731                .sstore(ARBOS_STATE_ADDRESS, slot, h)
732                .map_err(|_| PrecompileError::other("sstore failed"))?;
733            break;
734        }
735
736        // Combine: soFar = keccak256(thisLevel || soFar)
737        // Keccak charged via the burner: 30 + 6*2 = 42 for 64 bytes.
738        *gas_used += keccak_gas(64);
739        let mut preimage = [0u8; 64];
740        preimage[..32].copy_from_slice(&this_level.to_be_bytes::<32>());
741        preimage[32..].copy_from_slice(&so_far);
742        so_far = keccak256(preimage).to_vec();
743
744        // Clear the partial at this level.
745        *gas_used += STORAGE_WRITE_ZERO_COST;
746        internals
747            .sstore(ARBOS_STATE_ADDRESS, slot, U256::ZERO)
748            .map_err(|_| PrecompileError::other("sstore failed"))?;
749
750        level += 1;
751
752        // Record event for this intermediate node.
753        events.push(MerkleTreeNodeEvent {
754            level,
755            num_leaves: new_size - 1,
756            hash: U256::from_be_slice(&so_far),
757        });
758    }
759
760    // Read all partials for root computation.
761    // No gas charge here: Append doesn't read partials for root.
762    // The root is computed later in the block builder.
763    let num_partials = calc_num_partials(new_size);
764    let mut partials = Vec::with_capacity(num_partials as usize);
765    for i in 0..num_partials {
766        let pslot = map_slot(merkle_key.as_slice(), 2 + i);
767        let val = internals
768            .sload(ARBOS_STATE_ADDRESS, pslot)
769            .map_err(|_| PrecompileError::other("sload failed"))?
770            .data;
771        partials.push(B256::from(val.to_be_bytes::<32>()));
772    }
773
774    Ok((new_size, events, partials))
775}
776
777/// Calculate number of partials for a given size (Log2ceil).
778fn calc_num_partials(size: u64) -> u64 {
779    if size == 0 {
780        return 0;
781    }
782    64 - size.leading_zeros() as u64
783}
784
785/// Compute the merkle root from partials (MerkleAccumulator.Root()).
786///
787/// Pads with zero hashes when capacity gaps exist between populated partial levels.
788fn compute_merkle_root(partials: &[B256], size: u64) -> B256 {
789    if partials.is_empty() || size == 0 {
790        return B256::ZERO;
791    }
792
793    let num_partials = calc_num_partials(size);
794    let mut hash_so_far: Option<B256> = None;
795    let mut capacity_in_hash: u64 = 0;
796    let mut capacity: u64 = 1;
797
798    for level in 0..num_partials {
799        let partial = if (level as usize) < partials.len() {
800            partials[level as usize]
801        } else {
802            B256::ZERO
803        };
804
805        if partial != B256::ZERO {
806            match hash_so_far {
807                None => {
808                    hash_so_far = Some(partial);
809                    capacity_in_hash = capacity;
810                }
811                Some(ref h) => {
812                    // Pad with zero hashes until capacity matches.
813                    let mut current = *h;
814                    let mut cap = capacity_in_hash;
815                    while cap < capacity {
816                        let mut preimage = [0u8; 64];
817                        preimage[..32].copy_from_slice(current.as_slice());
818                        // second 32 bytes remain zero
819                        current = keccak256(preimage);
820                        cap *= 2;
821                    }
822                    // Combine: keccak256(partial || current)
823                    let mut preimage = [0u8; 64];
824                    preimage[..32].copy_from_slice(partial.as_slice());
825                    preimage[32..].copy_from_slice(current.as_slice());
826                    let combined = keccak256(preimage);
827                    hash_so_far = Some(combined);
828                    capacity_in_hash = 2 * capacity;
829                }
830            }
831        }
832        capacity *= 2;
833    }
834
835    hash_so_far.unwrap_or(B256::ZERO)
836}
837
838// ── L1 alias helpers ─────────────────────────────────────────────────
839
840fn apply_l1_alias(addr: Address) -> Address {
841    let mut bytes = [0u8; 20];
842    for (i, byte) in bytes.iter_mut().enumerate() {
843        *byte = addr.0[i].wrapping_add(L1_ALIAS_OFFSET.0[i]);
844    }
845    Address::new(bytes)
846}
847
848fn undo_l1_alias(addr: Address) -> Address {
849    let mut bytes = [0u8; 20];
850    for (i, byte) in bytes.iter_mut().enumerate() {
851        *byte = addr.0[i].wrapping_sub(L1_ALIAS_OFFSET.0[i]);
852    }
853    Address::new(bytes)
854}