arb_precompiles/
arbwasm.rs

1use std::sync::Arc;
2
3use alloy_evm::precompiles::{DynPrecompile, PrecompileInput};
4use alloy_primitives::{Address, B256, Log, U256};
5use alloy_sol_types::{SolError, SolEvent, SolInterface};
6use arb_context::ArbPrecompileCtx;
7use arb_storage::ARBOS_STATE_ADDRESS;
8use arbos::programs::{Program, hours_since_arbitrum, hours_to_age, params::StylusParams};
9use revm::precompile::{PrecompileId, PrecompileOutput, PrecompileResult};
10
11use crate::{ArbPrecompileError, interfaces::IArbWasm};
12
13/// ArbWasm precompile address (0x71).
14pub const ARBWASM_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, 0x71,
17]);
18
19const SLOAD_GAS: u64 = 800;
20const SSTORE_GAS: u64 = 20_000;
21const COPY_GAS: u64 = 3;
22const WARM_SLOAD_GAS: u64 = 100;
23const STORAGE_CODE_HASH_COST: u64 = 2_600;
24const FRAMEWORK_GAS_PROGRAM_ADDR: u64 = COPY_GAS + 800;
25const PROGRAM_LOOKUP_GAS: u64 =
26    FRAMEWORK_GAS_PROGRAM_ADDR + WARM_SLOAD_GAS + STORAGE_CODE_HASH_COST + SLOAD_GAS;
27
28const MIN_INIT_GAS_UNITS: u64 = 128;
29const MIN_CACHED_GAS_UNITS: u64 = 32;
30const COST_SCALAR_PERCENT: u64 = 2;
31
32pub fn create_arbwasm_precompile(ctx: Arc<ArbPrecompileCtx>) -> DynPrecompile {
33    DynPrecompile::new_stateful(PrecompileId::custom("arbwasm"), move |input| {
34        handler(input, &ctx)
35    })
36}
37
38fn handler(mut input: PrecompileInput<'_>, ctx: &ArbPrecompileCtx) -> PrecompileResult {
39    if let Some(result) =
40        crate::check_precompile_version(ctx, arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS)
41    {
42        return result;
43    }
44
45    let gas_limit = input.gas;
46
47    let call = match IArbWasm::ArbWasmCalls::abi_decode(input.data) {
48        Ok(c) => c,
49        Err(_) => return crate::burn_all_revert(gas_limit),
50    };
51
52    if let Some(r) = crate::reject_nonpayable_value(
53        input.value,
54        input.data,
55        gas_limit,
56        &[[0x58, 0xc7, 0x80, 0xc2], [0xc6, 0x89, 0xba, 0xd5]],
57    ) {
58        return r;
59    }
60    if let Some(r) = crate::reject_static_write(
61        input.is_static,
62        input.data,
63        gas_limit,
64        &[[0x58, 0xc7, 0x80, 0xc2], [0xc6, 0x89, 0xba, 0xd5]],
65    ) {
66        return r;
67    }
68    if let Some(r) = crate::reject_delegate_nonpure(
69        input.target_address != input.bytecode_address,
70        input.data,
71        gas_limit,
72        &[],
73    ) {
74        return r;
75    }
76
77    use IArbWasm::ArbWasmCalls as Calls;
78    match &call {
79        Calls::activateProgram(c) => return handle_activate_program(input, ctx, c.program),
80        Calls::codehashKeepalive(c) => return handle_codehash_keepalive(input, ctx, c.codehash),
81        _ => {}
82    }
83
84    let mut gas_used = 0u64;
85    crate::init_precompile_gas(&mut gas_used, ctx, input.data.len());
86
87    let result = match call {
88        Calls::stylusVersion(_) => {
89            let params = load_params(&mut input, &mut gas_used, ctx)?;
90            ok_u256(&mut gas_used, ctx, input.gas, U256::from(params.version))
91        }
92        Calls::inkPrice(_) => {
93            let params = load_params(&mut input, &mut gas_used, ctx)?;
94            ok_u256(&mut gas_used, ctx, input.gas, U256::from(params.ink_price))
95        }
96        Calls::maxStackDepth(_) => {
97            let params = load_params(&mut input, &mut gas_used, ctx)?;
98            ok_u256(
99                &mut gas_used,
100                ctx,
101                input.gas,
102                U256::from(params.max_stack_depth),
103            )
104        }
105        Calls::freePages(_) => {
106            let params = load_params(&mut input, &mut gas_used, ctx)?;
107            ok_u256(&mut gas_used, ctx, input.gas, U256::from(params.free_pages))
108        }
109        Calls::pageGas(_) => {
110            let params = load_params(&mut input, &mut gas_used, ctx)?;
111            ok_u256(&mut gas_used, ctx, input.gas, U256::from(params.page_gas))
112        }
113        Calls::pageRamp(_) => {
114            let params = load_params(&mut input, &mut gas_used, ctx)?;
115            ok_u256(&mut gas_used, ctx, input.gas, U256::from(params.page_ramp))
116        }
117        Calls::pageLimit(_) => {
118            let params = load_params(&mut input, &mut gas_used, ctx)?;
119            ok_u256(&mut gas_used, ctx, input.gas, U256::from(params.page_limit))
120        }
121        Calls::minInitGas(_) => {
122            let params = load_params(&mut input, &mut gas_used, ctx)?;
123            if ctx.block.arbos_version
124                < arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS_CHARGING_FIXES
125            {
126                let pre_revert_gas = (SLOAD_GAS + WARM_SLOAD_GAS).min(input.gas);
127                return Ok(PrecompileOutput::new_reverted(
128                    pre_revert_gas,
129                    Default::default(),
130                ));
131            }
132            let init = (params.min_init_gas as u64).saturating_mul(MIN_INIT_GAS_UNITS);
133            let cached = (params.min_cached_init_gas as u64).saturating_mul(MIN_CACHED_GAS_UNITS);
134            ok_two_u256(
135                &mut gas_used,
136                ctx,
137                input.gas,
138                U256::from(init),
139                U256::from(cached),
140            )
141        }
142        Calls::initCostScalar(_) => {
143            let params = load_params(&mut input, &mut gas_used, ctx)?;
144            let scalar = params.init_cost_scalar as u64;
145            ok_u256(
146                &mut gas_used,
147                ctx,
148                input.gas,
149                U256::from(scalar.saturating_mul(COST_SCALAR_PERCENT)),
150            )
151        }
152        Calls::expiryDays(_) => {
153            let params = load_params(&mut input, &mut gas_used, ctx)?;
154            ok_u256(
155                &mut gas_used,
156                ctx,
157                input.gas,
158                U256::from(params.expiry_days),
159            )
160        }
161        Calls::keepaliveDays(_) => {
162            let params = load_params(&mut input, &mut gas_used, ctx)?;
163            ok_u256(
164                &mut gas_used,
165                ctx,
166                input.gas,
167                U256::from(params.keepalive_days),
168            )
169        }
170        Calls::blockCacheSize(_) => {
171            let params = load_params(&mut input, &mut gas_used, ctx)?;
172            ok_u256(
173                &mut gas_used,
174                ctx,
175                input.gas,
176                U256::from(params.block_cache_size),
177            )
178        }
179        Calls::activationGas(_) => {
180            if let Some(r) = crate::check_method_version(
181                ctx,
182                input.gas,
183                arb_chainspec::arbos_version::ARBOS_VERSION_59,
184                0,
185            ) {
186                return r;
187            }
188            load_arbos(&mut input)?;
189            let internals = input.internals_mut();
190            let arb_state = ctx
191                .block
192                .arbos_state(internals)
193                .map_err(ArbPrecompileError::fatal)?;
194            let gas = arb_state
195                .programs
196                .activation_gas(internals)
197                .map_err(ArbPrecompileError::fatal)?;
198            crate::charge_storage_read(&mut gas_used, ctx, SLOAD_GAS);
199            ok_u256(&mut gas_used, ctx, input.gas, U256::from(gas))
200        }
201        Calls::codehashVersion(c) => {
202            const LOOKUP_GAS: u64 = SLOAD_GAS + WARM_SLOAD_GAS + SLOAD_GAS + COPY_GAS;
203
204            let (params, program) =
205                load_params_and_program(&mut input, ctx, &mut gas_used, c.codehash)?;
206            if let Err(r) = validate_active_program(
207                &program,
208                params.version,
209                params.expiry_days,
210                input.gas,
211                LOOKUP_GAS,
212            ) {
213                return r;
214            }
215            ok_u256(&mut gas_used, ctx, input.gas, U256::from(program.version))
216        }
217        Calls::codehashAsmSize(c) => {
218            const LOOKUP_GAS: u64 = SLOAD_GAS + WARM_SLOAD_GAS + SLOAD_GAS + COPY_GAS;
219
220            let (params, program) =
221                load_params_and_program(&mut input, ctx, &mut gas_used, c.codehash)?;
222            if let Err(r) = validate_active_program(
223                &program,
224                params.version,
225                params.expiry_days,
226                input.gas,
227                LOOKUP_GAS,
228            ) {
229                return r;
230            }
231            ok_u256(
232                &mut gas_used,
233                ctx,
234                input.gas,
235                U256::from(program.asm_size()),
236            )
237        }
238        Calls::programVersion(c) => {
239            let codehash = get_account_codehash(&mut input, ctx, &mut gas_used, c.program)?;
240            let (params, program) =
241                load_params_and_program(&mut input, ctx, &mut gas_used, codehash)?;
242            if let Err(r) = validate_active_program(
243                &program,
244                params.version,
245                params.expiry_days,
246                input.gas,
247                PROGRAM_LOOKUP_GAS,
248            ) {
249                return r;
250            }
251            ok_u256(&mut gas_used, ctx, input.gas, U256::from(program.version))
252        }
253        Calls::programInitGas(c) => {
254            let codehash = get_account_codehash(&mut input, ctx, &mut gas_used, c.program)?;
255            let (params, program) =
256                load_params_and_program(&mut input, ctx, &mut gas_used, codehash)?;
257            if let Err(r) = validate_active_program(
258                &program,
259                params.version,
260                params.expiry_days,
261                input.gas,
262                PROGRAM_LOOKUP_GAS,
263            ) {
264                return r;
265            }
266
267            let mut init_gas = program.init_gas(&params);
268            let cached_gas = program.cached_gas(&params);
269            if params.version > 1 {
270                init_gas = init_gas.saturating_add(cached_gas);
271            }
272
273            ok_two_u256(
274                &mut gas_used,
275                ctx,
276                input.gas,
277                U256::from(init_gas),
278                U256::from(cached_gas),
279            )
280        }
281        Calls::programMemoryFootprint(c) => {
282            let codehash = get_account_codehash(&mut input, ctx, &mut gas_used, c.program)?;
283            let (params, program) =
284                load_params_and_program(&mut input, ctx, &mut gas_used, codehash)?;
285            if let Err(r) = validate_active_program(
286                &program,
287                params.version,
288                params.expiry_days,
289                input.gas,
290                PROGRAM_LOOKUP_GAS,
291            ) {
292                return r;
293            }
294            ok_u256(&mut gas_used, ctx, input.gas, U256::from(program.footprint))
295        }
296        Calls::programTimeLeft(c) => {
297            let codehash = get_account_codehash(&mut input, ctx, &mut gas_used, c.program)?;
298            let (params, program) =
299                load_params_and_program(&mut input, ctx, &mut gas_used, codehash)?;
300            if let Err(r) = validate_active_program(
301                &program,
302                params.version,
303                params.expiry_days,
304                input.gas,
305                PROGRAM_LOOKUP_GAS,
306            ) {
307                return r;
308            }
309
310            let expiry_seconds = (params.expiry_days as u64) * 24 * 3600;
311            let time_left = expiry_seconds.saturating_sub(program.age_seconds);
312            ok_u256(&mut gas_used, ctx, input.gas, U256::from(time_left))
313        }
314        Calls::activateProgram(_) | Calls::codehashKeepalive(_) => unreachable!(),
315    };
316    crate::gas_check(ctx, input.gas, gas_used, result)
317}
318
319// ── Helpers ──────────────────────────────────────────────────────────
320
321fn load_arbos(input: &mut PrecompileInput<'_>) -> Result<(), ArbPrecompileError> {
322    input
323        .internals_mut()
324        .load_account(ARBOS_STATE_ADDRESS)
325        .map_err(ArbPrecompileError::fatal)?;
326    Ok(())
327}
328
329/// Load the active Stylus parameters. The framework's OpenArbosState SLOAD is
330/// already charged by `init_precompile_gas`; here we only add the warm follow-up
331/// access for the params slot.
332fn load_params(
333    input: &mut PrecompileInput<'_>,
334    gas_used: &mut u64,
335    ctx: &ArbPrecompileCtx,
336) -> Result<StylusParams, ArbPrecompileError> {
337    load_arbos(input)?;
338    let internals = input.internals_mut();
339    let arb_state = ctx
340        .block
341        .arbos_state(internals)
342        .map_err(ArbPrecompileError::fatal)?;
343    let params = arb_state
344        .programs
345        .params(internals)
346        .map_err(ArbPrecompileError::fatal)?;
347    crate::charge_params_read(gas_used, ctx);
348    Ok(params)
349}
350
351fn load_params_and_program(
352    input: &mut PrecompileInput<'_>,
353    ctx: &ArbPrecompileCtx,
354    gas_used: &mut u64,
355    codehash: B256,
356) -> Result<(StylusParams, Program), ArbPrecompileError> {
357    load_arbos(input)?;
358    let time = ctx.block.block_timestamp;
359    let internals = input.internals_mut();
360    let arb_state = ctx
361        .block
362        .arbos_state(internals)
363        .map_err(ArbPrecompileError::fatal)?;
364    let params = arb_state
365        .programs
366        .params(internals)
367        .map_err(ArbPrecompileError::fatal)?;
368    let program = arb_state
369        .programs
370        .get_program(internals, codehash, time)
371        .map_err(ArbPrecompileError::fatal)?;
372    // params (warm, computation) + program slot (one SLOAD).
373    crate::charge_params_read(gas_used, ctx);
374    crate::charge_storage_read(gas_used, ctx, SLOAD_GAS);
375    Ok((params, program))
376}
377
378/// Read an account's code hash, charging the per-lookup code-hash storage read.
379fn get_account_codehash(
380    input: &mut PrecompileInput<'_>,
381    ctx: &ArbPrecompileCtx,
382    gas_used: &mut u64,
383    address: Address,
384) -> Result<B256, ArbPrecompileError> {
385    let codehash = crate::without_access_list_effect(input.internals_mut(), |internals| {
386        Ok::<_, ArbPrecompileError>(
387            internals
388                .load_account(address)
389                .map_err(ArbPrecompileError::fatal)?
390                .data
391                .info
392                .code_hash,
393        )
394    })?;
395    crate::charge_storage_read(gas_used, ctx, STORAGE_CODE_HASH_COST);
396    Ok(codehash)
397}
398
399/// Returns ProgramNotActivated, ProgramNeedsUpgrade(progV, paramsV),
400/// or ProgramExpired(ageSeconds), in that order. `lookup_gas` is the
401/// method's argsCost + state-access charges (everything except the
402/// result-copy cost). The revert charges `lookup_gas + COPY_GAS * words`
403/// where `words` is rounded up from the actual error payload length.
404fn validate_active_program(
405    program: &Program,
406    params_version: u16,
407    expiry_days: u16,
408    gas_limit: u64,
409    lookup_gas: u64,
410) -> Result<(), PrecompileResult> {
411    if program.version == 0 {
412        let data = IArbWasm::ProgramNotActivated {}.abi_encode();
413        return Err(revert_with_payload(data, lookup_gas, gas_limit));
414    }
415    if program.version != params_version {
416        let data = IArbWasm::ProgramNeedsUpgrade {
417            version: program.version,
418            stylusVersion: params_version,
419        }
420        .abi_encode();
421        return Err(revert_with_payload(data, lookup_gas, gas_limit));
422    }
423    let expiry_seconds = (expiry_days as u64).saturating_mul(86_400);
424    if program.age_seconds > expiry_seconds {
425        let data = IArbWasm::ProgramExpired {
426            ageInSeconds: program.age_seconds,
427        }
428        .abi_encode();
429        return Err(revert_with_payload(data, lookup_gas, gas_limit));
430    }
431    Ok(())
432}
433
434/// Revert with `lookup_gas + CopyGas * ceil(payload_len / 32)` charged.
435fn revert_with_payload(payload: Vec<u8>, lookup_gas: u64, gas_limit: u64) -> PrecompileResult {
436    let result_cost = COPY_GAS.saturating_mul((payload.len() as u64).div_ceil(32));
437    let gas_used = lookup_gas.saturating_add(result_cost);
438    Ok(PrecompileOutput::new_reverted(
439        gas_used.min(gas_limit),
440        payload.into(),
441    ))
442}
443
444/// Return a single `uint256` view result. Charges the result-copy as
445/// computation and returns whatever the gas accumulator currently holds.
446fn ok_u256(
447    gas_used: &mut u64,
448    ctx: &ArbPrecompileCtx,
449    gas_limit: u64,
450    value: U256,
451) -> PrecompileResult {
452    crate::charge_computation(gas_used, ctx, COPY_GAS);
453    Ok(PrecompileOutput::new(
454        (*gas_used).min(gas_limit),
455        value.to_be_bytes::<32>().to_vec().into(),
456    ))
457}
458
459fn ok_two_u256(
460    gas_used: &mut u64,
461    ctx: &ArbPrecompileCtx,
462    gas_limit: u64,
463    a: U256,
464    b: U256,
465) -> PrecompileResult {
466    let mut out = Vec::with_capacity(64);
467    out.extend_from_slice(&a.to_be_bytes::<32>());
468    out.extend_from_slice(&b.to_be_bytes::<32>());
469    crate::charge_computation(gas_used, ctx, 2 * COPY_GAS);
470    Ok(PrecompileOutput::new(
471        (*gas_used).min(gas_limit),
472        out.into(),
473    ))
474}
475
476fn div_ceil(a: u64, b: u64) -> u64 {
477    a.div_ceil(b)
478}
479
480fn handle_activate_program(
481    mut input: PrecompileInput<'_>,
482    ctx: &ArbPrecompileCtx,
483    program_address: Address,
484) -> PrecompileResult {
485    const ACTIVATION_UPFRONT_GAS: u64 = 1_659_168;
486
487    let mut gas_used = 0u64;
488    let args_cost = COPY_GAS * (input.data.len() as u64).saturating_sub(4).div_ceil(32);
489    crate::charge_l2_calldata(&mut gas_used, ctx, args_cost);
490    crate::charge_storage_read(&mut gas_used, ctx, SLOAD_GAS);
491
492    if ctx.block.arbos_version >= arb_chainspec::arbos_version::ARBOS_VERSION_59 {
493        load_arbos(&mut input)?;
494        let internals = input.internals_mut();
495        let arb_state = ctx
496            .block
497            .arbos_state(internals)
498            .map_err(ArbPrecompileError::fatal)?;
499        let activation_gas = arb_state
500            .programs
501            .activation_gas(internals)
502            .map_err(ArbPrecompileError::fatal)?;
503        crate::charge_storage_read(&mut gas_used, ctx, SLOAD_GAS);
504        // The configurable activation gas is burned up front (default 0).
505        crate::charge_computation(&mut gas_used, ctx, activation_gas);
506    }
507
508    crate::charge_computation(&mut gas_used, ctx, ACTIVATION_UPFRONT_GAS);
509
510    let (code_hash, code_bytes) =
511        crate::without_access_list_effect(input.internals_mut(), |internals| {
512            let code_hash = internals
513                .load_account(program_address)
514                .map_err(ArbPrecompileError::fatal)?
515                .data
516                .info
517                .code_hash;
518            let code_bytes = internals
519                .load_account_code(program_address)
520                .map_err(ArbPrecompileError::fatal)?
521                .data
522                .code()
523                .map(|c| c.original_bytes())
524                .unwrap_or_default()
525                .to_vec();
526            Ok::<_, ArbPrecompileError>((code_hash, code_bytes))
527        })?;
528
529    load_arbos(&mut input)?;
530    let time = ctx.block.block_timestamp;
531    crate::charge_computation(&mut gas_used, ctx, WARM_SLOAD_GAS);
532    let (params, existing_program) = {
533        let internals = input.internals_mut();
534        let arb_state = ctx
535            .block
536            .arbos_state(internals)
537            .map_err(ArbPrecompileError::fatal)?;
538        let params = arb_state
539            .programs
540            .params(internals)
541            .map_err(ArbPrecompileError::fatal)?;
542        let existing = arb_state
543            .programs
544            .get_program(internals, code_hash, time)
545            .map_err(ArbPrecompileError::fatal)?;
546        (params, existing)
547    };
548    crate::charge_storage_read(&mut gas_used, ctx, SLOAD_GAS);
549
550    // An already-current program short-circuits before reconstructing its WASM,
551    // so a fragmented root is not billed for fragment reads it never needs.
552    if existing_program.version == params.version
553        && existing_program.age_seconds <= (params.expiry_days as u64) * 86400
554    {
555        return crate::revert_sol_error(
556            &mut gas_used,
557            ctx,
558            IArbWasm::ProgramUpToDate {}.abi_encode(),
559            input.gas,
560        );
561    }
562
563    if code_bytes.is_empty() {
564        return crate::revert_sol_error(
565            &mut gas_used,
566            ctx,
567            IArbWasm::ProgramNotWasm {}.abi_encode(),
568            input.gas,
569        );
570    }
571    if !arb_stylus::is_stylus_deployable(&code_bytes, ctx.block.arbos_version) {
572        let arbos_v = ctx.block.arbos_version;
573        if arbos_v < arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS_CONTRACT_LIMIT
574            || arb_stylus::is_stylus_fragment(&code_bytes)
575        {
576            return Ok(PrecompileOutput::new_reverted(
577                gas_used.min(input.gas),
578                Default::default(),
579            ));
580        }
581        return crate::revert_sol_error(
582            &mut gas_used,
583            ctx,
584            IArbWasm::ProgramNotWasm {}.abi_encode(),
585            input.gas,
586        );
587    }
588
589    // A root program's WASM lives in fragment contracts referenced by the root;
590    // reconstruct it, charging each fragment read and enforcing the size and
591    // count limits. A classic program decompresses inline.
592    let gas_limit = input.gas;
593    let wasm_result = if arb_stylus::is_stylus_root(&code_bytes) {
594        const MAX_FRAGMENT_CODE_SIZE: u64 = 24_576;
595        let max_wasm_size = params.max_wasm_size;
596        let max_fragment_count = params.max_fragment_count;
597        arb_stylus::get_wasm_from_root(
598            &code_bytes,
599            max_wasm_size,
600            max_fragment_count,
601            true,
602            |addr| {
603                let (warm, code) = {
604                    let load = input
605                        .internals_mut()
606                        .load_account_code(addr)
607                        .map_err(|e| arb_stylus::StylusError::Activation(format!("{e}")))?;
608                    let code = load
609                        .data
610                        .code()
611                        .map(|c| c.original_bytes())
612                        .unwrap_or_default()
613                        .to_vec();
614                    (!load.is_cold, code)
615                };
616                // Fail before charging the actual read unless a max-sized
617                // fragment read is affordable; out of gas consumes all of it.
618                if gas_limit.saturating_sub(gas_used)
619                    < arb_stylus::fragment_read_gas(warm, MAX_FRAGMENT_CODE_SIZE)
620                {
621                    gas_used = gas_limit;
622                    return Err(arb_stylus::StylusError::InvalidProgram(
623                        "out of gas reading fragment",
624                    ));
625                }
626                crate::charge_storage_read(
627                    &mut gas_used,
628                    ctx,
629                    arb_stylus::fragment_read_gas(warm, code.len() as u64),
630                );
631                Ok(code)
632            },
633        )
634    } else {
635        arb_stylus::decompress_wasm(&code_bytes)
636    };
637    let wasm = match wasm_result {
638        Ok(w) => w,
639        Err(_) => {
640            // Reconstruction and decompression failures surface as non-solidity
641            // errors, which revert with empty data and no result-copy charge.
642            return Ok(PrecompileOutput::new_reverted(
643                gas_used.min(input.gas),
644                Default::default(),
645            ));
646        }
647    };
648
649    let was_cached = existing_program.cached;
650
651    let gas_available = input.gas.saturating_sub(gas_used);
652    let mut gas_for_prover = gas_available;
653
654    let info = match arb_stylus::activate_program(
655        &wasm,
656        code_hash.as_ref(),
657        params.version,
658        ctx.block.arbos_version,
659        params.page_limit,
660        false,
661        &mut gas_for_prover,
662    ) {
663        Ok(info) => info,
664        Err(_) => {
665            crate::charge_computation(&mut gas_used, ctx, gas_available);
666            return Err(ArbPrecompileError::empty_revert(gas_used).into());
667        }
668    };
669
670    let prover_gas_used = gas_available.saturating_sub(gas_for_prover);
671    crate::charge_computation(&mut gas_used, ctx, prover_gas_used);
672
673    // Re-activating a cached program reads its previous module hash so the stale
674    // cache entry can be evicted before the new one is stored.
675    if was_cached {
676        let _prev_module_hash = {
677            let internals = input.internals_mut();
678            let arb_state = ctx
679                .block
680                .arbos_state(internals)
681                .map_err(ArbPrecompileError::fatal)?;
682            arb_state
683                .programs
684                .get_module_hash(internals, code_hash)
685                .map_err(ArbPrecompileError::fatal)?
686        };
687        crate::charge_storage_read(&mut gas_used, ctx, SLOAD_GAS);
688    }
689
690    {
691        let internals = input.internals_mut();
692        let arb_state = ctx
693            .block
694            .arbos_state(internals)
695            .map_err(ArbPrecompileError::fatal)?;
696        arb_state
697            .programs
698            .set_module_hash(internals, code_hash, info.module_hash)
699            .map_err(ArbPrecompileError::fatal)?;
700    }
701    crate::charge_storage_write(&mut gas_used, ctx, SSTORE_GAS);
702
703    let data_fee = {
704        let internals = input.internals_mut();
705        let arb_state = ctx
706            .block
707            .arbos_state(internals)
708            .map_err(ArbPrecompileError::fatal)?;
709        arb_state
710            .programs
711            .data_pricer
712            .update_model(internals, info.asm_estimate, time)
713            .map_err(ArbPrecompileError::fatal)?
714    };
715    crate::charge_storage_read(&mut gas_used, ctx, 5 * SLOAD_GAS);
716    crate::charge_storage_write(&mut gas_used, ctx, 2 * SSTORE_GAS);
717
718    let estimate_kb = div_ceil(info.asm_estimate as u64, 1024).min(0xFF_FFFF) as u32;
719    let new_program = Program {
720        version: params.version,
721        init_cost: info.init_gas,
722        cached_cost: info.cached_init_gas,
723        footprint: info.footprint,
724        asm_estimate_kb: estimate_kb,
725        activated_at: hours_since_arbitrum(time),
726        age_seconds: 0,
727        cached: was_cached,
728    };
729    {
730        let internals = input.internals_mut();
731        let arb_state = ctx
732            .block
733            .arbos_state(internals)
734            .map_err(ArbPrecompileError::fatal)?;
735        arb_state
736            .programs
737            .set_program(internals, code_hash, new_program)
738            .map_err(ArbPrecompileError::fatal)?;
739    }
740    crate::charge_storage_write(&mut gas_used, ctx, SSTORE_GAS);
741
742    let stashed_outer_value = ctx.stylus_call_value();
743    let inner_call_value = input.value;
744    let effective_value = if inner_call_value > U256::ZERO {
745        inner_call_value
746    } else {
747        stashed_outer_value
748    };
749    if effective_value < data_fee {
750        return crate::revert_sol_error(
751            &mut gas_used,
752            ctx,
753            IArbWasm::ProgramInsufficientValue {
754                have: effective_value,
755                want: data_fee,
756            }
757            .abi_encode(),
758            input.gas,
759        );
760    }
761
762    if inner_call_value > U256::ZERO {
763        let caller = input.caller;
764        let network_addr = {
765            let internals = input.internals_mut();
766            let arb_state = ctx
767                .block
768                .arbos_state(internals)
769                .map_err(ArbPrecompileError::fatal)?;
770            arb_state
771                .network_fee_account(internals)
772                .map_err(ArbPrecompileError::fatal)?
773        };
774        crate::charge_storage_read(&mut gas_used, ctx, SLOAD_GAS);
775        let _ = input
776            .internals_mut()
777            .transfer(ARBWASM_ADDRESS, network_addr, data_fee);
778        let repay = inner_call_value.saturating_sub(data_fee);
779        if repay > U256::ZERO {
780            let _ = input
781                .internals_mut()
782                .transfer(ARBWASM_ADDRESS, caller, repay);
783        }
784        ctx.set_stylus_activation_addr(Some(program_address));
785    } else {
786        crate::charge_storage_read(&mut gas_used, ctx, SLOAD_GAS);
787        ctx.set_stylus_activation_addr(Some(program_address));
788        ctx.set_stylus_activation_data_fee(data_fee);
789    }
790
791    let event_topic = IArbWasm::ProgramActivated::SIGNATURE_HASH;
792    let mut event_data = Vec::with_capacity(128);
793    event_data.extend_from_slice(&info.module_hash.0);
794    event_data.extend_from_slice(&[0u8; 12]);
795    event_data.extend_from_slice(program_address.as_slice());
796    event_data.extend_from_slice(&data_fee.to_be_bytes::<32>());
797    let mut ver = [0u8; 32];
798    ver[30..32].copy_from_slice(&params.version.to_be_bytes());
799    event_data.extend_from_slice(&ver);
800    let event_gas = 375 + 2 * 375 + 8 * event_data.len() as u64;
801    crate::charge_history_growth(&mut gas_used, ctx, event_gas);
802    input.internals_mut().log(Log::new_unchecked(
803        ARBWASM_ADDRESS,
804        vec![event_topic, code_hash],
805        event_data.into(),
806    ));
807
808    let return_data = {
809        let mut output = Vec::with_capacity(64);
810        let mut ver_out = [0u8; 32];
811        ver_out[30..32].copy_from_slice(&params.version.to_be_bytes());
812        output.extend_from_slice(&ver_out);
813        output.extend_from_slice(&data_fee.to_be_bytes::<32>());
814        output
815    };
816    let return_gas = COPY_GAS * (return_data.len() as u64).div_ceil(32);
817    crate::charge_computation(&mut gas_used, ctx, return_gas);
818
819    if gas_used > input.gas {
820        return Err(ArbPrecompileError::OutOfGas.into());
821    }
822    Ok(PrecompileOutput::new(gas_used, return_data.into()))
823}
824
825fn handle_codehash_keepalive(
826    mut input: PrecompileInput<'_>,
827    ctx: &ArbPrecompileCtx,
828    codehash: B256,
829) -> PrecompileResult {
830    let mut gas_used = 0u64;
831    let args_cost = COPY_GAS * (input.data.len() as u64).saturating_sub(4).div_ceil(32);
832    crate::charge_l2_calldata(&mut gas_used, ctx, args_cost);
833
834    load_arbos(&mut input)?;
835    let time = ctx.block.block_timestamp;
836    let (params, mut program) = {
837        let internals = input.internals_mut();
838        let arb_state = ctx
839            .block
840            .arbos_state(internals)
841            .map_err(ArbPrecompileError::fatal)?;
842        let params = arb_state
843            .programs
844            .params(internals)
845            .map_err(ArbPrecompileError::fatal)?;
846        let program = arb_state
847            .programs
848            .get_program(internals, codehash, time)
849            .map_err(ArbPrecompileError::fatal)?;
850        (params, program)
851    };
852    crate::charge_params_read(&mut gas_used, ctx);
853    crate::charge_storage_read(&mut gas_used, ctx, 2 * SLOAD_GAS);
854
855    if program.version == 0 {
856        return crate::revert_sol_error(
857            &mut gas_used,
858            ctx,
859            IArbWasm::ProgramNotActivated {}.abi_encode(),
860            input.gas,
861        );
862    }
863    if program.version != params.version {
864        return crate::revert_sol_error(
865            &mut gas_used,
866            ctx,
867            IArbWasm::ProgramNeedsUpgrade {
868                version: program.version,
869                stylusVersion: params.version,
870            }
871            .abi_encode(),
872            input.gas,
873        );
874    }
875    let age = hours_to_age(time, program.activated_at);
876    if age > (params.expiry_days as u64) * 86400 {
877        return crate::revert_sol_error(
878            &mut gas_used,
879            ctx,
880            IArbWasm::ProgramExpired { ageInSeconds: age }.abi_encode(),
881            input.gas,
882        );
883    }
884    if age < (params.keepalive_days as u64) * 86400 {
885        return crate::revert_sol_error(
886            &mut gas_used,
887            ctx,
888            IArbWasm::ProgramKeepaliveTooSoon { ageInSeconds: age }.abi_encode(),
889            input.gas,
890        );
891    }
892
893    let asm_size = program.asm_size();
894
895    let data_fee = {
896        let internals = input.internals_mut();
897        let arb_state = ctx
898            .block
899            .arbos_state(internals)
900            .map_err(ArbPrecompileError::fatal)?;
901        arb_state
902            .programs
903            .data_pricer
904            .update_model(internals, asm_size, time)
905            .map_err(ArbPrecompileError::fatal)?
906    };
907    crate::charge_storage_read(&mut gas_used, ctx, 5 * SLOAD_GAS);
908    crate::charge_storage_write(&mut gas_used, ctx, 2 * SSTORE_GAS);
909
910    program.activated_at = hours_since_arbitrum(time);
911    program.age_seconds = 0;
912    {
913        let internals = input.internals_mut();
914        let arb_state = ctx
915            .block
916            .arbos_state(internals)
917            .map_err(ArbPrecompileError::fatal)?;
918        arb_state
919            .programs
920            .set_program(internals, codehash, program)
921            .map_err(ArbPrecompileError::fatal)?;
922    }
923    crate::charge_storage_write(&mut gas_used, ctx, SSTORE_GAS);
924
925    let stashed_outer_value = ctx.stylus_call_value();
926    let inner_call_value = input.value;
927    let effective_value = if inner_call_value > U256::ZERO {
928        inner_call_value
929    } else {
930        stashed_outer_value
931    };
932    if effective_value < data_fee {
933        return crate::revert_sol_error(
934            &mut gas_used,
935            ctx,
936            IArbWasm::ProgramInsufficientValue {
937                have: effective_value,
938                want: data_fee,
939            }
940            .abi_encode(),
941            input.gas,
942        );
943    }
944
945    if inner_call_value > U256::ZERO {
946        let caller = input.caller;
947        let network_addr = {
948            let internals = input.internals_mut();
949            let arb_state = ctx
950                .block
951                .arbos_state(internals)
952                .map_err(ArbPrecompileError::fatal)?;
953            arb_state
954                .network_fee_account(internals)
955                .map_err(ArbPrecompileError::fatal)?
956        };
957        crate::charge_storage_read(&mut gas_used, ctx, SLOAD_GAS);
958        let _ = input
959            .internals_mut()
960            .transfer(ARBWASM_ADDRESS, network_addr, data_fee);
961        let repay = inner_call_value.saturating_sub(data_fee);
962        if repay > U256::ZERO {
963            let _ = input
964                .internals_mut()
965                .transfer(ARBWASM_ADDRESS, caller, repay);
966        }
967        ctx.set_stylus_keepalive_hash(Some(codehash));
968    } else {
969        crate::charge_storage_read(&mut gas_used, ctx, SLOAD_GAS);
970        ctx.set_stylus_keepalive_hash(Some(codehash));
971        ctx.set_stylus_activation_data_fee(data_fee);
972    }
973
974    let event_topic = IArbWasm::ProgramLifetimeExtended::SIGNATURE_HASH;
975    let mut event_data = Vec::with_capacity(32);
976    event_data.extend_from_slice(&data_fee.to_be_bytes::<32>());
977    let event_gas = 375 + 2 * 375 + 8 * event_data.len() as u64;
978    crate::charge_history_growth(&mut gas_used, ctx, event_gas);
979    input.internals_mut().log(Log::new_unchecked(
980        ARBWASM_ADDRESS,
981        vec![event_topic, codehash],
982        event_data.into(),
983    ));
984
985    if gas_used > input.gas {
986        return Err(ArbPrecompileError::OutOfGas.into());
987    }
988    Ok(PrecompileOutput::new(gas_used, Vec::new().into()))
989}
990
991#[cfg(test)]
992mod failure_gas_tests {
993    use super::*;
994
995    fn unactivated() -> Program {
996        Program {
997            version: 0,
998            init_cost: 0,
999            cached_cost: 0,
1000            footprint: 0,
1001            asm_estimate_kb: 0,
1002            activated_at: 0,
1003            age_seconds: 0,
1004            cached: false,
1005        }
1006    }
1007
1008    fn revert_gas(lookup_gas: u64) -> u64 {
1009        let r = validate_active_program(&unactivated(), 1, 365, 1_000_000, lookup_gas)
1010            .expect_err("unactivated program should revert");
1011        let out = r.expect("revert wraps an Ok(PrecompileOutput)");
1012        out.gas_used
1013    }
1014
1015    #[test]
1016    fn codehash_asm_size_failure_charges_lookup_plus_error_word() {
1017        const LOOKUP_GAS: u64 = SLOAD_GAS + WARM_SLOAD_GAS + SLOAD_GAS + COPY_GAS;
1018        assert_eq!(LOOKUP_GAS, 1703);
1019        assert_eq!(revert_gas(LOOKUP_GAS), 1706);
1020    }
1021
1022    #[test]
1023    fn program_version_family_failure_charges_lookup_plus_error_word() {
1024        assert_eq!(PROGRAM_LOOKUP_GAS, 4303);
1025        assert_eq!(revert_gas(PROGRAM_LOOKUP_GAS), 4306);
1026    }
1027
1028    #[test]
1029    fn revert_is_capped_at_gas_limit() {
1030        let r = validate_active_program(&unactivated(), 1, 365, 500, 1703)
1031            .expect_err("unactivated program should revert");
1032        let out = r.expect("revert wraps an Ok(PrecompileOutput)");
1033        assert_eq!(out.gas_used, 500);
1034    }
1035}