1use std::collections::HashMap;
2
3use alloy_primitives::{Address, Bytes, U256, address, keccak256};
4use arb_storage_errors::{DatabaseError, DatabaseErrorInfo, StorageError};
5use revm::Database;
6
7fn db_read_error<E: core::fmt::Display>(err: E) -> StorageError {
8 DatabaseError::Read(DatabaseErrorInfo::new(err.to_string())).into()
9}
10
11pub const ARBOS_STATE_ADDRESS: Address = address!("A4B05FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF");
13
14pub const FILTERED_TX_STATE_ADDRESS: Address = address!("a4b0500000000000000000000000000000000001");
16
17fn ensure_cache_account<D: Database>(state: &mut revm::database::State<D>, addr: Address) {
20 use revm_database::AccountStatus;
21
22 let _ = state.load_cache_account(addr);
23
24 if let Some(cached) = state.cache.accounts.get_mut(&addr)
25 && cached.account.is_none()
26 {
27 cached.account = Some(revm_database::PlainAccount {
28 info: revm_state::AccountInfo {
29 balance: U256::ZERO,
30 nonce: 0,
31 code_hash: keccak256([]),
32 code: None,
33 account_id: None,
34 },
35 storage: Default::default(),
36 });
37 cached.status = AccountStatus::InMemoryChange;
38 }
39}
40
41pub fn read_storage_at<D: Database>(
43 state: &mut revm::database::State<D>,
44 account: Address,
45 slot: U256,
46) -> Result<U256, StorageError> {
47 if let Some(cached_acc) = state.cache.accounts.get(&account)
48 && let Some(ref account) = cached_acc.account
49 && let Some(&value) = account.storage.get(&slot)
50 {
51 return Ok(value);
52 }
53
54 if let Some(acc) = state.bundle_state.state.get(&account)
55 && let Some(slot_entry) = acc.storage.get(&slot)
56 {
57 return Ok(slot_entry.present_value);
58 }
59
60 state.database.storage(account, slot).map_err(db_read_error)
61}
62
63pub fn write_arbos_storage<D: Database>(
68 state: &mut revm::database::State<D>,
69 slot: U256,
70 value: U256,
71) -> Result<(), StorageError> {
72 write_storage_at(state, ARBOS_STATE_ADDRESS, slot, value)
73}
74
75pub fn write_storage_at<D: Database>(
77 state: &mut revm::database::State<D>,
78 account: Address,
79 slot: U256,
80 value: U256,
81) -> Result<(), StorageError> {
82 use revm_database::states::StorageSlot;
83
84 ensure_cache_account(state, account);
85
86 let current_value = {
87 state
88 .cache
89 .accounts
90 .get(&account)
91 .and_then(|ca| ca.account.as_ref())
92 .and_then(|a| a.storage.get(&slot).copied())
93 }
94 .or_else(|| {
95 state
96 .bundle_state
97 .state
98 .get(&account)
99 .and_then(|a| a.storage.get(&slot))
100 .map(|s| s.present_value)
101 });
102
103 let original_value = state
104 .database
105 .storage(account, slot)
106 .map_err(db_read_error)?;
107
108 let prev_value = current_value.unwrap_or(original_value);
109
110 if value == prev_value {
111 return Ok(());
112 }
113
114 let (info, previous_info, previous_status, current_status) = {
116 let cached_acc = match state.cache.accounts.get_mut(&account) {
117 Some(acc) => acc,
118 None => return Ok(()),
119 };
120
121 let previous_status = cached_acc.status;
122 let (info, previous_info, had_no_nonce_and_code) = match cached_acc.account.as_ref() {
123 Some(a) => {
124 let info = a.info.clone();
125 let had_no_nonce_and_code = info.has_no_code_and_nonce();
126 (Some(info.clone()), Some(info), had_no_nonce_and_code)
127 }
128 None => (None, None, false),
129 };
130
131 if let Some(ref mut account) = cached_acc.account {
132 account.storage.insert(slot, value);
133 }
134
135 cached_acc.status = cached_acc.status.on_changed(had_no_nonce_and_code);
136 let current_status = cached_acc.status;
137 (info, previous_info, previous_status, current_status)
138 };
139
140 if account == ARBOS_STATE_ADDRESS {
141 tracing::trace!(
142 target: "arb::storage",
143 ?slot,
144 ?value,
145 ?prev_value,
146 ?original_value,
147 "write_storage_at applying transition"
148 );
149 }
150 let mut storage_changes: revm_database::StorageWithOriginalValues = HashMap::default();
151 storage_changes.insert(slot, StorageSlot::new_changed(original_value, value));
152
153 let transition = revm::database::TransitionAccount {
154 info,
155 status: current_status,
156 previous_info,
157 previous_status,
158 storage: storage_changes,
159 storage_was_destroyed: false,
160 };
161
162 state.apply_transition([(account, transition)]);
163 Ok(())
164}
165
166pub fn get_account_balance<D: Database>(
168 state: &mut revm::database::State<D>,
169 addr: Address,
170) -> U256 {
171 if let Some(cached_acc) = state.cache.accounts.get(&addr)
172 && let Some(ref account) = cached_acc.account
173 {
174 return account.info.balance;
175 }
176
177 state
178 .database
179 .basic(addr)
180 .ok()
181 .flatten()
182 .map(|info| info.balance)
183 .unwrap_or(U256::ZERO)
184}
185
186pub fn set_account_nonce<D: Database>(
188 state: &mut revm::database::State<D>,
189 addr: Address,
190 nonce: u64,
191) {
192 ensure_cache_account(state, addr);
193
194 let (previous_info, previous_status, current_info, current_status) = {
195 let cached_acc = match state.cache.accounts.get_mut(&addr) {
196 Some(acc) => acc,
197 None => return,
198 };
199 let previous_status = cached_acc.status;
200 let previous_info = cached_acc.account.as_ref().map(|a| a.info.clone());
201
202 if let Some(ref mut account) = cached_acc.account {
203 account.info.nonce = nonce;
204 }
205
206 let had_no_nonce_and_code = previous_info
207 .as_ref()
208 .map(|info| info.has_no_code_and_nonce())
209 .unwrap_or_default();
210 cached_acc.status = cached_acc.status.on_changed(had_no_nonce_and_code);
211
212 let current_info = cached_acc.account.as_ref().map(|a| a.info.clone());
213 let current_status = cached_acc.status;
214 (previous_info, previous_status, current_info, current_status)
215 };
216
217 let transition = revm::database::TransitionAccount {
218 info: current_info,
219 status: current_status,
220 previous_info,
221 previous_status,
222 storage: HashMap::default(),
223 storage_was_destroyed: false,
224 };
225 state.apply_transition(vec![(addr, transition)]);
226}
227
228pub fn set_account_code<D: Database>(
230 state: &mut revm::database::State<D>,
231 addr: Address,
232 code: Bytes,
233) {
234 use revm_state::Bytecode;
235
236 ensure_cache_account(state, addr);
237 let code_hash = keccak256(&code);
238 let bytecode = Bytecode::new_raw(code);
239
240 let (previous_info, previous_status, current_info, current_status) = {
241 let cached_acc = match state.cache.accounts.get_mut(&addr) {
242 Some(acc) => acc,
243 None => return,
244 };
245 let previous_status = cached_acc.status;
246 let previous_info = cached_acc.account.as_ref().map(|a| a.info.clone());
247
248 if let Some(ref mut account) = cached_acc.account {
249 account.info.code_hash = code_hash;
250 account.info.code = Some(bytecode);
251 }
252
253 let had_no_nonce_and_code = previous_info
254 .as_ref()
255 .map(|info| info.has_no_code_and_nonce())
256 .unwrap_or_default();
257 cached_acc.status = cached_acc.status.on_changed(had_no_nonce_and_code);
258
259 let current_info = cached_acc.account.as_ref().map(|a| a.info.clone());
260 let current_status = cached_acc.status;
261 (previous_info, previous_status, current_info, current_status)
262 };
263
264 let transition = revm::database::TransitionAccount {
265 info: current_info,
266 status: current_status,
267 previous_info,
268 previous_status,
269 storage: HashMap::default(),
270 storage_was_destroyed: false,
271 };
272 state.apply_transition(vec![(addr, transition)]);
273}
274
275#[cfg(test)]
276mod tests {
277 use revm_database::{StateBuilder, states::bundle_state::BundleRetention};
278
279 use super::*;
280
281 #[derive(Default)]
283 struct EmptyDb;
284
285 impl Database for EmptyDb {
286 type Error = std::convert::Infallible;
287 fn basic(
288 &mut self,
289 _address: Address,
290 ) -> Result<Option<revm_state::AccountInfo>, Self::Error> {
291 Ok(None)
292 }
293 fn code_by_hash(
294 &mut self,
295 _code_hash: alloy_primitives::B256,
296 ) -> Result<revm_state::Bytecode, Self::Error> {
297 Ok(revm_state::Bytecode::default())
298 }
299 fn storage(&mut self, _address: Address, _index: U256) -> Result<U256, Self::Error> {
300 Ok(U256::ZERO)
301 }
302 fn block_hash(&mut self, _number: u64) -> Result<alloy_primitives::B256, Self::Error> {
303 Ok(alloy_primitives::B256::ZERO)
304 }
305 }
306
307 fn make_state() -> revm::database::State<EmptyDb> {
308 StateBuilder::new()
309 .with_database(EmptyDb)
310 .with_bundle_update()
311 .build()
312 }
313
314 #[test]
315 fn test_write_storage_at_creates_transition() {
316 let mut state = make_state();
317 let slot = U256::from(42);
318 let value = U256::from(12345);
319
320 write_storage_at(&mut state, ARBOS_STATE_ADDRESS, slot, value).unwrap();
321
322 let cached = state.cache.accounts.get(&ARBOS_STATE_ADDRESS).unwrap();
324 let stored = cached
325 .account
326 .as_ref()
327 .unwrap()
328 .storage
329 .get(&slot)
330 .copied()
331 .unwrap();
332 assert_eq!(stored, value, "Value should be in cache");
333
334 state.merge_transitions(BundleRetention::Reverts);
336 let bundle = state.take_bundle();
337
338 let bundle_acct = bundle
340 .state
341 .get(&ARBOS_STATE_ADDRESS)
342 .expect("ArbOS account should be in bundle after merge");
343 let bundle_slot = bundle_acct
344 .storage
345 .get(&slot)
346 .expect("Slot should be in bundle storage");
347 assert_eq!(
348 bundle_slot.present_value, value,
349 "Bundle present_value should match"
350 );
351 }
352
353 #[test]
354 fn test_write_zero_value_is_noop_for_new_slot() {
355 let mut state = make_state();
356 let slot = U256::from(42);
357
358 write_storage_at(&mut state, ARBOS_STATE_ADDRESS, slot, U256::ZERO).expect("write zero");
360
361 state.merge_transitions(BundleRetention::Reverts);
363 let bundle = state.take_bundle();
364
365 if let Some(acct) = bundle.state.get(&ARBOS_STATE_ADDRESS) {
367 assert!(
368 !acct.storage.contains_key(&slot),
369 "Slot written with zero should not appear in bundle"
370 );
371 }
372 }
373
374 #[test]
375 fn test_write_survives_multiple_transitions() {
376 let mut state = make_state();
377 let slot_a = U256::from(10);
378 let slot_b = U256::from(20);
379
380 write_storage_at(&mut state, ARBOS_STATE_ADDRESS, slot_a, U256::from(100))
381 .expect("write A");
382 write_storage_at(&mut state, ARBOS_STATE_ADDRESS, slot_b, U256::from(200))
383 .expect("write B");
384
385 state.merge_transitions(BundleRetention::Reverts);
387 let bundle = state.take_bundle();
388
389 let acct = bundle
390 .state
391 .get(&ARBOS_STATE_ADDRESS)
392 .expect("ArbOS account should be in bundle");
393 assert_eq!(
394 acct.storage.get(&slot_a).unwrap().present_value,
395 U256::from(100),
396 "Slot A should survive merge"
397 );
398 assert_eq!(
399 acct.storage.get(&slot_b).unwrap().present_value,
400 U256::from(200),
401 "Slot B should survive merge"
402 );
403 }
404
405 #[test]
406 fn test_read_after_write_returns_written_value() {
407 let mut state = make_state();
408 let slot = U256::from(42);
409 let value = U256::from(99999);
410
411 write_storage_at(&mut state, ARBOS_STATE_ADDRESS, slot, value).expect("write");
412
413 let read_val = read_storage_at(&mut state, ARBOS_STATE_ADDRESS, slot).expect("read");
414 assert_eq!(read_val, value, "Read should return written value");
415 }
416
417 #[test]
424 fn test_write_survives_evm_commit_flow() {
425 let mut state = make_state();
426 let slot_basefee = U256::from(10);
427 let slot_backlog = U256::from(20);
428
429 write_storage_at(
430 &mut state,
431 ARBOS_STATE_ADDRESS,
432 slot_basefee,
433 U256::from(100_000_000),
434 )
435 .expect("baseFee write");
436
437 use revm_database::DatabaseCommit;
439 let empty_state: alloy_primitives::map::HashMap<Address, revm_state::Account> =
440 Default::default();
441 state.commit(empty_state);
442
443 let sender = address!("1111111111111111111111111111111111111111");
445 let mut user_changes: alloy_primitives::map::HashMap<Address, revm_state::Account> =
446 Default::default();
447 let _ = state.load_cache_account(sender);
449 let mut sender_acct = revm_state::Account::default();
450 sender_acct.info.balance = U256::from(1_000_000);
451 sender_acct.info.nonce = 1;
452 sender_acct.mark_touch();
453 user_changes.insert(sender, sender_acct);
454 state.commit(user_changes);
455
456 write_storage_at(
457 &mut state,
458 ARBOS_STATE_ADDRESS,
459 slot_backlog,
460 U256::from(540_000),
461 )
462 .expect("gasBacklog write");
463
464 state.merge_transitions(BundleRetention::Reverts);
466 let bundle = state.take_bundle();
467
468 let acct = bundle
470 .state
471 .get(&ARBOS_STATE_ADDRESS)
472 .expect("ArbOS account should be in bundle");
473 assert_eq!(
474 acct.storage.get(&slot_basefee).unwrap().present_value,
475 U256::from(100_000_000),
476 "baseFee slot should survive"
477 );
478 assert_eq!(
479 acct.storage.get(&slot_backlog).unwrap().present_value,
480 U256::from(540_000),
481 "gasBacklog slot should survive"
482 );
483 }
484}