arb_precompiles/
arbwasm.rs

1use alloy_evm::precompiles::{DynPrecompile, PrecompileInput};
2use alloy_primitives::{Address, B256, U256};
3use alloy_sol_types::{SolError, SolEvent, SolInterface};
4use revm::precompile::{PrecompileError, PrecompileId, PrecompileOutput, PrecompileResult};
5
6use crate::{
7    interfaces::IArbWasm,
8    storage_slot::{
9        derive_subspace_key, map_slot, map_slot_b256, ARBOS_STATE_ADDRESS, PROGRAMS_DATA_KEY,
10        PROGRAMS_PARAMS_KEY, PROGRAMS_SUBSPACE, ROOT_STORAGE_KEY,
11    },
12};
13
14/// ArbWasm precompile address (0x71).
15pub const ARBWASM_ADDRESS: Address = Address::new([
16    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
17    0x00, 0x00, 0x00, 0x71,
18]);
19
20const SLOAD_GAS: u64 = 800;
21const SSTORE_GAS: u64 = 20_000;
22const COPY_GAS: u64 = 3;
23const WARM_SLOAD_GAS: u64 = 100;
24
25/// Initial page ramp constant (not stored in packed params).
26const INITIAL_PAGE_RAMP: u64 = 620674314;
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() -> DynPrecompile {
33    DynPrecompile::new_stateful(PrecompileId::custom("arbwasm"), handler)
34}
35
36fn handler(mut input: PrecompileInput<'_>) -> PrecompileResult {
37    if let Some(result) =
38        crate::check_precompile_version(arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS)
39    {
40        return result;
41    }
42
43    let gas_limit = input.gas;
44
45    let call = match IArbWasm::ArbWasmCalls::abi_decode(input.data) {
46        Ok(c) => c,
47        Err(_) => return crate::burn_all_revert(gas_limit),
48    };
49
50    use IArbWasm::ArbWasmCalls as Calls;
51    // State-modifying methods own their gas accounting.
52    match &call {
53        Calls::activateProgram(c) => return handle_activate_program(input, c.program),
54        Calls::codehashKeepalive(c) => return handle_codehash_keepalive(input, c.codehash),
55        _ => {}
56    }
57
58    crate::init_precompile_gas(input.data.len());
59
60    let result = match call {
61        Calls::stylusVersion(_) => {
62            let params = load_params_word(&mut input)?;
63            let version = u16::from_be_bytes([params[0], params[1]]);
64            ok_u256(SLOAD_GAS + COPY_GAS, U256::from(version))
65        }
66        Calls::inkPrice(_) => {
67            let params = load_params_word(&mut input)?;
68            let ink_price = (params[2] as u32) << 16 | (params[3] as u32) << 8 | params[4] as u32;
69            ok_u256(SLOAD_GAS + COPY_GAS, U256::from(ink_price))
70        }
71        Calls::maxStackDepth(_) => {
72            let params = load_params_word(&mut input)?;
73            let depth = u32::from_be_bytes([params[5], params[6], params[7], params[8]]);
74            ok_u256(SLOAD_GAS + COPY_GAS, U256::from(depth))
75        }
76        Calls::freePages(_) => {
77            let params = load_params_word(&mut input)?;
78            let pages = u16::from_be_bytes([params[9], params[10]]);
79            ok_u256(SLOAD_GAS + COPY_GAS, U256::from(pages))
80        }
81        Calls::pageGas(_) => {
82            let params = load_params_word(&mut input)?;
83            let gas = u16::from_be_bytes([params[11], params[12]]);
84            ok_u256(SLOAD_GAS + COPY_GAS, U256::from(gas))
85        }
86        Calls::pageRamp(_) => {
87            load_arbos(&mut input)?;
88            ok_u256(COPY_GAS, U256::from(INITIAL_PAGE_RAMP))
89        }
90        Calls::pageLimit(_) => {
91            let params = load_params_word(&mut input)?;
92            let limit = u16::from_be_bytes([params[13], params[14]]);
93            ok_u256(SLOAD_GAS + COPY_GAS, U256::from(limit))
94        }
95        Calls::minInitGas(_) => {
96            if let Some(result) = crate::check_method_version(
97                input.gas,
98                arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS_CHARGING_FIXES,
99                0,
100            ) {
101                return result;
102            }
103            let params = load_params_word(&mut input)?;
104            let min_init = params[15] as u64;
105            let min_cached = params[16] as u64;
106            let init = min_init.saturating_mul(MIN_INIT_GAS_UNITS);
107            let cached = min_cached.saturating_mul(MIN_CACHED_GAS_UNITS);
108            ok_two_u256(SLOAD_GAS + COPY_GAS, U256::from(init), U256::from(cached))
109        }
110        Calls::initCostScalar(_) => {
111            let params = load_params_word(&mut input)?;
112            let scalar = params[17] as u64;
113            ok_u256(
114                SLOAD_GAS + COPY_GAS,
115                U256::from(scalar.saturating_mul(COST_SCALAR_PERCENT)),
116            )
117        }
118        Calls::expiryDays(_) => {
119            let params = load_params_word(&mut input)?;
120            let days = u16::from_be_bytes([params[19], params[20]]);
121            ok_u256(SLOAD_GAS + COPY_GAS, U256::from(days))
122        }
123        Calls::keepaliveDays(_) => {
124            let params = load_params_word(&mut input)?;
125            let days = u16::from_be_bytes([params[21], params[22]]);
126            ok_u256(SLOAD_GAS + COPY_GAS, U256::from(days))
127        }
128        Calls::blockCacheSize(_) => {
129            let params = load_params_word(&mut input)?;
130            let size = u16::from_be_bytes([params[23], params[24]]);
131            ok_u256(SLOAD_GAS + COPY_GAS, U256::from(size))
132        }
133        Calls::activationGas(_) => {
134            let programs_key = derive_subspace_key(ROOT_STORAGE_KEY, PROGRAMS_SUBSPACE);
135            let activation_key = derive_subspace_key(programs_key.as_slice(), &[5]);
136            let slot = map_slot(activation_key.as_slice(), 0);
137            let gas = sload_field(&mut input, slot)?;
138            ok_u256(SLOAD_GAS + COPY_GAS, gas)
139        }
140        Calls::codehashVersion(c) => {
141            let (params_word, program_word) = load_params_and_program(&mut input, c.codehash)?;
142            let params_version = u16::from_be_bytes([params_word[0], params_word[1]]);
143            let expiry_days = params_expiry_days(&params_word);
144            let program = parse_program(&program_word, &params_word);
145            if let Err(r) =
146                validate_active_program(&program, params_version, expiry_days, input.gas)
147            {
148                return r;
149            }
150            ok_u256(2 * SLOAD_GAS + COPY_GAS, U256::from(program.version))
151        }
152        Calls::codehashAsmSize(c) => {
153            let (params_word, program_word) = load_params_and_program(&mut input, c.codehash)?;
154            let params_version = u16::from_be_bytes([params_word[0], params_word[1]]);
155            let expiry_days = params_expiry_days(&params_word);
156            let program = parse_program(&program_word, &params_word);
157            if let Err(r) =
158                validate_active_program(&program, params_version, expiry_days, input.gas)
159            {
160                return r;
161            }
162            let asm_size = program.asm_estimate_kb.saturating_mul(1024);
163            ok_u256(
164                SLOAD_GAS + WARM_SLOAD_GAS + SLOAD_GAS + 2 * COPY_GAS,
165                U256::from(asm_size),
166            )
167        }
168        Calls::programVersion(c) => {
169            let codehash = get_account_codehash(&mut input, c.program)?;
170            let (params_word, program_word) = load_params_and_program(&mut input, codehash)?;
171            let params_version = u16::from_be_bytes([params_word[0], params_word[1]]);
172            let expiry_days = params_expiry_days(&params_word);
173            let program = parse_program(&program_word, &params_word);
174            if let Err(r) =
175                validate_active_program(&program, params_version, expiry_days, input.gas)
176            {
177                return r;
178            }
179            ok_u256(3 * SLOAD_GAS + COPY_GAS, U256::from(program.version))
180        }
181        Calls::programInitGas(c) => {
182            let codehash = get_account_codehash(&mut input, c.program)?;
183            let (params_word, program_word) = load_params_and_program(&mut input, codehash)?;
184            let params_version = u16::from_be_bytes([params_word[0], params_word[1]]);
185            let expiry_days = params_expiry_days(&params_word);
186            let program = parse_program(&program_word, &params_word);
187            if let Err(r) =
188                validate_active_program(&program, params_version, expiry_days, input.gas)
189            {
190                return r;
191            }
192
193            let min_init = params_word[15] as u64;
194            let min_cached = params_word[16] as u64;
195            let init_cost_scalar = params_word[17] as u64;
196            let cached_cost_scalar = params_word[18] as u64;
197
198            let init_base = min_init.saturating_mul(MIN_INIT_GAS_UNITS);
199            let init_dyno =
200                (program.init_cost as u64).saturating_mul(init_cost_scalar * COST_SCALAR_PERCENT);
201            let mut init_gas = init_base.saturating_add(div_ceil(init_dyno, 100));
202
203            let cached_base = min_cached.saturating_mul(MIN_CACHED_GAS_UNITS);
204            let cached_dyno = (program.cached_cost as u64)
205                .saturating_mul(cached_cost_scalar * COST_SCALAR_PERCENT);
206            let cached_gas = cached_base.saturating_add(div_ceil(cached_dyno, 100));
207
208            if params_version > 1 {
209                init_gas = init_gas.saturating_add(cached_gas);
210            }
211
212            ok_two_u256(
213                3 * SLOAD_GAS + COPY_GAS,
214                U256::from(init_gas),
215                U256::from(cached_gas),
216            )
217        }
218        Calls::programMemoryFootprint(c) => {
219            let codehash = get_account_codehash(&mut input, c.program)?;
220            let (params_word, program_word) = load_params_and_program(&mut input, codehash)?;
221            let params_version = u16::from_be_bytes([params_word[0], params_word[1]]);
222            let expiry_days = params_expiry_days(&params_word);
223            let program = parse_program(&program_word, &params_word);
224            if let Err(r) =
225                validate_active_program(&program, params_version, expiry_days, input.gas)
226            {
227                return r;
228            }
229            ok_u256(3 * SLOAD_GAS + COPY_GAS, U256::from(program.footprint))
230        }
231        Calls::programTimeLeft(c) => {
232            let codehash = get_account_codehash(&mut input, c.program)?;
233            let (params_word, program_word) = load_params_and_program(&mut input, codehash)?;
234            let params_version = u16::from_be_bytes([params_word[0], params_word[1]]);
235            let expiry_days = params_expiry_days(&params_word);
236            let program = parse_program(&program_word, &params_word);
237            if let Err(r) =
238                validate_active_program(&program, params_version, expiry_days, input.gas)
239            {
240                return r;
241            }
242
243            let expiry_seconds = (expiry_days as u64) * 24 * 3600;
244            let time_left = expiry_seconds.saturating_sub(program.age_seconds);
245            ok_u256(3 * SLOAD_GAS + COPY_GAS, U256::from(time_left))
246        }
247        Calls::activateProgram(_) | Calls::codehashKeepalive(_) => unreachable!(),
248    };
249    crate::gas_check(input.gas, result)
250}
251
252// ── Helpers ──────────────────────────────────────────────────────────
253
254fn params_expiry_days(params_word: &[u8; 32]) -> u16 {
255    u16::from_be_bytes([params_word[19], params_word[20]])
256}
257
258fn load_arbos(input: &mut PrecompileInput<'_>) -> Result<(), PrecompileError> {
259    input
260        .internals_mut()
261        .load_account(ARBOS_STATE_ADDRESS)
262        .map_err(|e| PrecompileError::other(format!("load_account: {e:?}")))?;
263    Ok(())
264}
265
266fn sload_field(input: &mut PrecompileInput<'_>, slot: U256) -> Result<U256, PrecompileError> {
267    let val = input
268        .internals_mut()
269        .sload(ARBOS_STATE_ADDRESS, slot)
270        .map_err(|_| PrecompileError::other("sload failed"))?;
271    crate::charge_precompile_gas(SLOAD_GAS);
272    Ok(val.data)
273}
274
275/// Load the packed StylusParams word (slot 0) from storage.
276fn load_params_word(input: &mut PrecompileInput<'_>) -> Result<[u8; 32], PrecompileError> {
277    load_arbos(input)?;
278    let programs_key = derive_subspace_key(ROOT_STORAGE_KEY, PROGRAMS_SUBSPACE);
279    let params_key = derive_subspace_key(programs_key.as_slice(), PROGRAMS_PARAMS_KEY);
280    let slot = map_slot(params_key.as_slice(), 0);
281    let value = sload_field(input, slot)?;
282    Ok(value.to_be_bytes::<32>())
283}
284
285fn load_params_and_program(
286    input: &mut PrecompileInput<'_>,
287    codehash: B256,
288) -> Result<([u8; 32], [u8; 32]), PrecompileError> {
289    load_arbos(input)?;
290    let programs_key = derive_subspace_key(ROOT_STORAGE_KEY, PROGRAMS_SUBSPACE);
291
292    // Params
293    let params_key = derive_subspace_key(programs_key.as_slice(), PROGRAMS_PARAMS_KEY);
294    let params_slot = map_slot(params_key.as_slice(), 0);
295    let params_value = sload_field(input, params_slot)?;
296
297    let data_key = derive_subspace_key(programs_key.as_slice(), PROGRAMS_DATA_KEY);
298    let program_slot = map_slot_b256(data_key.as_slice(), &codehash);
299    let program_value = sload_field(input, program_slot)?;
300
301    Ok((
302        params_value.to_be_bytes::<32>(),
303        program_value.to_be_bytes::<32>(),
304    ))
305}
306
307/// Get the code hash for an account address.
308fn get_account_codehash(
309    input: &mut PrecompileInput<'_>,
310    address: Address,
311) -> Result<B256, PrecompileError> {
312    let account = input
313        .internals_mut()
314        .load_account(address)
315        .map_err(|e| PrecompileError::other(format!("load_account: {e:?}")))?;
316    Ok(account.data.info.code_hash)
317}
318
319/// Parsed program entry from a storage word.
320struct ProgramInfo {
321    version: u16,
322    init_cost: u16,
323    cached_cost: u16,
324    footprint: u16,
325    asm_estimate_kb: u32,
326    age_seconds: u64,
327}
328
329/// Arbitrum start time (epoch for encoding hours in program data).
330const ARBITRUM_START_TIME: u64 = 1421388000;
331
332fn parse_program(data: &[u8; 32], params_word: &[u8; 32]) -> ProgramInfo {
333    let version = u16::from_be_bytes([data[0], data[1]]);
334    let init_cost = u16::from_be_bytes([data[2], data[3]]);
335    let cached_cost = u16::from_be_bytes([data[4], data[5]]);
336    let footprint = u16::from_be_bytes([data[6], data[7]]);
337    let activated_at = (data[8] as u32) << 16 | (data[9] as u32) << 8 | data[10] as u32;
338    let asm_estimate_kb = (data[11] as u32) << 16 | (data[12] as u32) << 8 | data[13] as u32;
339
340    let _ = params_word;
341    let age_seconds = hours_to_age(block_timestamp(), activated_at);
342
343    ProgramInfo {
344        version,
345        init_cost,
346        cached_cost,
347        footprint,
348        asm_estimate_kb,
349        age_seconds,
350    }
351}
352
353/// Get the current block timestamp from the thread-local.
354fn block_timestamp() -> u64 {
355    crate::get_block_timestamp()
356}
357
358fn hours_to_age(time: u64, hours: u32) -> u64 {
359    let seconds = (hours as u64).saturating_mul(3600);
360    let activated_at = ARBITRUM_START_TIME.saturating_add(seconds);
361    time.saturating_sub(activated_at)
362}
363
364/// Returns ProgramNotActivated, ProgramNeedsUpgrade(progV, paramsV),
365/// or ProgramExpired(ageSeconds), in that order.
366fn validate_active_program(
367    program: &ProgramInfo,
368    params_version: u16,
369    expiry_days: u16,
370    gas_limit: u64,
371) -> Result<(), PrecompileResult> {
372    if program.version == 0 {
373        let data = IArbWasm::ProgramNotActivated {}.abi_encode();
374        return Err(crate::sol_error_revert(data, gas_limit));
375    }
376    if program.version != params_version {
377        let data = IArbWasm::ProgramNeedsUpgrade {
378            version: program.version,
379            stylusVersion: params_version,
380        }
381        .abi_encode();
382        return Err(crate::sol_error_revert(data, gas_limit));
383    }
384    let expiry_seconds = (expiry_days as u64).saturating_mul(86_400);
385    if program.age_seconds > expiry_seconds {
386        let data = IArbWasm::ProgramExpired {
387            ageInSeconds: program.age_seconds,
388        }
389        .abi_encode();
390        return Err(crate::sol_error_revert(data, gas_limit));
391    }
392    Ok(())
393}
394
395fn ok_u256(gas_cost: u64, value: U256) -> PrecompileResult {
396    Ok(PrecompileOutput::new(
397        gas_cost,
398        value.to_be_bytes::<32>().to_vec().into(),
399    ))
400}
401
402fn ok_two_u256(gas_cost: u64, a: U256, b: U256) -> PrecompileResult {
403    let mut out = Vec::with_capacity(64);
404    out.extend_from_slice(&a.to_be_bytes::<32>());
405    out.extend_from_slice(&b.to_be_bytes::<32>());
406    Ok(PrecompileOutput::new(gas_cost, out.into()))
407}
408
409fn div_ceil(a: u64, b: u64) -> u64 {
410    a.div_ceil(b)
411}
412
413fn hours_since_arbitrum(time: u64) -> u32 {
414    let elapsed = time.saturating_sub(ARBITRUM_START_TIME);
415    (elapsed / 3600) as u32
416}
417
418/// Approximates `b * e^(x/b)` where `b = 10_000` (basis points), via Horner's
419/// method with accuracy=12.
420fn approx_exp_basis_points(x: u64) -> u64 {
421    let b = 10_000u64;
422    let accuracy = 12u64;
423    let mut res = b + x / accuracy;
424    for i in (1..accuracy).rev() {
425        res = b + res.saturating_mul(x) / (i * b);
426    }
427    res
428}
429
430fn handle_activate_program(
431    mut input: PrecompileInput<'_>,
432    program_address: Address,
433) -> PrecompileResult {
434    const ACTIVATION_UPFRONT_GAS: u64 = 1_659_168;
435
436    crate::reset_precompile_gas();
437    let args_cost = COPY_GAS * (input.data.len() as u64).saturating_sub(4).div_ceil(32);
438    crate::charge_precompile_gas(args_cost);
439    crate::charge_precompile_gas(SLOAD_GAS); // OpenArbosState version read
440    crate::charge_precompile_gas(ACTIVATION_UPFRONT_GAS);
441
442    let code_hash = {
443        let account = input
444            .internals_mut()
445            .load_account(program_address)
446            .map_err(|e| PrecompileError::other(format!("load_account: {e:?}")))?;
447        account.data.info.code_hash
448    };
449
450    let code_bytes = {
451        let code_account = input
452            .internals_mut()
453            .load_account_code(program_address)
454            .map_err(|e| PrecompileError::other(format!("load_account_code: {e:?}")))?;
455        code_account
456            .data
457            .code()
458            .map(|c| c.original_bytes())
459            .unwrap_or_default()
460            .to_vec()
461    };
462
463    if code_bytes.is_empty()
464        || !arb_stylus::is_stylus_deployable(&code_bytes, crate::get_arbos_version())
465    {
466        return Err(PrecompileError::other("ProgramNotWasm()"));
467    }
468
469    let wasm = arb_stylus::decompress_wasm(&code_bytes)
470        .map_err(|e| PrecompileError::other(format!("ProgramNotWasm: {e}")))?;
471
472    // Params: charge WarmStorageReadCost (100), subsequent reads are free.
473    load_arbos(&mut input)?;
474    crate::charge_precompile_gas(100); // WarmStorageReadCostEIP2929
475    let params_word = {
476        let programs_key = derive_subspace_key(ROOT_STORAGE_KEY, PROGRAMS_SUBSPACE);
477        let params_key = derive_subspace_key(programs_key.as_slice(), PROGRAMS_PARAMS_KEY);
478        let slot = map_slot(params_key.as_slice(), 0);
479        let val = input
480            .internals_mut()
481            .sload(ARBOS_STATE_ADDRESS, slot)
482            .map_err(|_| PrecompileError::other("sload failed"))?
483            .data;
484        val.to_be_bytes::<32>()
485    };
486    let params_version = u16::from_be_bytes([params_word[0], params_word[1]]);
487    let page_limit = u16::from_be_bytes([params_word[13], params_word[14]]);
488
489    let time = block_timestamp();
490    let programs_key = derive_subspace_key(ROOT_STORAGE_KEY, PROGRAMS_SUBSPACE);
491    let data_key = derive_subspace_key(programs_key.as_slice(), PROGRAMS_DATA_KEY);
492    let program_slot = map_slot_b256(data_key.as_slice(), &code_hash);
493
494    let existing = sload_field(&mut input, program_slot)?;
495    let existing_bytes = existing.to_be_bytes::<32>();
496    let existing_version = u16::from_be_bytes([existing_bytes[0], existing_bytes[1]]);
497    let was_cached = existing_bytes[14] != 0;
498
499    if existing_version == params_version {
500        let activated_at = (existing_bytes[8] as u32) << 16
501            | (existing_bytes[9] as u32) << 8
502            | existing_bytes[10] as u32;
503        let age = hours_to_age(time, activated_at);
504        let expiry_days = u16::from_be_bytes([params_word[19], params_word[20]]);
505        if age <= (expiry_days as u64) * 86400 {
506            return Err(PrecompileError::other("ProgramUpToDate()"));
507        }
508    }
509
510    let gas_available = input.gas.saturating_sub(crate::get_precompile_gas());
511    let mut gas_for_prover = gas_available;
512    let diag_pre_prover = crate::get_precompile_gas();
513    let diag_gas_to_prover = gas_for_prover;
514
515    let info = match arb_stylus::activate_program(
516        &wasm,
517        code_hash.as_ref(),
518        params_version,
519        crate::get_arbos_version(),
520        page_limit,
521        false,
522        &mut gas_for_prover,
523    ) {
524        Ok(info) => info,
525        Err(e) => {
526            crate::charge_precompile_gas(gas_available);
527            return Err(PrecompileError::other(format!("{e}")));
528        }
529    };
530
531    let prover_gas_used = gas_available.saturating_sub(gas_for_prover);
532    crate::charge_precompile_gas(prover_gas_used);
533    let wasm_hash = alloy_primitives::keccak256(&wasm);
534    tracing::warn!(target: "stylus",
535        input_gas = input.gas, pre_prover = diag_pre_prover, to_prover = diag_gas_to_prover,
536        prover_used = prover_gas_used, after_prover = crate::get_precompile_gas(),
537        wasm_len = wasm.len(), %wasm_hash, "activateProgram gas breakdown");
538
539    // Store module hash
540    let module_hashes_key = derive_subspace_key(programs_key.as_slice(), &[2]);
541    let module_hash_slot = map_slot_b256(module_hashes_key.as_slice(), &code_hash);
542    input
543        .internals_mut()
544        .sstore(
545            ARBOS_STATE_ADDRESS,
546            module_hash_slot,
547            U256::from_be_bytes(info.module_hash.0),
548        )
549        .map_err(|_| PrecompileError::other("sstore failed"))?;
550    crate::charge_precompile_gas(SSTORE_GAS);
551    let data_pricer_key = derive_subspace_key(programs_key.as_slice(), &[3]);
552    let demand: u32 =
553        sload_field(&mut input, map_slot(data_pricer_key.as_slice(), 0))?.to::<u64>() as u32;
554    let bps: u32 =
555        sload_field(&mut input, map_slot(data_pricer_key.as_slice(), 1))?.to::<u64>() as u32;
556    let last_update: u64 =
557        sload_field(&mut input, map_slot(data_pricer_key.as_slice(), 2))?.to::<u64>();
558    let min_price: u32 =
559        sload_field(&mut input, map_slot(data_pricer_key.as_slice(), 3))?.to::<u64>() as u32;
560    let inertia: u32 =
561        sload_field(&mut input, map_slot(data_pricer_key.as_slice(), 4))?.to::<u64>() as u32;
562
563    let passed = (time.saturating_sub(last_update)) as u32;
564    let credit = bps.saturating_mul(passed);
565    let new_demand = demand
566        .saturating_sub(credit)
567        .saturating_add(info.asm_estimate);
568
569    input
570        .internals_mut()
571        .sstore(
572            ARBOS_STATE_ADDRESS,
573            map_slot(data_pricer_key.as_slice(), 0),
574            U256::from(new_demand),
575        )
576        .map_err(|_| PrecompileError::other("sstore failed"))?;
577    crate::charge_precompile_gas(SSTORE_GAS);
578    input
579        .internals_mut()
580        .sstore(
581            ARBOS_STATE_ADDRESS,
582            map_slot(data_pricer_key.as_slice(), 2),
583            U256::from(time),
584        )
585        .map_err(|_| PrecompileError::other("sstore failed"))?;
586    crate::charge_precompile_gas(SSTORE_GAS);
587
588    let exponent = if inertia > 0 {
589        10_000u64 * (new_demand as u64) / (inertia as u64)
590    } else {
591        0
592    };
593    let multiplier = approx_exp_basis_points(exponent);
594    let cost_per_byte = (min_price as u64).saturating_mul(multiplier) / 10_000;
595    let data_fee = U256::from(cost_per_byte.saturating_mul(info.asm_estimate as u64));
596
597    // Store program data
598    let estimate_kb = div_ceil(info.asm_estimate as u64, 1024).min(0xFF_FFFF) as u32;
599    let hours = hours_since_arbitrum(time);
600    let mut pd = [0u8; 32];
601    pd[0..2].copy_from_slice(&params_version.to_be_bytes());
602    pd[2..4].copy_from_slice(&info.init_gas.to_be_bytes());
603    pd[4..6].copy_from_slice(&info.cached_init_gas.to_be_bytes());
604    pd[6..8].copy_from_slice(&info.footprint.to_be_bytes());
605    pd[8] = (hours >> 16) as u8;
606    pd[9] = (hours >> 8) as u8;
607    pd[10] = hours as u8;
608    pd[11] = (estimate_kb >> 16) as u8;
609    pd[12] = (estimate_kb >> 8) as u8;
610    pd[13] = estimate_kb as u8;
611    pd[14] = was_cached as u8;
612
613    input
614        .internals_mut()
615        .sstore(ARBOS_STATE_ADDRESS, program_slot, U256::from_be_bytes(pd))
616        .map_err(|_| PrecompileError::other("sstore failed"))?;
617    crate::charge_precompile_gas(SSTORE_GAS);
618
619    // payActivationDataFee reads NetworkFeeAccount from storage (800 gas)
620    crate::charge_precompile_gas(SLOAD_GAS);
621
622    // Signal executor to handle the data fee payment
623    crate::set_stylus_activation_request(Some(program_address));
624    crate::set_stylus_activation_data_fee(data_fee);
625
626    let event_topic = IArbWasm::ProgramActivated::SIGNATURE_HASH;
627    let mut event_data = Vec::with_capacity(128);
628    event_data.extend_from_slice(&info.module_hash.0);
629    event_data.extend_from_slice(&[0u8; 12]);
630    event_data.extend_from_slice(program_address.as_slice());
631    event_data.extend_from_slice(&data_fee.to_be_bytes::<32>());
632    let mut ver = [0u8; 32];
633    ver[30..32].copy_from_slice(&params_version.to_be_bytes());
634    event_data.extend_from_slice(&ver);
635    // Event gas: LogGas(375) + (1 + indexed_count) * LogTopicGas(375) + LogDataGas(8) * data_bytes
636    let event_gas = 375 + 2 * 375 + 8 * event_data.len() as u64;
637    crate::charge_precompile_gas(event_gas);
638    crate::emit_log(ARBWASM_ADDRESS, &[event_topic, code_hash], &event_data);
639
640    // Return encoding gas: CopyGas * words(return_len)
641    let return_data = {
642        let mut output = Vec::with_capacity(64);
643        let mut ver_out = [0u8; 32];
644        ver_out[30..32].copy_from_slice(&params_version.to_be_bytes());
645        output.extend_from_slice(&ver_out);
646        output.extend_from_slice(&data_fee.to_be_bytes::<32>());
647        output
648    };
649    let return_gas = COPY_GAS * (return_data.len() as u64).div_ceil(32);
650    crate::charge_precompile_gas(return_gas);
651
652    let gas_used = crate::get_precompile_gas();
653    tracing::warn!(target: "stylus",
654        total = gas_used, args = args_cost, prover = prover_gas_used,
655        event = event_gas, ret = return_gas, "activateProgram total gas");
656    Ok(PrecompileOutput::new(gas_used, return_data.into()))
657}
658
659fn handle_codehash_keepalive(mut input: PrecompileInput<'_>, codehash: B256) -> PrecompileResult {
660    crate::reset_precompile_gas();
661    let args_cost = COPY_GAS * (input.data.len() as u64).saturating_sub(4).div_ceil(32);
662    crate::charge_precompile_gas(args_cost);
663
664    load_arbos(&mut input)?;
665    let params_word = load_params_word(&mut input)?;
666    let params_version = u16::from_be_bytes([params_word[0], params_word[1]]);
667    let keepalive_days = u16::from_be_bytes([params_word[21], params_word[22]]);
668    let expiry_days = u16::from_be_bytes([params_word[19], params_word[20]]);
669    let time = block_timestamp();
670
671    let programs_key = derive_subspace_key(ROOT_STORAGE_KEY, PROGRAMS_SUBSPACE);
672    let data_key = derive_subspace_key(programs_key.as_slice(), PROGRAMS_DATA_KEY);
673    let program_slot = map_slot_b256(data_key.as_slice(), &codehash);
674    let program_bytes = sload_field(&mut input, program_slot)?.to_be_bytes::<32>();
675    let program = parse_program(&program_bytes, &params_word);
676
677    if program.version == 0 {
678        return Err(PrecompileError::other("ProgramNotActivated()"));
679    }
680    let age = hours_to_age(
681        time,
682        program_bytes[8] as u32 * 65536 + program_bytes[9] as u32 * 256 + program_bytes[10] as u32,
683    );
684    if age > (expiry_days as u64) * 86400 {
685        return Err(PrecompileError::other("ProgramExpired()"));
686    }
687    if program.version != params_version {
688        return Err(PrecompileError::other("ProgramNeedsUpgrade()"));
689    }
690    if age < (keepalive_days as u64) * 86400 {
691        return Err(PrecompileError::other("ProgramKeepaliveTooSoon()"));
692    }
693
694    let asm_size = program.asm_estimate_kb * 1024;
695
696    // Update data pricer
697    let data_pricer_key = derive_subspace_key(programs_key.as_slice(), &[3]);
698    let demand: u32 =
699        sload_field(&mut input, map_slot(data_pricer_key.as_slice(), 0))?.to::<u64>() as u32;
700    let bps: u32 =
701        sload_field(&mut input, map_slot(data_pricer_key.as_slice(), 1))?.to::<u64>() as u32;
702    let last_update: u64 =
703        sload_field(&mut input, map_slot(data_pricer_key.as_slice(), 2))?.to::<u64>();
704    let min_price: u32 =
705        sload_field(&mut input, map_slot(data_pricer_key.as_slice(), 3))?.to::<u64>() as u32;
706    let inertia: u32 =
707        sload_field(&mut input, map_slot(data_pricer_key.as_slice(), 4))?.to::<u64>() as u32;
708
709    let passed = (time.saturating_sub(last_update)) as u32;
710    let credit = bps.saturating_mul(passed);
711    let new_demand = demand.saturating_sub(credit).saturating_add(asm_size);
712
713    input
714        .internals_mut()
715        .sstore(
716            ARBOS_STATE_ADDRESS,
717            map_slot(data_pricer_key.as_slice(), 0),
718            U256::from(new_demand),
719        )
720        .map_err(|_| PrecompileError::other("sstore failed"))?;
721    crate::charge_precompile_gas(SSTORE_GAS);
722    input
723        .internals_mut()
724        .sstore(
725            ARBOS_STATE_ADDRESS,
726            map_slot(data_pricer_key.as_slice(), 2),
727            U256::from(time),
728        )
729        .map_err(|_| PrecompileError::other("sstore failed"))?;
730    crate::charge_precompile_gas(SSTORE_GAS);
731
732    let exponent = if inertia > 0 {
733        10_000u64 * (new_demand as u64) / (inertia as u64)
734    } else {
735        0
736    };
737    let multiplier = approx_exp_basis_points(exponent);
738    let cost_per_byte = (min_price as u64).saturating_mul(multiplier) / 10_000;
739    let data_fee = U256::from(cost_per_byte.saturating_mul(asm_size as u64));
740
741    // Reset activatedAt
742    let hours = hours_since_arbitrum(time);
743    let mut pd = program_bytes;
744    pd[8] = (hours >> 16) as u8;
745    pd[9] = (hours >> 8) as u8;
746    pd[10] = hours as u8;
747
748    input
749        .internals_mut()
750        .sstore(ARBOS_STATE_ADDRESS, program_slot, U256::from_be_bytes(pd))
751        .map_err(|_| PrecompileError::other("sstore failed"))?;
752    crate::charge_precompile_gas(SSTORE_GAS);
753
754    // payActivationDataFee reads NetworkFeeAccount from storage (800 gas)
755    crate::charge_precompile_gas(SLOAD_GAS);
756
757    // Signal executor to handle the data fee payment
758    crate::set_stylus_keepalive_request(Some(codehash));
759    crate::set_stylus_activation_data_fee(data_fee);
760
761    let event_topic = IArbWasm::ProgramLifetimeExtended::SIGNATURE_HASH;
762    let mut event_data = Vec::with_capacity(32);
763    event_data.extend_from_slice(&data_fee.to_be_bytes::<32>());
764    let event_gas = 375 + 2 * 375 + 8 * event_data.len() as u64;
765    crate::charge_precompile_gas(event_gas);
766    crate::emit_log(ARBWASM_ADDRESS, &[event_topic, codehash], &event_data);
767
768    // No return value for keepalive
769    let gas_used = crate::get_precompile_gas();
770    Ok(PrecompileOutput::new(gas_used, Vec::new().into()))
771}