arb_storage/
state_ops.rs

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
11/// ArbOS state address — the fictional account that stores all ArbOS state.
12pub const ARBOS_STATE_ADDRESS: Address = address!("A4B05FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF");
13
14/// Filtered transactions state address — a separate account for tracking filtered tx hashes.
15pub const FILTERED_TX_STATE_ADDRESS: Address = address!("a4b0500000000000000000000000000000000001");
16
17/// Ensures the account exists in the cache. If the account doesn't exist
18/// (database returned None), creates it with default values (nonce=0, balance=0).
19fn 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
41/// Reads a storage slot from an arbitrary account, checking cache -> bundle -> database.
42pub 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
63/// Writes a storage slot to the ArbOS account using the transition mechanism.
64///
65/// This ensures changes survive merge_transitions() and are properly journaled.
66/// Skips no-op writes where value == current value.
67pub 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
75/// Writes a storage slot to an arbitrary account using the transition mechanism.
76pub 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    // Storage-only write: AccountInfo is unchanged, so one clone suffices.
115    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
166/// Reads the balance of an account from the state.
167pub 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
186/// Sets the nonce of an account, loading it into cache if needed.
187pub 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
228/// Sets the code of an account, loading it into cache if needed.
229pub 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    /// In-memory database that returns empty for everything.
282    #[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        // Verify value is in cache.
323        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        // Merge transitions into bundle.
335        state.merge_transitions(BundleRetention::Reverts);
336        let bundle = state.take_bundle();
337
338        // Verify value is in bundle.
339        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        // Writing 0 to a slot that doesn't exist (DB returns 0) should be a no-op.
359        write_storage_at(&mut state, ARBOS_STATE_ADDRESS, slot, U256::ZERO).expect("write zero");
360
361        // After merge, the slot should NOT be in the bundle.
362        state.merge_transitions(BundleRetention::Reverts);
363        let bundle = state.take_bundle();
364
365        // Account might or might not be in bundle, but the slot should not.
366        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        // Merge and check both survive.
386        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    /// Simulates the real block execution flow:
418    /// 1. StartBlock internal tx writes slot A (baseFee) via write_storage_at
419    /// 2. EVM commit for internal tx (empty state)
420    /// 3. EVM commit for user tx (modifies different accounts)
421    /// 4. Post-commit hook writes slot B (gasBacklog) via write_storage_at
422    /// 5. Merge transitions Both slots should survive in the bundle.
423    #[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        // Step 2: EVM commit for internal tx (empty state).
438        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        // Step 3: EVM commit for user tx (modifies a different account).
444        let sender = address!("1111111111111111111111111111111111111111");
445        let mut user_changes: alloy_primitives::map::HashMap<Address, revm_state::Account> =
446            Default::default();
447        // Load sender into cache first so commit doesn't panic.
448        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        // Step 5: Merge transitions.
465        state.merge_transitions(BundleRetention::Reverts);
466        let bundle = state.take_bundle();
467
468        // Both slots should be in the bundle.
469        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}