arb_precompiles/
arbwasmcache.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, params::StylusParams};
9use revm::precompile::{PrecompileId, PrecompileOutput, PrecompileResult};
10
11use crate::{
12    ArbPrecompileError,
13    interfaces::{IArbWasm, IArbWasmCache},
14};
15
16/// ArbWasmCache precompile address (0x72).
17pub const ARBWASMCACHE_ADDRESS: Address = Address::new([
18    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
19    0x00, 0x00, 0x00, 0x72,
20]);
21
22const SLOAD_GAS: u64 = 800;
23const COPY_GAS: u64 = 3;
24
25const COLD_ACCOUNT_ACCESS_GAS: u64 = 2600;
26const SSTORE_SET_GAS: u64 = 20_000;
27const SSTORE_RESET_GAS: u64 = 5_000;
28
29/// LOG3 for UpdateProgramCache(address,bytes32,bool):
30/// base 375 + 3 topics * 375 + 32 bytes data * 8.
31const EMIT_UPDATE_PROGRAM_CACHE_GAS: u64 = 375 + 3 * 375 + 32 * 8;
32
33pub fn create_arbwasmcache_precompile(ctx: Arc<ArbPrecompileCtx>) -> DynPrecompile {
34    DynPrecompile::new_stateful(PrecompileId::custom("arbwasmcache"), move |input| {
35        handler(input, &ctx)
36    })
37}
38
39fn handler(mut input: PrecompileInput<'_>, ctx: &ArbPrecompileCtx) -> PrecompileResult {
40    if let Some(result) =
41        crate::check_precompile_version(ctx, arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS)
42    {
43        return result;
44    }
45
46    let mut gas_used = 0u64;
47    let gas_limit = input.gas;
48    crate::init_precompile_gas(&mut gas_used, ctx, input.data.len());
49
50    let call = match IArbWasmCache::ArbWasmCacheCalls::abi_decode(input.data) {
51        Ok(c) => c,
52        Err(_) => return crate::burn_all_revert(gas_limit),
53    };
54    if let Some(r) = crate::reject_nonpayable_value(input.value, input.data, gas_limit, &[]) {
55        return r;
56    }
57    if let Some(r) = crate::reject_static_write(
58        input.is_static,
59        input.data,
60        gas_limit,
61        &[
62            [0x4c, 0xea, 0xc8, 0x17],
63            [0xe7, 0x3a, 0xc9, 0xf2],
64            [0xce, 0x97, 0x20, 0x13],
65        ],
66    ) {
67        return r;
68    }
69    if let Some(r) = crate::reject_delegate_nonpure(
70        input.target_address != input.bytecode_address,
71        input.data,
72        gas_limit,
73        &[],
74    ) {
75        return r;
76    }
77
78    use IArbWasmCache::ArbWasmCacheCalls;
79    let result = match call {
80        ArbWasmCacheCalls::cacheCodehash(c) => {
81            handle_cache_codehash(&mut input, ctx, &mut gas_used, c.codehash)
82        }
83        ArbWasmCacheCalls::cacheProgram(c) => {
84            handle_cache_program(&mut input, ctx, &mut gas_used, c.addr)
85        }
86        ArbWasmCacheCalls::evictCodehash(c) => {
87            handle_evict_codehash(&mut input, ctx, &mut gas_used, c.codehash)
88        }
89        ArbWasmCacheCalls::isCacheManager(c) => {
90            handle_is_cache_manager(&mut input, &mut gas_used, c.manager, ctx)
91        }
92        ArbWasmCacheCalls::allCacheManagers(_) => {
93            handle_all_cache_managers(&mut input, &mut gas_used, ctx)
94        }
95        ArbWasmCacheCalls::codehashIsCached(c) => {
96            handle_codehash_is_cached(&mut input, &mut gas_used, ctx, c.codehash)
97        }
98    };
99    crate::gas_check(ctx, gas_limit, gas_used, result)
100}
101
102fn words_for_bytes(n: u64) -> u64 {
103    n.div_ceil(32)
104}
105
106// ── Helpers ──────────────────────────────────────────────────────────
107
108fn load_arbos(input: &mut PrecompileInput<'_>) -> Result<(), ArbPrecompileError> {
109    input
110        .internals_mut()
111        .load_account(ARBOS_STATE_ADDRESS)
112        .map_err(ArbPrecompileError::fatal)?;
113    Ok(())
114}
115
116fn handle_is_cache_manager(
117    input: &mut PrecompileInput<'_>,
118    gas_used: &mut u64,
119    addr: Address,
120    ctx: &ArbPrecompileCtx,
121) -> PrecompileResult {
122    let gas_limit = input.gas;
123    load_arbos(input)?;
124
125    let internals = input.internals_mut();
126    let arb_state = ctx
127        .block
128        .arbos_state(internals)
129        .map_err(ArbPrecompileError::fatal)?;
130    let is_member = arb_state
131        .programs
132        .cache_managers
133        .is_member(internals, addr)
134        .map_err(ArbPrecompileError::fatal)?;
135
136    let result = if is_member {
137        U256::from(1u64)
138    } else {
139        U256::ZERO
140    };
141    crate::charge_storage_read(gas_used, ctx, SLOAD_GAS);
142    crate::charge_computation(gas_used, ctx, COPY_GAS * words_for_bytes(32));
143    Ok(PrecompileOutput::new(
144        (*gas_used).min(gas_limit),
145        result.to_be_bytes::<32>().to_vec().into(),
146    ))
147}
148
149/// Return all cache manager addresses.
150fn handle_all_cache_managers(
151    input: &mut PrecompileInput<'_>,
152    gas_used: &mut u64,
153    ctx: &ArbPrecompileCtx,
154) -> PrecompileResult {
155    let gas_limit = input.gas;
156    load_arbos(input)?;
157
158    let internals = input.internals_mut();
159    let arb_state = ctx
160        .block
161        .arbos_state(internals)
162        .map_err(ArbPrecompileError::fatal)?;
163    let members = arb_state
164        .programs
165        .cache_managers
166        .all_members(internals, 256)
167        .map_err(ArbPrecompileError::fatal)?;
168    let count = members.len() as u64;
169    let sloads: u64 = 1 + count;
170
171    let mut out = Vec::with_capacity(64 + members.len() * 32);
172    out.extend_from_slice(&U256::from(32u64).to_be_bytes::<32>());
173    out.extend_from_slice(&U256::from(count).to_be_bytes::<32>());
174    for member in &members {
175        let mut word = [0u8; 32];
176        word[12..32].copy_from_slice(member.as_slice());
177        out.extend_from_slice(&word);
178    }
179
180    crate::charge_storage_read(gas_used, ctx, sloads * SLOAD_GAS);
181    crate::charge_computation(gas_used, ctx, COPY_GAS * words_for_bytes(out.len() as u64));
182    Ok(PrecompileOutput::new(
183        (*gas_used).min(gas_limit),
184        out.into(),
185    ))
186}
187
188fn handle_codehash_is_cached(
189    input: &mut PrecompileInput<'_>,
190    gas_used: &mut u64,
191    ctx: &ArbPrecompileCtx,
192    codehash: B256,
193) -> PrecompileResult {
194    let gas_limit = input.gas;
195    load_arbos(input)?;
196
197    let time = ctx.block.block_timestamp;
198    let internals = input.internals_mut();
199    let arb_state = ctx
200        .block
201        .arbos_state(internals)
202        .map_err(ArbPrecompileError::fatal)?;
203    let program = arb_state
204        .programs
205        .get_program(internals, codehash, time)
206        .map_err(ArbPrecompileError::fatal)?;
207
208    let result = if program.cached {
209        U256::from(1u64)
210    } else {
211        U256::ZERO
212    };
213    crate::charge_storage_read(gas_used, ctx, SLOAD_GAS);
214    crate::charge_computation(gas_used, ctx, COPY_GAS * words_for_bytes(32));
215    Ok(PrecompileOutput::new(
216        (*gas_used).min(gas_limit),
217        result.to_be_bytes::<32>().to_vec().into(),
218    ))
219}
220
221/// Caller must be a cache manager OR chain owner. Returns `(has_access, gas)`:
222/// `gas` is 1 SLOAD if the caller is a cache manager (short-circuit), else
223/// 2 SLOADs (cache-managers probe then chain-owners probe).
224fn caller_has_cache_access(
225    input: &mut PrecompileInput<'_>,
226    caller: Address,
227    ctx: &ArbPrecompileCtx,
228) -> Result<(bool, u64), ArbPrecompileError> {
229    let internals = input.internals_mut();
230    let arb_state = ctx
231        .block
232        .arbos_state(internals)
233        .map_err(ArbPrecompileError::fatal)?;
234    if arb_state
235        .programs
236        .cache_managers
237        .is_member(internals, caller)
238        .map_err(ArbPrecompileError::fatal)?
239    {
240        return Ok((true, SLOAD_GAS));
241    }
242    let is_owner = arb_state
243        .chain_owners
244        .is_member(internals, caller)
245        .map_err(ArbPrecompileError::fatal)?;
246    Ok((is_owner, 2 * SLOAD_GAS))
247}
248
249fn read_params_and_program(
250    input: &mut PrecompileInput<'_>,
251    codehash: B256,
252    time: u64,
253    ctx: &ArbPrecompileCtx,
254) -> Result<(StylusParams, Program), ArbPrecompileError> {
255    let internals = input.internals_mut();
256    let arb_state = ctx
257        .block
258        .arbos_state(internals)
259        .map_err(ArbPrecompileError::fatal)?;
260    let params = arb_state
261        .programs
262        .params(internals)
263        .map_err(ArbPrecompileError::fatal)?;
264    let program = arb_state
265        .programs
266        .get_program(internals, codehash, time)
267        .map_err(ArbPrecompileError::fatal)?;
268    Ok((params, program))
269}
270
271/// `pre_set_gas` lets the caller include an extra cold-account-access charge
272/// that must be paid on every exit path (e.g., `cacheProgram`'s GetCodeHash).
273fn set_program_cached(
274    input: &mut PrecompileInput<'_>,
275    ctx: &ArbPrecompileCtx,
276    gas_used: &mut u64,
277    codehash: B256,
278    cache: bool,
279    pre_set_gas: u64,
280) -> PrecompileResult {
281    let caller = input.caller;
282    let now = ctx.block.block_timestamp;
283    let gas_limit = input.gas;
284
285    // `pre_set_gas` is `ColdAccountAccessCostEIP2929` (an account read) for
286    // `cacheProgram`'s GetCodeHash, and zero otherwise — both are read-class.
287    crate::charge_storage_read(gas_used, ctx, pre_set_gas);
288
289    load_arbos(input)?;
290
291    let (has_access, access_gas) = caller_has_cache_access(input, caller, ctx)?;
292    crate::charge_storage_read(gas_used, ctx, access_gas);
293    if !has_access {
294        return crate::burn_all_revert(gas_limit);
295    }
296
297    let (params, mut program) = read_params_and_program(input, codehash, now, ctx)?;
298    // params is a warm read billed to computation; get_program is one SLOAD.
299    crate::charge_params_read(gas_used, ctx);
300    crate::charge_storage_read(gas_used, ctx, SLOAD_GAS);
301    let already_cached = program.cached;
302    let expiry_seconds = (params.expiry_days as u64).saturating_mul(86_400);
303    let expired = program.age_seconds > expiry_seconds;
304
305    if cache && program.version != params.version {
306        let data = IArbWasm::ProgramNeedsUpgrade {
307            version: program.version,
308            stylusVersion: params.version,
309        }
310        .abi_encode();
311        return crate::sol_error_revert(gas_used, ctx, data, gas_limit);
312    }
313    if cache && expired {
314        let data = IArbWasm::ProgramExpired {
315            ageInSeconds: program.age_seconds,
316        }
317        .abi_encode();
318        return crate::sol_error_revert(gas_used, ctx, data, gas_limit);
319    }
320    if already_cached == cache {
321        // The cache state is unchanged; return without any further read.
322        return Ok(PrecompileOutput::new(
323            (*gas_used).min(gas_limit),
324            Vec::new().into(),
325        ));
326    }
327
328    program.cached = cache;
329    let prog_init_cost = program.init_cost;
330    {
331        let internals = input.internals_mut();
332        let arb_state = ctx
333            .block
334            .arbos_state(internals)
335            .map_err(ArbPrecompileError::fatal)?;
336        arb_state
337            .programs
338            .set_program(internals, codehash, program)
339            .map_err(ArbPrecompileError::fatal)?;
340    }
341    let stored = program.to_storage();
342    let stored_u = U256::from_be_bytes(stored.0);
343    let sstore_gas = if stored_u == U256::ZERO {
344        SSTORE_RESET_GAS
345    } else {
346        SSTORE_SET_GAS
347    };
348
349    let topic1 = address_to_b256(caller);
350    let event_data = U256::from(cache as u64).to_be_bytes::<32>().to_vec();
351    input.internals_mut().log(Log::new_unchecked(
352        ARBWASMCACHE_ADDRESS,
353        vec![
354            IArbWasmCache::UpdateProgramCache::SIGNATURE_HASH,
355            topic1,
356            codehash,
357        ],
358        event_data.into(),
359    ));
360
361    // Re-caching reads the previous module hash (one SLOAD), then charges the
362    // init cost, the cache-update event, and the program write.
363    crate::charge_storage_read(gas_used, ctx, SLOAD_GAS);
364    crate::charge_storage_read(gas_used, ctx, prog_init_cost as u64);
365    crate::charge_history_growth(gas_used, ctx, EMIT_UPDATE_PROGRAM_CACHE_GAS);
366    crate::charge_storage_write(gas_used, ctx, sstore_gas);
367    Ok(PrecompileOutput::new(
368        (*gas_used).min(gas_limit),
369        Vec::new().into(),
370    ))
371}
372
373fn address_to_b256(addr: Address) -> B256 {
374    let mut bytes = [0u8; 32];
375    bytes[12..32].copy_from_slice(addr.as_slice());
376    B256::from(bytes)
377}
378
379fn handle_cache_codehash(
380    input: &mut PrecompileInput<'_>,
381    ctx: &ArbPrecompileCtx,
382    gas_used: &mut u64,
383    codehash: B256,
384) -> PrecompileResult {
385    if let Some(r) = crate::check_method_version(
386        ctx,
387        input.gas,
388        arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS,
389        arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS,
390    ) {
391        return r;
392    }
393    set_program_cached(input, ctx, gas_used, codehash, true, 0)
394}
395
396/// `cacheProgram` reads the code hash from an account, which costs
397/// `ColdAccountAccessCostEIP2929` even when the slot is already warm.
398fn handle_cache_program(
399    input: &mut PrecompileInput<'_>,
400    ctx: &ArbPrecompileCtx,
401    gas_used: &mut u64,
402    addr: Address,
403) -> PrecompileResult {
404    if let Some(r) = crate::check_method_version(
405        ctx,
406        input.gas,
407        arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS_FIXES,
408        0,
409    ) {
410        return r;
411    }
412    let codehash = crate::without_access_list_effect(input.internals_mut(), |internals| {
413        internals
414            .load_account(addr)
415            .map(|acct| acct.data.info.code_hash)
416            .map_err(ArbPrecompileError::fatal)
417    })?;
418    set_program_cached(
419        input,
420        ctx,
421        gas_used,
422        codehash,
423        true,
424        COLD_ACCOUNT_ACCESS_GAS,
425    )
426}
427
428fn handle_evict_codehash(
429    input: &mut PrecompileInput<'_>,
430    ctx: &ArbPrecompileCtx,
431    gas_used: &mut u64,
432    codehash: B256,
433) -> PrecompileResult {
434    set_program_cached(input, ctx, gas_used, codehash, false, 0)
435}