arb_precompiles/
arbwasmcache.rs

1use alloy_evm::precompiles::{DynPrecompile, PrecompileInput};
2use alloy_primitives::{Address, Log, B256, U256};
3use alloy_sol_types::{SolError, SolEvent, SolInterface};
4use revm::precompile::{PrecompileError, PrecompileId, PrecompileOutput, PrecompileResult};
5
6use crate::{
7    interfaces::{IArbWasm, IArbWasmCache},
8    storage_slot::{
9        derive_subspace_key, map_slot, map_slot_b256, ARBOS_STATE_ADDRESS, CACHE_MANAGERS_KEY,
10        CHAIN_OWNER_SUBSPACE, PROGRAMS_DATA_KEY, PROGRAMS_PARAMS_KEY, PROGRAMS_SUBSPACE,
11        ROOT_STORAGE_KEY,
12    },
13};
14
15const ARBITRUM_START_TIME: u64 = 1_421_388_000;
16
17fn hours_to_age(time: u64, hours_since_start: u32) -> u64 {
18    let activated_at = ARBITRUM_START_TIME.saturating_add((hours_since_start as u64) * 3600);
19    time.saturating_sub(activated_at)
20}
21
22/// ArbWasmCache precompile address (0x72).
23pub const ARBWASMCACHE_ADDRESS: Address = Address::new([
24    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
25    0x00, 0x00, 0x00, 0x72,
26]);
27
28const SLOAD_GAS: u64 = 800;
29const COPY_GAS: u64 = 3;
30
31const WARM_SLOAD_GAS: u64 = 100;
32const COLD_ACCOUNT_ACCESS_GAS: u64 = 2600;
33const SSTORE_SET_GAS: u64 = 20_000;
34const SSTORE_RESET_GAS: u64 = 5_000;
35
36/// LOG3 for UpdateProgramCache(address,bytes32,bool):
37/// base 375 + 3 topics * 375 + 32 bytes data * 8.
38const EMIT_UPDATE_PROGRAM_CACHE_GAS: u64 = 375 + 3 * 375 + 32 * 8;
39
40/// AddressSet by_address sub-key.
41const BY_ADDRESS_KEY: &[u8] = &[0];
42
43pub fn create_arbwasmcache_precompile() -> DynPrecompile {
44    DynPrecompile::new_stateful(PrecompileId::custom("arbwasmcache"), handler)
45}
46
47fn handler(mut input: PrecompileInput<'_>) -> PrecompileResult {
48    if let Some(result) =
49        crate::check_precompile_version(arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS)
50    {
51        return result;
52    }
53
54    let gas_limit = input.gas;
55    crate::init_precompile_gas(input.data.len());
56
57    let call = match IArbWasmCache::ArbWasmCacheCalls::abi_decode(input.data) {
58        Ok(c) => c,
59        Err(_) => return crate::burn_all_revert(gas_limit),
60    };
61
62    use IArbWasmCache::ArbWasmCacheCalls;
63    let result = match call {
64        ArbWasmCacheCalls::cacheCodehash(c) => handle_cache_codehash(&mut input, c.codehash),
65        ArbWasmCacheCalls::cacheProgram(c) => handle_cache_program(&mut input, c.addr),
66        ArbWasmCacheCalls::evictCodehash(c) => handle_evict_codehash(&mut input, c.codehash),
67        ArbWasmCacheCalls::isCacheManager(c) => handle_is_cache_manager(&mut input, c.manager),
68        ArbWasmCacheCalls::allCacheManagers(_) => handle_all_cache_managers(&mut input),
69        ArbWasmCacheCalls::codehashIsCached(c) => handle_codehash_is_cached(&mut input, c.codehash),
70    };
71    crate::gas_check(gas_limit, result)
72}
73
74fn words_for_bytes(n: u64) -> u64 {
75    n.div_ceil(32)
76}
77
78// ── Helpers ──────────────────────────────────────────────────────────
79
80fn load_arbos(input: &mut PrecompileInput<'_>) -> Result<(), PrecompileError> {
81    input
82        .internals_mut()
83        .load_account(ARBOS_STATE_ADDRESS)
84        .map_err(|e| PrecompileError::other(format!("load_account: {e:?}")))?;
85    Ok(())
86}
87
88fn sload_field(input: &mut PrecompileInput<'_>, slot: U256) -> Result<U256, PrecompileError> {
89    let val = input
90        .internals_mut()
91        .sload(ARBOS_STATE_ADDRESS, slot)
92        .map_err(|_| PrecompileError::other("sload failed"))?;
93    crate::charge_precompile_gas(SLOAD_GAS);
94    Ok(val.data)
95}
96
97/// Compute the cache managers AddressSet storage key.
98fn cache_managers_key() -> B256 {
99    let programs_key = derive_subspace_key(ROOT_STORAGE_KEY, PROGRAMS_SUBSPACE);
100    derive_subspace_key(programs_key.as_slice(), CACHE_MANAGERS_KEY)
101}
102
103fn handle_is_cache_manager(input: &mut PrecompileInput<'_>, addr: Address) -> PrecompileResult {
104    let data_len = input.data.len();
105    load_arbos(input)?;
106
107    let cm_key = cache_managers_key();
108    let by_addr_key = derive_subspace_key(cm_key.as_slice(), BY_ADDRESS_KEY);
109    let addr_hash = address_to_b256(addr);
110    let slot = map_slot_b256(by_addr_key.as_slice(), &addr_hash);
111    let value = sload_field(input, slot)?;
112
113    let is_member = value != U256::ZERO;
114    let result = if is_member {
115        U256::from(1u64)
116    } else {
117        U256::ZERO
118    };
119    let args_cost = COPY_GAS * words_for_bytes(data_len.saturating_sub(4) as u64);
120    let result_cost = COPY_GAS * words_for_bytes(32);
121    Ok(PrecompileOutput::new(
122        SLOAD_GAS + SLOAD_GAS + args_cost + result_cost,
123        result.to_be_bytes::<32>().to_vec().into(),
124    ))
125}
126
127/// Return all cache manager addresses.
128fn handle_all_cache_managers(input: &mut PrecompileInput<'_>) -> PrecompileResult {
129    load_arbos(input)?;
130
131    let cm_key = cache_managers_key();
132    let size_slot = map_slot(cm_key.as_slice(), 0);
133    let size = sload_field(input, size_slot)?.saturating_to::<u64>();
134    let mut sloads: u64 = 1;
135
136    // Cap to prevent excessive reads.
137    let count = size.min(256);
138
139    // ABI: offset to dynamic array, then length, then elements.
140    let mut out = Vec::with_capacity(64 + count as usize * 32);
141    out.extend_from_slice(&U256::from(32u64).to_be_bytes::<32>());
142    out.extend_from_slice(&U256::from(count).to_be_bytes::<32>());
143
144    for i in 1..=count {
145        let addr_slot = map_slot(cm_key.as_slice(), i);
146        let addr_value = sload_field(input, addr_slot)?;
147        out.extend_from_slice(&addr_value.to_be_bytes::<32>());
148        sloads += 1;
149    }
150
151    // Gas: OpenArbosState(800) + sloads * SLOAD(800) + argsCost + resultCost
152    let args_cost = COPY_GAS * words_for_bytes(input.data.len().saturating_sub(4) as u64);
153    let result_cost = COPY_GAS * words_for_bytes(out.len() as u64);
154    let total = SLOAD_GAS + sloads * SLOAD_GAS + args_cost + result_cost;
155    Ok(PrecompileOutput::new(total.min(input.gas), out.into()))
156}
157
158fn handle_codehash_is_cached(input: &mut PrecompileInput<'_>, codehash: B256) -> PrecompileResult {
159    let data_len = input.data.len();
160    load_arbos(input)?;
161
162    let programs_key = derive_subspace_key(ROOT_STORAGE_KEY, PROGRAMS_SUBSPACE);
163    let data_key = derive_subspace_key(programs_key.as_slice(), PROGRAMS_DATA_KEY);
164    let program_slot = map_slot_b256(data_key.as_slice(), &codehash);
165    let program_word = sload_field(input, program_slot)?;
166
167    // Byte 14 of the program word is the cached flag.
168    let word_bytes = program_word.to_be_bytes::<32>();
169    let is_cached = word_bytes[14] != 0;
170
171    let result = if is_cached {
172        U256::from(1u64)
173    } else {
174        U256::ZERO
175    };
176    let args_cost = COPY_GAS * words_for_bytes(data_len.saturating_sub(4) as u64);
177    let result_cost = COPY_GAS * words_for_bytes(32);
178    Ok(PrecompileOutput::new(
179        SLOAD_GAS + SLOAD_GAS + args_cost + result_cost,
180        result.to_be_bytes::<32>().to_vec().into(),
181    ))
182}
183
184fn address_to_b256(addr: Address) -> B256 {
185    let mut bytes = [0u8; 32];
186    bytes[12..32].copy_from_slice(addr.as_slice());
187    B256::from(bytes)
188}
189
190fn sstore_field(
191    input: &mut PrecompileInput<'_>,
192    slot: U256,
193    value: U256,
194) -> Result<(), PrecompileError> {
195    input
196        .internals_mut()
197        .sstore(ARBOS_STATE_ADDRESS, slot, value)
198        .map_err(|_| PrecompileError::other("sstore failed"))?;
199    Ok(())
200}
201
202/// Read `version` (bytes 0-1) and `expiry_days` (bytes 19-20) from slot 0
203/// of the Programs.Params storage word.
204fn read_program_params(input: &mut PrecompileInput<'_>) -> Result<(u16, u16), PrecompileError> {
205    let programs_key = derive_subspace_key(ROOT_STORAGE_KEY, PROGRAMS_SUBSPACE);
206    let params_key = derive_subspace_key(programs_key.as_slice(), PROGRAMS_PARAMS_KEY);
207    let slot = map_slot(params_key.as_slice(), 0);
208    let word = sload_field(input, slot)?.to_be_bytes::<32>();
209    let version = u16::from_be_bytes([word[0], word[1]]);
210    let expiry_days = u16::from_be_bytes([word[19], word[20]]);
211    Ok((version, expiry_days))
212}
213
214fn program_data_slot(codehash: B256) -> U256 {
215    let programs_key = derive_subspace_key(ROOT_STORAGE_KEY, PROGRAMS_SUBSPACE);
216    let data_key = derive_subspace_key(programs_key.as_slice(), PROGRAMS_DATA_KEY);
217    map_slot_b256(data_key.as_slice(), &codehash)
218}
219
220/// Caller must be a cache manager OR chain owner. Returns `(has_access, gas)`:
221/// `gas` is 1 SLOAD if the caller is a cache manager (short-circuit), else
222/// 2 SLOADs (cache-managers probe then chain-owners probe).
223fn caller_has_cache_access(
224    input: &mut PrecompileInput<'_>,
225    caller: Address,
226) -> Result<(bool, u64), PrecompileError> {
227    let cm_key = cache_managers_key();
228    let cm_by_addr = derive_subspace_key(cm_key.as_slice(), BY_ADDRESS_KEY);
229    let addr_hash = address_to_b256(caller);
230    let cm_slot = map_slot_b256(cm_by_addr.as_slice(), &addr_hash);
231    if sload_field(input, cm_slot)? != U256::ZERO {
232        return Ok((true, SLOAD_GAS));
233    }
234
235    let owner_key = derive_subspace_key(ROOT_STORAGE_KEY, CHAIN_OWNER_SUBSPACE);
236    let owner_by_addr = derive_subspace_key(owner_key.as_slice(), BY_ADDRESS_KEY);
237    let owner_slot = map_slot_b256(owner_by_addr.as_slice(), &addr_hash);
238    let is_owner = sload_field(input, owner_slot)? != U256::ZERO;
239    Ok((is_owner, 2 * SLOAD_GAS))
240}
241
242/// `pre_set_gas` lets the caller include an extra charge that must be paid on
243/// every exit path (e.g., the GetCodeHash access cost for `cacheProgram`).
244fn set_program_cached(
245    input: &mut PrecompileInput<'_>,
246    codehash: B256,
247    cache: bool,
248    pre_set_gas: u64,
249) -> PrecompileResult {
250    let data_len = input.data.len();
251    let caller = input.caller;
252    let now: u64 = input
253        .internals()
254        .block_timestamp()
255        .try_into()
256        .unwrap_or(0u64);
257
258    let args_cost = COPY_GAS * words_for_bytes(data_len.saturating_sub(4) as u64);
259    let boilerplate_gas = args_cost + SLOAD_GAS + pre_set_gas;
260
261    load_arbos(input)?;
262
263    let (has_access, access_gas) = caller_has_cache_access(input, caller)?;
264    if !has_access {
265        return crate::burn_all_revert(input.gas);
266    }
267
268    let (params_version, expiry_days) = read_program_params(input)?;
269
270    let prog_slot = program_data_slot(codehash);
271    let mut prog_word = sload_field(input, prog_slot)?.to_be_bytes::<32>();
272    let prog_version = u16::from_be_bytes([prog_word[0], prog_word[1]]);
273    let prog_init_cost = u16::from_be_bytes([prog_word[2], prog_word[3]]);
274    let activated_at_hours =
275        ((prog_word[8] as u32) << 16) | ((prog_word[9] as u32) << 8) | prog_word[10] as u32;
276    let age_seconds = hours_to_age(now, activated_at_hours);
277    let expiry_seconds = (expiry_days as u64).saturating_mul(86_400);
278    let expired = age_seconds > expiry_seconds;
279    let already_cached = prog_word[14] != 0;
280
281    // Matches the early-return point before any mutation.
282    let after_get_program_gas = boilerplate_gas + access_gas + WARM_SLOAD_GAS + SLOAD_GAS;
283
284    if cache && prog_version != params_version {
285        let data = IArbWasm::ProgramNeedsUpgrade {
286            version: prog_version,
287            stylusVersion: params_version,
288        }
289        .abi_encode();
290        return crate::sol_error_revert(data, input.gas);
291    }
292    if cache && expired {
293        let data = IArbWasm::ProgramExpired {
294            ageInSeconds: age_seconds,
295        }
296        .abi_encode();
297        return crate::sol_error_revert(data, input.gas);
298    }
299    if already_cached == cache {
300        return Ok(PrecompileOutput::new(
301            after_get_program_gas.min(input.gas),
302            Vec::new().into(),
303        ));
304    }
305
306    prog_word[14] = if cache { 1 } else { 0 };
307    let new_word = U256::from_be_bytes(prog_word);
308    sstore_field(input, prog_slot, new_word)?;
309    let sstore_gas = if new_word == U256::ZERO {
310        SSTORE_RESET_GAS
311    } else {
312        SSTORE_SET_GAS
313    };
314
315    let topic1 = address_to_b256(caller);
316    let event_data = U256::from(cache as u64).to_be_bytes::<32>().to_vec();
317    input.internals_mut().log(Log::new_unchecked(
318        ARBWASMCACHE_ADDRESS,
319        vec![
320            IArbWasmCache::UpdateProgramCache::SIGNATURE_HASH,
321            topic1,
322            codehash,
323        ],
324        event_data.into(),
325    ));
326
327    let gas_used = after_get_program_gas
328        + EMIT_UPDATE_PROGRAM_CACHE_GAS
329        + prog_init_cost as u64
330        + SLOAD_GAS
331        + sstore_gas;
332    Ok(PrecompileOutput::new(
333        gas_used.min(input.gas),
334        Vec::new().into(),
335    ))
336}
337
338fn handle_cache_codehash(input: &mut PrecompileInput<'_>, codehash: B256) -> PrecompileResult {
339    set_program_cached(input, codehash, true, 0)
340}
341
342/// `cacheProgram` reads the code hash from an account, which costs
343/// `ColdAccountAccessCostEIP2929` even when the slot is already warm.
344fn handle_cache_program(input: &mut PrecompileInput<'_>, addr: Address) -> PrecompileResult {
345    let codehash = {
346        let acct = input
347            .internals_mut()
348            .load_account(addr)
349            .map_err(|e| PrecompileError::other(format!("load_account: {e:?}")))?;
350        acct.data.info.code_hash
351    };
352    set_program_cached(input, codehash, true, COLD_ACCOUNT_ACCESS_GAS)
353}
354
355fn handle_evict_codehash(input: &mut PrecompileInput<'_>, codehash: B256) -> PrecompileResult {
356    set_program_cached(input, codehash, false, 0)
357}