arb_precompiles/
arbsys.rs

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