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
22pub 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
36const EMIT_UPDATE_PROGRAM_CACHE_GAS: u64 = 375 + 3 * 375 + 32 * 8;
39
40const 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
78fn 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
97fn 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
127fn 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 let count = size.min(256);
138
139 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 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 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
202fn 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
220fn 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
242fn 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 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
342fn 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}