arb_precompiles/
arbsys.rs

1use std::sync::Arc;
2
3use alloy_evm::precompiles::{DynPrecompile, PrecompileInput};
4use alloy_primitives::{Address, B256, Log, U256, keccak256};
5use alloy_sol_types::{SolError, SolEvent, SolInterface};
6use arb_context::ArbPrecompileCtx;
7use arb_storage::ARBOS_STATE_ADDRESS;
8use arbos::merkle_accumulator::calc_num_partials;
9use revm::precompile::{PrecompileId, PrecompileOutput, PrecompileResult};
10
11use crate::{ArbPrecompileError, interfaces::IArbSys};
12
13/// ArbSys precompile address (0x64).
14pub const ARBSYS_ADDRESS: Address = Address::new([
15    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
16    0x00, 0x00, 0x00, 0x64,
17]);
18
19// L1 alias offset: 0x1111000000000000000000000000000000001111
20const L1_ALIAS_OFFSET: Address = Address::new([
21    0x11, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
22    0x00, 0x00, 0x11, 0x11,
23]);
24
25// MerkleAccumulator: size at offset 0, partials at offset (2 + level).
26
27// Gas costs from the precompile framework (params package).
28const COPY_GAS: u64 = 3; // per 32-byte word
29const LOG_GAS: u64 = 375;
30const LOG_TOPIC_GAS: u64 = 375;
31const LOG_DATA_GAS: u64 = 8; // per byte
32
33// Storage gas costs from ArbOS storage accounting.
34const STORAGE_READ_COST: u64 = 800; // params.SloadGasEIP2200
35const STORAGE_WRITE_COST: u64 = 20_000; // params.SstoreSetGasEIP2200
36const STORAGE_WRITE_ZERO_COST: u64 = 5_000; // params.SstoreResetGasEIP2200
37
38fn words_for_bytes(n: u64) -> u64 {
39    n.div_ceil(32)
40}
41
42/// Keccak gas from the storage burner: 30 + 6*words.
43fn keccak_gas(byte_count: u64) -> u64 {
44    30 + 6 * words_for_bytes(byte_count)
45}
46
47pub fn l2_to_l1_tx_topic() -> B256 {
48    IArbSys::L2ToL1Tx::SIGNATURE_HASH
49}
50
51pub fn send_merkle_update_topic() -> B256 {
52    IArbSys::SendMerkleUpdate::SIGNATURE_HASH
53}
54
55pub fn create_arbsys_precompile(ctx: Arc<ArbPrecompileCtx>) -> DynPrecompile {
56    DynPrecompile::new_stateful(PrecompileId::custom("arbsys"), move |input| {
57        handler(input, &ctx)
58    })
59}
60
61fn handler(mut input: PrecompileInput<'_>, ctx: &ArbPrecompileCtx) -> PrecompileResult {
62    let mut gas_used = 0u64;
63    let gas_limit = input.gas;
64    let data = input.data;
65
66    let call = match IArbSys::ArbSysCalls::abi_decode(data) {
67        Ok(c) => c,
68        Err(_) => return crate::burn_all_revert(gas_limit),
69    };
70
71    // withdrawEth and sendTxToL1 are payable; every other method rejects value.
72    if let Some(r) = crate::reject_nonpayable_value(
73        input.value,
74        data,
75        gas_limit,
76        &[[0x25, 0xe1, 0x60, 0x63], [0x92, 0x8c, 0x16, 0x9a]],
77    ) {
78        return r;
79    }
80    if let Some(r) = crate::reject_static_write(
81        input.is_static,
82        input.data,
83        gas_limit,
84        &[[0x25, 0xe1, 0x60, 0x63], [0x92, 0x8c, 0x16, 0x9a]],
85    ) {
86        return r;
87    }
88    if let Some(r) = crate::reject_delegate_nonpure(
89        input.target_address != input.bytecode_address,
90        input.data,
91        gas_limit,
92        &[[0x4d, 0xbb, 0xd5, 0x06]],
93    ) {
94        return r;
95    }
96
97    // `mapL1SenderContractAddressToL2Alias` is `pure` (no state access), so
98    // the framework skips the `OpenArbosState` SLOAD; every other method is at
99    // least `view` and pays for it.
100    let is_pure = matches!(
101        call,
102        IArbSys::ArbSysCalls::mapL1SenderContractAddressToL2Alias(_)
103    );
104    if is_pure {
105        crate::init_precompile_gas_pure(&mut gas_used, ctx, data.len());
106    } else {
107        crate::init_precompile_gas(&mut gas_used, ctx, data.len());
108    }
109
110    use IArbSys::ArbSysCalls;
111    let result = match call {
112        ArbSysCalls::arbBlockNumber(_) => handle_arb_block_number(&mut input, &mut gas_used, ctx),
113        ArbSysCalls::arbBlockHash(c) => {
114            handle_arb_block_hash(&mut input, &mut gas_used, ctx, c.arbBlockNum)
115        }
116        ArbSysCalls::arbChainID(_) => handle_arb_chain_id(&mut input, &mut gas_used, ctx),
117        ArbSysCalls::arbOSVersion(_) => handle_arbos_version(&mut input, &mut gas_used, ctx),
118        ArbSysCalls::getStorageGasAvailable(_) => {
119            handle_get_storage_gas(&mut input, &mut gas_used, ctx)
120        }
121        ArbSysCalls::isTopLevelCall(_) => handle_is_top_level_call(&mut input, &mut gas_used, ctx),
122        ArbSysCalls::mapL1SenderContractAddressToL2Alias(c) => {
123            handle_map_l1_sender(&mut input, &mut gas_used, ctx, c.sender)
124        }
125        ArbSysCalls::wasMyCallersAddressAliased(_) => {
126            handle_was_aliased(&mut input, &mut gas_used, ctx)
127        }
128        ArbSysCalls::myCallersAddressWithoutAliasing(_) => {
129            handle_caller_without_alias(&mut input, &mut gas_used, ctx)
130        }
131        ArbSysCalls::withdrawEth(c) => {
132            handle_withdraw_eth(&mut input, &mut gas_used, ctx, c.destination)
133        }
134        ArbSysCalls::sendTxToL1(c) => handle_send_tx_to_l1(
135            &mut input,
136            &mut gas_used,
137            ctx,
138            c.destination,
139            c.data.as_ref(),
140        ),
141        ArbSysCalls::sendMerkleTreeState(_) => {
142            handle_send_merkle_tree_state(&mut input, &mut gas_used, ctx)
143        }
144    };
145    crate::gas_check(ctx, gas_limit, gas_used, result)
146}
147
148// ── view functions ───────────────────────────────────────────────────
149
150fn handle_arb_block_number(
151    input: &mut PrecompileInput<'_>,
152    gas_used: &mut u64,
153    ctx: &ArbPrecompileCtx,
154) -> PrecompileResult {
155    let block_num = U256::from(ctx.block.l2_block_number);
156    let gas_limit = input.gas;
157    crate::charge_computation(gas_used, ctx, COPY_GAS * words_for_bytes(32));
158    Ok(PrecompileOutput::new(
159        (*gas_used).min(gas_limit),
160        block_num.to_be_bytes::<32>().to_vec().into(),
161    ))
162}
163
164#[derive(Debug, thiserror::Error)]
165#[error("arbBlockHash: L2 block {requested} hash unavailable (current {current})")]
166struct MissingL2BlockHash {
167    requested: u64,
168    current: u64,
169}
170
171fn handle_arb_block_hash(
172    input: &mut PrecompileInput<'_>,
173    gas_used: &mut u64,
174    ctx: &ArbPrecompileCtx,
175    requested_u256: U256,
176) -> PrecompileResult {
177    let requested: u64 = requested_u256.try_into().unwrap_or(u64::MAX);
178    let current = ctx.block.l2_block_number;
179    let gas_limit = input.gas;
180
181    if requested >= current || requested + 256 < current {
182        let arbos_version = ctx.block.arbos_version;
183        if arbos_version >= 11 {
184            let revert_data = IArbSys::InvalidBlockNumber {
185                requested: requested_u256,
186                current: U256::from(current),
187            }
188            .abi_encode();
189            crate::charge_computation(
190                gas_used,
191                ctx,
192                COPY_GAS * words_for_bytes(revert_data.len() as u64),
193            );
194            return Ok(PrecompileOutput::new_reverted(
195                (*gas_used).min(gas_limit),
196                revert_data.into(),
197            ));
198        }
199        return Err(ArbPrecompileError::empty_revert(*gas_used).into());
200    }
201
202    // The window is populated before execution, so an in-range miss is an
203    // internal inconsistency — fail loudly instead of returning a zero hash.
204    let hash = match ctx.block.cached_l2_block_hash(requested) {
205        Some(hash) => hash,
206        None => {
207            return Err(
208                ArbPrecompileError::fatal(MissingL2BlockHash { requested, current }).into(),
209            );
210        }
211    };
212
213    crate::charge_computation(gas_used, ctx, COPY_GAS * words_for_bytes(32));
214    Ok(PrecompileOutput::new(
215        (*gas_used).min(gas_limit),
216        hash.0.to_vec().into(),
217    ))
218}
219
220fn handle_arb_chain_id(
221    input: &mut PrecompileInput<'_>,
222    gas_used: &mut u64,
223    ctx: &ArbPrecompileCtx,
224) -> PrecompileResult {
225    let chain_id = input.internals().chain_id();
226    let gas_limit = input.gas;
227    crate::charge_computation(gas_used, ctx, COPY_GAS * words_for_bytes(32));
228    Ok(PrecompileOutput::new(
229        (*gas_used).min(gas_limit),
230        U256::from(chain_id).to_be_bytes::<32>().to_vec().into(),
231    ))
232}
233
234/// User-visible ArbOS version: stored format version + 55.
235fn arbos_version_from_format(format_version: U256) -> U256 {
236    format_version + U256::from(55)
237}
238
239fn handle_arbos_version(
240    input: &mut PrecompileInput<'_>,
241    gas_used: &mut u64,
242    ctx: &ArbPrecompileCtx,
243) -> PrecompileResult {
244    let gas_limit = input.gas;
245    let internals = input.internals_mut();
246
247    internals
248        .load_account(ARBOS_STATE_ADDRESS)
249        .map_err(ArbPrecompileError::fatal)?;
250
251    let arb_state = ctx
252        .block
253        .arbos_state(internals)
254        .map_err(ArbPrecompileError::fatal)?;
255    let version = arbos_version_from_format(U256::from(arb_state.arbos_version()));
256
257    crate::charge_computation(gas_used, ctx, COPY_GAS * words_for_bytes(32));
258    Ok(PrecompileOutput::new(
259        (*gas_used).min(gas_limit),
260        version.to_be_bytes::<32>().to_vec().into(),
261    ))
262}
263
264fn handle_is_top_level_call(
265    input: &mut PrecompileInput<'_>,
266    gas_used: &mut u64,
267    ctx: &ArbPrecompileCtx,
268) -> PrecompileResult {
269    let depth = ctx.evm_depth();
270    let is_top = depth <= 2;
271    let val = if is_top { U256::from(1) } else { U256::ZERO };
272    let gas_limit = input.gas;
273    crate::charge_computation(gas_used, ctx, COPY_GAS * words_for_bytes(32));
274    Ok(PrecompileOutput::new(
275        (*gas_used).min(gas_limit),
276        val.to_be_bytes::<32>().to_vec().into(),
277    ))
278}
279
280fn handle_was_aliased(
281    input: &mut PrecompileInput<'_>,
282    gas_used: &mut u64,
283    ctx: &ArbPrecompileCtx,
284) -> PrecompileResult {
285    let gas_limit = input.gas;
286    let internals = input.internals_mut();
287    internals
288        .load_account(ARBOS_STATE_ADDRESS)
289        .map_err(ArbPrecompileError::fatal)?;
290    let arb_state = ctx
291        .block
292        .arbos_state(internals)
293        .map_err(ArbPrecompileError::fatal)?;
294    let arbos_version = arb_state.arbos_version();
295
296    let tx_origin = input.internals().tx_origin();
297    let depth = ctx.evm_depth();
298    let is_top_level = if arbos_version < 6 {
299        depth == 2
300    } else if depth <= 2 {
301        true
302    } else {
303        ctx.caller_at_depth(depth - 1)
304            .map(|c| tx_origin == c)
305            .unwrap_or(false)
306    };
307
308    let aliased = is_top_level && ctx.tx_is_aliased();
309    let val = if aliased { U256::from(1) } else { U256::ZERO };
310    crate::charge_computation(gas_used, ctx, COPY_GAS * words_for_bytes(32));
311    Ok(PrecompileOutput::new(
312        (*gas_used).min(gas_limit),
313        val.to_be_bytes::<32>().to_vec().into(),
314    ))
315}
316
317fn handle_caller_without_alias(
318    input: &mut PrecompileInput<'_>,
319    gas_used: &mut u64,
320    ctx: &ArbPrecompileCtx,
321) -> PrecompileResult {
322    let gas_limit = input.gas;
323    let depth = ctx.evm_depth();
324    let address = if depth > 1 {
325        ctx.caller_at_depth(depth - 1).unwrap_or(Address::ZERO)
326    } else {
327        Address::ZERO
328    };
329
330    let arbos_version = ctx.block.arbos_version;
331    let is_top_level = if arbos_version < 6 {
332        depth == 2
333    } else if depth <= 2 {
334        true
335    } else {
336        let tx_origin = input.internals().tx_origin();
337        ctx.caller_at_depth(depth - 1)
338            .map(|c| tx_origin == c)
339            .unwrap_or(false)
340    };
341    let aliased = is_top_level && ctx.tx_is_aliased();
342    let result_addr = if aliased {
343        undo_l1_alias(address)
344    } else {
345        address
346    };
347
348    let mut out = [0u8; 32];
349    out[12..32].copy_from_slice(result_addr.as_slice());
350    crate::charge_computation(gas_used, ctx, COPY_GAS * words_for_bytes(32));
351    Ok(PrecompileOutput::new(
352        (*gas_used).min(gas_limit),
353        out.to_vec().into(),
354    ))
355}
356
357fn handle_map_l1_sender(
358    input: &mut PrecompileInput<'_>,
359    gas_used: &mut u64,
360    ctx: &ArbPrecompileCtx,
361    l1_addr: Address,
362) -> PrecompileResult {
363    let aliased = apply_l1_alias(l1_addr);
364    let gas_limit = input.gas;
365    let mut out = [0u8; 32];
366    out[12..32].copy_from_slice(aliased.as_slice());
367    // `mapL1SenderContractAddressToL2Alias` is `pure` — no OpenArbosState read,
368    // init already charged argsCost only. Body adds result_cost as Computation.
369    crate::charge_computation(gas_used, ctx, COPY_GAS * words_for_bytes(32));
370    Ok(PrecompileOutput::new(
371        (*gas_used).min(gas_limit),
372        out.to_vec().into(),
373    ))
374}
375
376fn handle_get_storage_gas(
377    input: &mut PrecompileInput<'_>,
378    gas_used: &mut u64,
379    ctx: &ArbPrecompileCtx,
380) -> PrecompileResult {
381    let gas_limit = input.gas;
382    crate::charge_computation(gas_used, ctx, COPY_GAS * words_for_bytes(32));
383    Ok(PrecompileOutput::new(
384        (*gas_used).min(gas_limit),
385        U256::ZERO.to_be_bytes::<32>().to_vec().into(),
386    ))
387}
388
389// ── L2→L1 messaging ─────────────────────────────────────────────────
390
391fn handle_withdraw_eth(
392    input: &mut PrecompileInput<'_>,
393    gas_used: &mut u64,
394    ctx: &ArbPrecompileCtx,
395    destination: Address,
396) -> PrecompileResult {
397    if input.is_static {
398        return Err(ArbPrecompileError::empty_revert(*gas_used).into());
399    }
400    do_send_tx_to_l1(input, gas_used, ctx, destination, &[])
401}
402
403fn handle_send_tx_to_l1(
404    input: &mut PrecompileInput<'_>,
405    gas_used: &mut u64,
406    ctx: &ArbPrecompileCtx,
407    destination: Address,
408    calldata: &[u8],
409) -> PrecompileResult {
410    if input.is_static {
411        return Err(ArbPrecompileError::empty_revert(*gas_used).into());
412    }
413    do_send_tx_to_l1(input, gas_used, ctx, destination, calldata)
414}
415
416fn do_send_tx_to_l1(
417    input: &mut PrecompileInput<'_>,
418    gas_used: &mut u64,
419    ctx: &ArbPrecompileCtx,
420    destination: Address,
421    calldata: &[u8],
422) -> PrecompileResult {
423    let caller = input.caller;
424    let value = input.value;
425    let gas_limit = input.gas;
426    // Read the L1 block number recorded by StartBlock. `block_env.number` holds
427    // the header's mix_hash L1 value, which can lag the StartBlock-updated one.
428    let l1_block_number = U256::from(ctx.block.l1_block_number_for_evm);
429    let l2_block_number = U256::from(ctx.block.l2_block_number);
430    let timestamp = input.internals().block_timestamp();
431
432    let internals = input.internals_mut();
433
434    internals
435        .load_account(ARBOS_STATE_ADDRESS)
436        .map_err(ArbPrecompileError::fatal)?;
437
438    let arb_state = ctx
439        .block
440        .arbos_state(internals)
441        .map_err(ArbPrecompileError::fatal)?;
442    let arbos_version = arb_state.arbos_version();
443
444    // ArbOS v41+: prevent sending value when native token owners exist.
445    if !value.is_zero() && arbos_version >= 41 {
446        crate::charge_storage_read(gas_used, ctx, STORAGE_READ_COST);
447        let num_owners = arb_state
448            .native_token_owners
449            .size(internals)
450            .map_err(ArbPrecompileError::fatal)?;
451        if num_owners != 0 {
452            return Err(ArbPrecompileError::empty_revert(*gas_used).into());
453        }
454    }
455
456    // Merkle accumulator size: one read before append, one phantom read after.
457    crate::charge_storage_read(gas_used, ctx, STORAGE_READ_COST);
458    let old_size = arb_state
459        .send_merkle_accumulator
460        .size(internals)
461        .map_err(ArbPrecompileError::fatal)?;
462
463    // keccak hash burn — pure computation.
464    let send_hash_input_len = 20 + 20 + 32 * 4 + calldata.len() as u64;
465    crate::charge_computation(gas_used, ctx, keccak_gas(send_hash_input_len));
466    let send_hash = compute_send_hash(
467        caller,
468        destination,
469        l2_block_number,
470        l1_block_number,
471        timestamp,
472        value,
473        calldata,
474    );
475
476    let merkle_events = arb_state
477        .send_merkle_accumulator
478        .append(internals, send_hash)
479        .map_err(ArbPrecompileError::fatal)?;
480    let new_size = old_size + 1;
481
482    // Per-level merge: one read + one keccak + one write at the reset price.
483    // The append's outer terminator is either an extra read+write or just a
484    // write depending on whether the last merge consumed all old partials.
485    let num_partials_old = calc_num_partials(old_size);
486    let n_events = merkle_events.len() as u64;
487    let per_merge_keccak = keccak_gas(64);
488    crate::charge_storage_read(gas_used, ctx, n_events * STORAGE_READ_COST);
489    crate::charge_computation(gas_used, ctx, n_events * per_merge_keccak);
490    crate::charge_storage_write(gas_used, ctx, n_events * STORAGE_WRITE_ZERO_COST);
491    if n_events == num_partials_old {
492        crate::charge_storage_write(gas_used, ctx, STORAGE_WRITE_COST);
493    } else {
494        crate::charge_storage_read(gas_used, ctx, STORAGE_READ_COST);
495        crate::charge_storage_write(gas_used, ctx, STORAGE_WRITE_COST);
496    }
497    crate::charge_storage_write(gas_used, ctx, STORAGE_WRITE_COST); // size.set
498    crate::charge_storage_read(gas_used, ctx, STORAGE_READ_COST); // phantom post-Append size
499
500    // Emit SendMerkleUpdate events (one per intermediate node, all topics, empty data).
501    let update_topic = send_merkle_update_topic();
502    for evt in &merkle_events {
503        // position = (level << 192) + numLeaves
504        let position: U256 = (U256::from(evt.level) << 192) | U256::from(evt.num_leaves);
505        internals.log(Log::new_unchecked(
506            ARBSYS_ADDRESS,
507            vec![
508                update_topic,
509                B256::from(U256::ZERO.to_be_bytes::<32>()),
510                evt.hash,
511                B256::from(position.to_be_bytes::<32>()),
512            ],
513            Default::default(),
514        ));
515        // 4 topics (event_id + 3 indexed), 0 data bytes.
516        crate::charge_history_growth(gas_used, ctx, LOG_GAS + LOG_TOPIC_GAS * 4);
517    }
518
519    let leaf_num = new_size - 1;
520
521    // Emit L2ToL1Tx event.
522    // Topics: [event_id, destination (indexed), hash (indexed), position (indexed)]
523    // Data: ABI-encoded [caller, arbBlockNum, ethBlockNum, timestamp, callvalue, bytes]
524    let l2l1_topic = l2_to_l1_tx_topic();
525    let dest_topic = B256::left_padding_from(destination.as_slice());
526    let hash_topic = B256::from(U256::from_be_bytes(send_hash.0).to_be_bytes::<32>());
527    let position_topic = B256::from(U256::from(leaf_num).to_be_bytes::<32>());
528
529    let mut event_data = Vec::with_capacity(256);
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    event_data.extend_from_slice(&l2_block_number.to_be_bytes::<32>());
534    event_data.extend_from_slice(&l1_block_number.to_be_bytes::<32>());
535    event_data.extend_from_slice(&timestamp.to_be_bytes::<32>());
536    event_data.extend_from_slice(&value.to_be_bytes::<32>());
537    event_data.extend_from_slice(&U256::from(6 * 32).to_be_bytes::<32>());
538    event_data.extend_from_slice(&U256::from(calldata.len()).to_be_bytes::<32>());
539    event_data.extend_from_slice(calldata);
540    let pad = (32 - calldata.len() % 32) % 32;
541    event_data.extend(std::iter::repeat_n(0u8, pad));
542
543    let l2l1_data_len = event_data.len() as u64;
544    internals.log(Log::new_unchecked(
545        ARBSYS_ADDRESS,
546        vec![l2l1_topic, dest_topic, hash_topic, position_topic],
547        event_data.into(),
548    ));
549    crate::charge_history_growth(
550        gas_used,
551        ctx,
552        LOG_GAS + LOG_TOPIC_GAS * 4 + LOG_DATA_GAS * l2l1_data_len,
553    );
554
555    let return_val = if arbos_version >= 4 {
556        U256::from(leaf_num)
557    } else {
558        U256::from_be_bytes(send_hash.0)
559    };
560
561    let output = return_val.to_be_bytes::<32>().to_vec();
562    crate::charge_computation(
563        gas_used,
564        ctx,
565        COPY_GAS * words_for_bytes(output.len() as u64),
566    );
567
568    Ok(PrecompileOutput::new(
569        (*gas_used).min(gas_limit),
570        output.into(),
571    ))
572}
573
574fn handle_send_merkle_tree_state(
575    input: &mut PrecompileInput<'_>,
576    gas_used: &mut u64,
577    ctx: &ArbPrecompileCtx,
578) -> PrecompileResult {
579    // Only callable by address zero (for state export).
580    if input.caller != Address::ZERO {
581        return Err(ArbPrecompileError::empty_revert(*gas_used).into());
582    }
583    let gas_limit = input.gas;
584    let internals = input.internals_mut();
585
586    internals
587        .load_account(ARBOS_STATE_ADDRESS)
588        .map_err(ArbPrecompileError::fatal)?;
589
590    let arb_state = ctx
591        .block
592        .arbos_state(internals)
593        .map_err(ArbPrecompileError::fatal)?;
594
595    crate::charge_storage_read(gas_used, ctx, STORAGE_READ_COST);
596    let size_u64 = arb_state
597        .send_merkle_accumulator
598        .size(internals)
599        .map_err(ArbPrecompileError::fatal)?;
600    let size = U256::from(size_u64);
601
602    let num_partials = calc_num_partials(size_u64);
603    let mut partials = Vec::new();
604    for i in 0..num_partials {
605        crate::charge_storage_read(gas_used, ctx, STORAGE_READ_COST);
606        let val = arb_state
607            .send_merkle_accumulator
608            .partial_at(internals, i)
609            .map_err(ArbPrecompileError::fatal)?;
610        partials.push(val);
611    }
612
613    let root = compute_merkle_root(&partials, size_u64);
614
615    // ABI: uint256 size, bytes32 root, bytes32[] partials
616    let num_partials = partials.len();
617    let mut out = Vec::with_capacity(96 + num_partials * 32);
618    out.extend_from_slice(&size.to_be_bytes::<32>());
619    out.extend_from_slice(&root.0);
620    out.extend_from_slice(&U256::from(96u64).to_be_bytes::<32>());
621    out.extend_from_slice(&U256::from(num_partials).to_be_bytes::<32>());
622    for p in &partials {
623        out.extend_from_slice(p.0.as_slice());
624    }
625
626    crate::charge_computation(gas_used, ctx, COPY_GAS * words_for_bytes(out.len() as u64));
627    Ok(PrecompileOutput::new(
628        (*gas_used).min(gas_limit),
629        out.into(),
630    ))
631}
632
633// ── Merkle helpers ───────────────────────────────────────────────────
634
635fn compute_send_hash(
636    sender: Address,
637    dest: Address,
638    arb_block_num: U256,
639    eth_block_num: U256,
640    timestamp: U256,
641    value: U256,
642    data: &[u8],
643) -> B256 {
644    // Uses raw 20-byte addresses (no left-padding to 32 bytes).
645    let mut preimage = Vec::with_capacity(200 + data.len());
646    preimage.extend_from_slice(sender.as_slice()); // 20 bytes
647    preimage.extend_from_slice(dest.as_slice()); // 20 bytes
648    preimage.extend_from_slice(&arb_block_num.to_be_bytes::<32>());
649    preimage.extend_from_slice(&eth_block_num.to_be_bytes::<32>());
650    preimage.extend_from_slice(&timestamp.to_be_bytes::<32>());
651    preimage.extend_from_slice(&value.to_be_bytes::<32>());
652    preimage.extend_from_slice(data);
653    keccak256(&preimage)
654}
655
656/// Compute the merkle root from partials (MerkleAccumulator.Root()).
657///
658/// Pads with zero hashes when capacity gaps exist between populated partial levels.
659fn compute_merkle_root(partials: &[B256], size: u64) -> B256 {
660    if partials.is_empty() || size == 0 {
661        return B256::ZERO;
662    }
663
664    let num_partials = calc_num_partials(size);
665    let mut hash_so_far: Option<B256> = None;
666    let mut capacity_in_hash: u64 = 0;
667    let mut capacity: u64 = 1;
668
669    for level in 0..num_partials {
670        let partial = if (level as usize) < partials.len() {
671            partials[level as usize]
672        } else {
673            B256::ZERO
674        };
675
676        if partial != B256::ZERO {
677            match hash_so_far {
678                None => {
679                    hash_so_far = Some(partial);
680                    capacity_in_hash = capacity;
681                }
682                Some(ref h) => {
683                    // Pad with zero hashes until capacity matches.
684                    let mut current = *h;
685                    let mut cap = capacity_in_hash;
686                    while cap < capacity {
687                        let mut preimage = [0u8; 64];
688                        preimage[..32].copy_from_slice(current.as_slice());
689                        // second 32 bytes remain zero
690                        current = keccak256(preimage);
691                        cap *= 2;
692                    }
693                    // Combine: keccak256(partial || current)
694                    let mut preimage = [0u8; 64];
695                    preimage[..32].copy_from_slice(partial.as_slice());
696                    preimage[32..].copy_from_slice(current.as_slice());
697                    let combined = keccak256(preimage);
698                    hash_so_far = Some(combined);
699                    capacity_in_hash = 2 * capacity;
700                }
701            }
702        }
703        capacity *= 2;
704    }
705
706    hash_so_far.unwrap_or(B256::ZERO)
707}
708
709// ── L1 alias helpers ─────────────────────────────────────────────────
710
711fn alias_offset_u256() -> U256 {
712    U256::from_be_slice(L1_ALIAS_OFFSET.as_slice())
713}
714
715fn truncate_to_address(v: U256) -> Address {
716    let bytes = v.to_be_bytes::<32>();
717    Address::from_slice(&bytes[12..])
718}
719
720fn apply_l1_alias(addr: Address) -> Address {
721    let val = U256::from_be_slice(addr.as_slice());
722    truncate_to_address(val.wrapping_add(alias_offset_u256()))
723}
724
725fn undo_l1_alias(addr: Address) -> Address {
726    let val = U256::from_be_slice(addr.as_slice());
727    truncate_to_address(val.wrapping_sub(alias_offset_u256()))
728}
729
730#[cfg(test)]
731mod alias_tests {
732    use alloy_primitives::address;
733
734    use super::*;
735
736    #[test]
737    fn alias_simple_no_carry() {
738        let l1 = address!("0000000000000000000000000000000000000000");
739        let aliased = apply_l1_alias(l1);
740        assert_eq!(aliased, L1_ALIAS_OFFSET);
741        assert_eq!(undo_l1_alias(aliased), l1);
742    }
743
744    #[test]
745    fn alias_carry_propagates_across_bytes() {
746        let l1 = address!("00ef000000000000000000000000000000000000");
747        let expected = address!("1200000000000000000000000000000000001111");
748        assert_eq!(apply_l1_alias(l1), expected);
749        assert_eq!(undo_l1_alias(expected), l1);
750    }
751
752    #[test]
753    fn alias_wraps_at_160_bits() {
754        // (2^160 - 1) + 0x1111000000000000000000000000000000001111
755        //   = 2^160 + (0x1111000000000000000000000000000000001110)
756        //   ≡ 0x1111000000000000000000000000000000001110 (mod 2^160)
757        let l1 = address!("ffffffffffffffffffffffffffffffffffffffff");
758        let expected = address!("1111000000000000000000000000000000001110");
759        assert_eq!(apply_l1_alias(l1), expected);
760        assert_eq!(undo_l1_alias(expected), l1);
761    }
762
763    #[test]
764    fn alias_inverse_round_trip() {
765        let cases = [
766            address!("0123456789abcdef0123456789abcdef01234567"),
767            address!("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"),
768            address!("ffeeffeeffeeffeeffeeffeeffeeffeeffeeffee"),
769        ];
770        for addr in cases {
771            let aliased = apply_l1_alias(addr);
772            let restored = undo_l1_alias(aliased);
773            assert_eq!(restored, addr, "round trip failed for {addr}");
774        }
775    }
776}
777
778#[cfg(test)]
779mod version_tests {
780    use super::*;
781
782    #[test]
783    fn arb_os_version_returns_format_plus_55() {
784        // formatVersion 51 → user-visible ArbOS version 106 (0x6a)
785        assert_eq!(arbos_version_from_format(U256::from(51)), U256::from(106),);
786        // formatVersion 1 → 56 (the lowest publicly used version)
787        assert_eq!(arbos_version_from_format(U256::from(1)), U256::from(56),);
788        // formatVersion 0 → 55
789        assert_eq!(arbos_version_from_format(U256::ZERO), U256::from(55),);
790    }
791}