arb_storage/
state_ops.rs

1use alloy_primitives::{address, keccak256, Address, Bytes, U256};
2use revm::Database;
3use std::collections::HashMap;
4
5/// ArbOS state address — the fictional account that stores all ArbOS state.
6pub const ARBOS_STATE_ADDRESS: Address = address!("A4B05FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF");
7
8/// Filtered transactions state address — a separate account for tracking filtered tx hashes.
9pub const FILTERED_TX_STATE_ADDRESS: Address = address!("a4b0500000000000000000000000000000000001");
10
11/// Ensures the ArbOS account exists in bundle_state.
12///
13/// Uses database.basic() instead of state.basic() to avoid cache non-determinism.
14pub fn ensure_arbos_account_in_bundle<D: Database>(state: &mut revm::database::State<D>) {
15    ensure_account_in_bundle(state, ARBOS_STATE_ADDRESS);
16}
17
18/// Ensures an arbitrary account exists in bundle_state with nonce=1.
19pub fn ensure_account_in_bundle<D: Database>(state: &mut revm::database::State<D>, addr: Address) {
20    use revm_database::{AccountStatus, BundleAccount};
21    use revm_state::AccountInfo;
22
23    if state.bundle_state.state.contains_key(&addr) {
24        return;
25    }
26
27    let db_info = state.database.basic(addr).ok().flatten();
28
29    let info = db_info.or_else(|| {
30        Some(AccountInfo {
31            balance: U256::ZERO,
32            nonce: 1,
33            code_hash: keccak256([]),
34            code: None,
35            account_id: None,
36        })
37    });
38
39    let acc = BundleAccount {
40        info: info.clone(),
41        storage: HashMap::default(),
42        original_info: info,
43        status: AccountStatus::Loaded,
44    };
45    state.bundle_state.state.insert(addr, acc);
46}
47
48/// Ensures the account exists in the cache. If the account doesn't exist
49/// (database returned None), creates it with default values (nonce=0, balance=0).
50fn ensure_cache_account<D: Database>(state: &mut revm::database::State<D>, addr: Address) {
51    use revm_database::AccountStatus;
52
53    let _ = state.load_cache_account(addr);
54
55    if let Some(cached) = state.cache.accounts.get_mut(&addr) {
56        if cached.account.is_none() {
57            cached.account = Some(revm_database::PlainAccount {
58                info: revm_state::AccountInfo {
59                    balance: U256::ZERO,
60                    nonce: 0,
61                    code_hash: keccak256([]),
62                    code: None,
63                    account_id: None,
64                },
65                storage: Default::default(),
66            });
67            cached.status = AccountStatus::InMemoryChange;
68        }
69    }
70}
71
72/// Reads a storage slot from the ArbOS account, checking cache -> bundle -> database.
73pub fn read_arbos_storage<D: Database>(state: &mut revm::database::State<D>, slot: U256) -> U256 {
74    read_storage_at(state, ARBOS_STATE_ADDRESS, slot)
75}
76
77/// Reads a storage slot from an arbitrary account, checking cache -> bundle -> database.
78pub fn read_storage_at<D: Database>(
79    state: &mut revm::database::State<D>,
80    account: Address,
81    slot: U256,
82) -> U256 {
83    // Check cache first
84    if let Some(cached_acc) = state.cache.accounts.get(&account) {
85        if let Some(ref account) = cached_acc.account {
86            if let Some(&value) = account.storage.get(&slot) {
87                return value;
88            }
89        }
90    }
91
92    // Check bundle_state
93    if let Some(acc) = state.bundle_state.state.get(&account) {
94        if let Some(slot_entry) = acc.storage.get(&slot) {
95            return slot_entry.present_value;
96        }
97    }
98
99    // Fall back to database
100    state.database.storage(account, slot).unwrap_or(U256::ZERO)
101}
102
103/// Writes a storage slot to the ArbOS account using the transition mechanism.
104///
105/// This ensures changes survive merge_transitions() and are properly journaled.
106/// Skips no-op writes where value == current value.
107pub fn write_arbos_storage<D: Database>(
108    state: &mut revm::database::State<D>,
109    slot: U256,
110    value: U256,
111) {
112    write_storage_at(state, ARBOS_STATE_ADDRESS, slot, value);
113}
114
115/// Writes a storage slot to an arbitrary account using the transition mechanism.
116pub fn write_storage_at<D: Database>(
117    state: &mut revm::database::State<D>,
118    account: Address,
119    slot: U256,
120    value: U256,
121) {
122    use revm_database::states::StorageSlot;
123
124    // Ensure account exists in cache (creates it if database returns None).
125    ensure_cache_account(state, account);
126
127    // Get current value from cache/bundle, and original from DB
128    let current_value = {
129        state
130            .cache
131            .accounts
132            .get(&account)
133            .and_then(|ca| ca.account.as_ref())
134            .and_then(|a| a.storage.get(&slot).copied())
135    }
136    .or_else(|| {
137        state
138            .bundle_state
139            .state
140            .get(&account)
141            .and_then(|a| a.storage.get(&slot))
142            .map(|s| s.present_value)
143    });
144
145    let original_value = state.database.storage(account, slot).unwrap_or(U256::ZERO);
146
147    // Skip no-op writes
148    let prev_value = current_value.unwrap_or(original_value);
149
150    if value == prev_value {
151        return;
152    }
153
154    // Modify cache entry
155    let (previous_info, previous_status, current_info, current_status) = {
156        let cached_acc = match state.cache.accounts.get_mut(&account) {
157            Some(acc) => acc,
158            None => return,
159        };
160
161        let previous_status = cached_acc.status;
162        let previous_info = cached_acc.account.as_ref().map(|a| a.info.clone());
163
164        if let Some(ref mut account) = cached_acc.account {
165            account.storage.insert(slot, value);
166        }
167
168        let had_no_nonce_and_code = previous_info
169            .as_ref()
170            .map(|info| info.has_no_code_and_nonce())
171            .unwrap_or_default();
172        cached_acc.status = cached_acc.status.on_changed(had_no_nonce_and_code);
173
174        let current_info = cached_acc.account.as_ref().map(|a| a.info.clone());
175        let current_status = cached_acc.status;
176        (previous_info, previous_status, current_info, current_status)
177    };
178
179    // Create and apply transition
180    if account == ARBOS_STATE_ADDRESS {
181        tracing::debug!(
182            target: "arb::storage",
183            ?slot,
184            ?value,
185            ?prev_value,
186            ?original_value,
187            "write_storage_at applying transition"
188        );
189    }
190    let mut storage_changes: revm_database::StorageWithOriginalValues = HashMap::default();
191    storage_changes.insert(slot, StorageSlot::new_changed(original_value, value));
192
193    let transition = revm::database::TransitionAccount {
194        info: current_info,
195        status: current_status,
196        previous_info,
197        previous_status,
198        storage: storage_changes,
199        storage_was_destroyed: false,
200    };
201
202    state.apply_transition(vec![(account, transition)]);
203}
204
205/// Reads the balance of an account from the state.
206pub fn get_account_balance<D: Database>(
207    state: &mut revm::database::State<D>,
208    addr: Address,
209) -> U256 {
210    if let Some(cached_acc) = state.cache.accounts.get(&addr) {
211        if let Some(ref account) = cached_acc.account {
212            return account.info.balance;
213        }
214    }
215
216    state
217        .database
218        .basic(addr)
219        .ok()
220        .flatten()
221        .map(|info| info.balance)
222        .unwrap_or(U256::ZERO)
223}
224
225/// Sets the nonce of an account, loading it into cache if needed.
226pub fn set_account_nonce<D: Database>(
227    state: &mut revm::database::State<D>,
228    addr: Address,
229    nonce: u64,
230) {
231    ensure_cache_account(state, addr);
232
233    let (previous_info, previous_status, current_info, current_status) = {
234        let cached_acc = match state.cache.accounts.get_mut(&addr) {
235            Some(acc) => acc,
236            None => return,
237        };
238        let previous_status = cached_acc.status;
239        let previous_info = cached_acc.account.as_ref().map(|a| a.info.clone());
240
241        if let Some(ref mut account) = cached_acc.account {
242            account.info.nonce = nonce;
243        }
244
245        let had_no_nonce_and_code = previous_info
246            .as_ref()
247            .map(|info| info.has_no_code_and_nonce())
248            .unwrap_or_default();
249        cached_acc.status = cached_acc.status.on_changed(had_no_nonce_and_code);
250
251        let current_info = cached_acc.account.as_ref().map(|a| a.info.clone());
252        let current_status = cached_acc.status;
253        (previous_info, previous_status, current_info, current_status)
254    };
255
256    let transition = revm::database::TransitionAccount {
257        info: current_info,
258        status: current_status,
259        previous_info,
260        previous_status,
261        storage: HashMap::default(),
262        storage_was_destroyed: false,
263    };
264    state.apply_transition(vec![(addr, transition)]);
265}
266
267/// Sets the code of an account, loading it into cache if needed.
268pub fn set_account_code<D: Database>(
269    state: &mut revm::database::State<D>,
270    addr: Address,
271    code: Bytes,
272) {
273    use revm_state::Bytecode;
274
275    ensure_cache_account(state, addr);
276    let code_hash = keccak256(&code);
277    let bytecode = Bytecode::new_raw(code);
278
279    let (previous_info, previous_status, current_info, current_status) = {
280        let cached_acc = match state.cache.accounts.get_mut(&addr) {
281            Some(acc) => acc,
282            None => return,
283        };
284        let previous_status = cached_acc.status;
285        let previous_info = cached_acc.account.as_ref().map(|a| a.info.clone());
286
287        if let Some(ref mut account) = cached_acc.account {
288            account.info.code_hash = code_hash;
289            account.info.code = Some(bytecode);
290        }
291
292        let had_no_nonce_and_code = previous_info
293            .as_ref()
294            .map(|info| info.has_no_code_and_nonce())
295            .unwrap_or_default();
296        cached_acc.status = cached_acc.status.on_changed(had_no_nonce_and_code);
297
298        let current_info = cached_acc.account.as_ref().map(|a| a.info.clone());
299        let current_status = cached_acc.status;
300        (previous_info, previous_status, current_info, current_status)
301    };
302
303    let transition = revm::database::TransitionAccount {
304        info: current_info,
305        status: current_status,
306        previous_info,
307        previous_status,
308        storage: HashMap::default(),
309        storage_was_destroyed: false,
310    };
311    state.apply_transition(vec![(addr, transition)]);
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317    use revm_database::{states::bundle_state::BundleRetention, StateBuilder};
318
319    /// In-memory database that returns empty for everything.
320    #[derive(Default)]
321    struct EmptyDb;
322
323    impl Database for EmptyDb {
324        type Error = std::convert::Infallible;
325        fn basic(
326            &mut self,
327            _address: Address,
328        ) -> Result<Option<revm_state::AccountInfo>, Self::Error> {
329            Ok(None)
330        }
331        fn code_by_hash(
332            &mut self,
333            _code_hash: alloy_primitives::B256,
334        ) -> Result<revm_state::Bytecode, Self::Error> {
335            Ok(revm_state::Bytecode::default())
336        }
337        fn storage(&mut self, _address: Address, _index: U256) -> Result<U256, Self::Error> {
338            Ok(U256::ZERO)
339        }
340        fn block_hash(&mut self, _number: u64) -> Result<alloy_primitives::B256, Self::Error> {
341            Ok(alloy_primitives::B256::ZERO)
342        }
343    }
344
345    fn make_state() -> revm::database::State<EmptyDb> {
346        StateBuilder::new()
347            .with_database(EmptyDb)
348            .with_bundle_update()
349            .build()
350    }
351
352    #[test]
353    fn test_write_storage_at_creates_transition() {
354        let mut state = make_state();
355        let slot = U256::from(42);
356        let value = U256::from(12345);
357
358        write_storage_at(&mut state, ARBOS_STATE_ADDRESS, slot, value);
359
360        // Verify value is in cache.
361        let cached = state.cache.accounts.get(&ARBOS_STATE_ADDRESS).unwrap();
362        let stored = cached
363            .account
364            .as_ref()
365            .unwrap()
366            .storage
367            .get(&slot)
368            .copied()
369            .unwrap();
370        assert_eq!(stored, value, "Value should be in cache");
371
372        // Merge transitions into bundle.
373        state.merge_transitions(BundleRetention::Reverts);
374        let bundle = state.take_bundle();
375
376        // Verify value is in bundle.
377        let bundle_acct = bundle
378            .state
379            .get(&ARBOS_STATE_ADDRESS)
380            .expect("ArbOS account should be in bundle after merge");
381        let bundle_slot = bundle_acct
382            .storage
383            .get(&slot)
384            .expect("Slot should be in bundle storage");
385        assert_eq!(
386            bundle_slot.present_value, value,
387            "Bundle present_value should match"
388        );
389    }
390
391    #[test]
392    fn test_write_zero_value_is_noop_for_new_slot() {
393        let mut state = make_state();
394        let slot = U256::from(42);
395
396        // Writing 0 to a slot that doesn't exist (DB returns 0) should be a no-op.
397        write_storage_at(&mut state, ARBOS_STATE_ADDRESS, slot, U256::ZERO);
398
399        // After merge, the slot should NOT be in the bundle.
400        state.merge_transitions(BundleRetention::Reverts);
401        let bundle = state.take_bundle();
402
403        // Account might or might not be in bundle, but the slot should not.
404        if let Some(acct) = bundle.state.get(&ARBOS_STATE_ADDRESS) {
405            assert!(
406                !acct.storage.contains_key(&slot),
407                "Slot written with zero should not appear in bundle"
408            );
409        }
410    }
411
412    #[test]
413    fn test_write_survives_multiple_transitions() {
414        let mut state = make_state();
415        let slot_a = U256::from(10);
416        let slot_b = U256::from(20);
417
418        // First transition: write slot A (simulates baseFee write during StartBlock).
419        write_storage_at(&mut state, ARBOS_STATE_ADDRESS, slot_a, U256::from(100));
420
421        // Second transition: write slot B (simulates gasBacklog write during user tx).
422        write_storage_at(&mut state, ARBOS_STATE_ADDRESS, slot_b, U256::from(200));
423
424        // Merge and check both survive.
425        state.merge_transitions(BundleRetention::Reverts);
426        let bundle = state.take_bundle();
427
428        let acct = bundle
429            .state
430            .get(&ARBOS_STATE_ADDRESS)
431            .expect("ArbOS account should be in bundle");
432        assert_eq!(
433            acct.storage.get(&slot_a).unwrap().present_value,
434            U256::from(100),
435            "Slot A should survive merge"
436        );
437        assert_eq!(
438            acct.storage.get(&slot_b).unwrap().present_value,
439            U256::from(200),
440            "Slot B should survive merge"
441        );
442    }
443
444    #[test]
445    fn test_read_after_write_returns_written_value() {
446        let mut state = make_state();
447        let slot = U256::from(42);
448        let value = U256::from(99999);
449
450        write_storage_at(&mut state, ARBOS_STATE_ADDRESS, slot, value);
451
452        // Read should return the written value from cache.
453        let read_val = read_storage_at(&mut state, ARBOS_STATE_ADDRESS, slot);
454        assert_eq!(read_val, value, "Read should return written value");
455    }
456
457    /// Simulates the real block execution flow:
458    /// 1. StartBlock internal tx writes slot A (baseFee) via write_storage_at
459    /// 2. EVM commit for internal tx (empty state)
460    /// 3. EVM commit for user tx (modifies different accounts)
461    /// 4. Post-commit hook writes slot B (gasBacklog) via write_storage_at
462    /// 5. Merge transitions Both slots should survive in the bundle.
463    #[test]
464    fn test_write_survives_evm_commit_flow() {
465        let mut state = make_state();
466        let slot_basefee = U256::from(10);
467        let slot_backlog = U256::from(20);
468
469        // Step 1: StartBlock writes baseFee.
470        write_storage_at(
471            &mut state,
472            ARBOS_STATE_ADDRESS,
473            slot_basefee,
474            U256::from(100_000_000),
475        );
476
477        // Step 2: EVM commit for internal tx (empty state).
478        use revm_database::DatabaseCommit;
479        let empty_state: alloy_primitives::map::HashMap<Address, revm_state::Account> =
480            Default::default();
481        state.commit(empty_state);
482
483        // Step 3: EVM commit for user tx (modifies a different account).
484        let sender = address!("1111111111111111111111111111111111111111");
485        let mut user_changes: alloy_primitives::map::HashMap<Address, revm_state::Account> =
486            Default::default();
487        // Load sender into cache first so commit doesn't panic.
488        let _ = state.load_cache_account(sender);
489        let mut sender_acct = revm_state::Account::default();
490        sender_acct.info.balance = U256::from(1_000_000);
491        sender_acct.info.nonce = 1;
492        sender_acct.mark_touch();
493        user_changes.insert(sender, sender_acct);
494        state.commit(user_changes);
495
496        // Step 4: Post-commit hook writes gasBacklog.
497        write_storage_at(
498            &mut state,
499            ARBOS_STATE_ADDRESS,
500            slot_backlog,
501            U256::from(540_000),
502        );
503
504        // Step 5: Merge transitions.
505        state.merge_transitions(BundleRetention::Reverts);
506        let bundle = state.take_bundle();
507
508        // Both slots should be in the bundle.
509        let acct = bundle
510            .state
511            .get(&ARBOS_STATE_ADDRESS)
512            .expect("ArbOS account should be in bundle");
513        assert_eq!(
514            acct.storage.get(&slot_basefee).unwrap().present_value,
515            U256::from(100_000_000),
516            "baseFee slot should survive"
517        );
518        assert_eq!(
519            acct.storage.get(&slot_backlog).unwrap().present_value,
520            U256::from(540_000),
521            "gasBacklog slot should survive"
522        );
523    }
524}