Skip to main content

zksync_vm2/
world_diff.rs

1use std::collections::BTreeMap;
2
3use primitive_types::{H160, U256};
4use zk_evm_abstractions::{aux::Timestamp, queries::LogQuery};
5use zkevm_opcode_defs::system_params::{
6    STORAGE_ACCESS_COLD_READ_COST, STORAGE_ACCESS_COLD_WRITE_COST, STORAGE_ACCESS_WARM_READ_COST,
7    STORAGE_ACCESS_WARM_WRITE_COST, STORAGE_AUX_BYTE,
8};
9use zksync_vm2_interface::{CycleStats, Event, HeapId, L2ToL1Log, Tracer};
10
11use crate::{
12    rollback::{Rollback, RollbackableLog, RollbackableMap, RollbackablePod, RollbackableSet},
13    StorageInterface, StorageSlot,
14};
15
16/// Merged value for `storage_writes`: pending written value + pubdata paid
17/// (formerly the separate `storage_changes` + `paid_changes` maps).
18#[derive(Debug, Clone, Copy, Default)]
19pub struct StorageWriteEntry {
20    /// Pending written value for the slot.
21    pub value: U256,
22    /// Pubdata cost paid for writing this slot (0 for free-storage slots).
23    pub paid: u32,
24}
25
26/// Per-slot access flags packed into `WorldDiff::slot_flags` (one byte per
27/// `(address, key)`), replacing three separate `(address, key)` sets.
28const SLOT_READ: u8 = 1;
29const SLOT_WRITTEN: u8 = 1 << 1;
30/// Set on a read at rollback-depth zero (`did_read_at_depth_zero` in
31/// `circuit_sequencer_api::sort_storage_access`). Downstream this forces a
32/// *protective read* into the deduplicated storage set — unless the slot is
33/// also written, in which case the write entry subsumes it.
34const SLOT_PROTECTIVE_READ: u8 = 1 << 2;
35
36/// Pending modifications to the global state that are executed at the end of a block.
37/// In other words, side effects.
38#[derive(Debug, Default)]
39pub struct WorldDiff {
40    // These are rolled back on revert or panic (and when the whole VM is rolled back).
41    /// Pending storage writes (value + pubdata paid), merged from the former
42    /// `storage_changes` + `paid_changes` to store the (address, key) once.
43    storage_writes: RollbackableMap<(H160, U256), StorageWriteEntry>,
44    transient_storage_changes: RollbackableMap<(H160, U256), U256>,
45    events: RollbackableLog<Event>,
46    l2_to_l1_logs: RollbackableLog<L2ToL1Log>,
47    pub(crate) pubdata: RollbackablePod<i32>,
48    storage_refunds: RollbackableLog<u32>,
49    pubdata_costs: RollbackableLog<i32>,
50    storage_logs: Vec<LogQuery>,
51    rollback_storage_logs: Vec<LogQuery>,
52    // The fields below are only rolled back when the whole VM is rolled back.
53    /// Tracks decommit visibility state for each bytecode hash.
54    ///
55    /// Besides successful decommits, we also retain far-call decommit attempts that failed with
56    /// out-of-gas in `pay_for_decommit()`. Legacy VM includes those hashes into "used contracts"
57    /// output, and shadow-mode compares that output (`CurrentExecutionState.used_contract_hashes`).
58    ///
59    /// This field is rolled back only by external VM snapshots.
60    pub(crate) decommitted_hashes: RollbackableMap<U256, DecommitState>,
61    /// Reverse index for `decommitted_hashes` entries that carry materialized heap pages.
62    ///
63    /// This is used to quickly check whether a heap page is globally pinned by decommitment reuse
64    /// semantics.
65    ///
66    /// This follows external snapshot / rollback semantics together with `decommitted_hashes`.
67    decommit_pinned_pages: RollbackableSet<u32>,
68    /// Per-slot access flags merged from three former `(address, key)` sets
69    /// (read / written / protective-read) so the 52-byte key is stored once
70    /// instead of three times. External-rollback semantics (whole-VM only),
71    /// matching the original sets. See the `SLOT_*` consts.
72    ///
73    /// `SLOT_PROTECTIVE_READ` keeps the dedup's `did_read_at_depth_zero`
74    /// predicate: set only by `read_storage_inner`, only in opt-out mode, and
75    /// only when `storage_writes` has no pending write for the slot at read time.
76    slot_flags: RollbackableMap<(H160, U256), u8>,
77
78    // This is never rolled back. It is just a cache to avoid asking these from DB every time.
79    storage_initial_values: BTreeMap<(H160, U256), StorageSlot>,
80
81    /// Selects two mutually exclusive bookkeeping modes (see
82    /// [`Self::set_record_storage_logs`] for the rationale); set once before
83    /// execution, never rolled back.
84    ///
85    /// - `false` (default): append the `storage_logs` / `rollback_storage_logs`
86    ///   trace; otherwise behave like the pre-optimization base — read-only slots
87    ///   are *not* cached in `storage_initial_values`.
88    /// - `true`: drop the trace; instead record the `SLOT_PROTECTIVE_READ` flag
89    ///   and the read-only `storage_initial_values` cache, the inputs a
90    ///   re-execution verifier derives the deduplicated set from.
91    skip_storage_logs: bool,
92}
93
94#[derive(Debug)]
95pub(crate) struct ExternalSnapshot {
96    internal_snapshot: Snapshot,
97    pub(crate) decommitted_hashes: <RollbackableMap<U256, DecommitState> as Rollback>::Snapshot,
98    decommit_pinned_pages: <RollbackableSet<u32> as Rollback>::Snapshot,
99    slot_flags: <RollbackableMap<(H160, U256), u8> as Rollback>::Snapshot,
100    storage_refunds: <RollbackableLog<u32> as Rollback>::Snapshot,
101    pubdata_costs: <RollbackableLog<i32> as Rollback>::Snapshot,
102}
103
104#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
105pub(crate) enum DecommitState {
106    /// A far-call decommit attempt ran out of gas before materialization.
107    ///
108    /// We preserve this state for legacy compatibility: old VM exposes these hashes as used
109    /// contracts. This state is observable via `decommitted_hashes()`, but it must not make
110    /// future decommits free.
111    ///
112    /// Note that `log.decommit` out-of-gas is not represented by this state because that opcode
113    /// exits before decommit bookkeeping.
114    #[default]
115    Unsuccessful,
116    /// A bytecode hash was successfully decommitted and has an assigned reusable heap page.
117    Succeeded(u32),
118}
119
120impl WorldDiff {
121    /// Controls whether the per-access storage log trace
122    /// (`storage_logs` / `rollback_storage_logs`) is accumulated.
123    ///
124    /// Recording is **on by default** — it is required by consumers that build
125    /// an in-circuit storage argument from `storage_log_queries()` (e.g. Boojum
126    /// witness generation via `sort_storage_access_queries`). A re-execution
127    /// verifier with no in-circuit storage argument (e.g. Airbender), which
128    /// derives the deduplicated storage set from the `SLOT_PROTECTIVE_READ`
129    /// flag + `storage_writes` instead, can pass `false` to avoid the trace's
130    /// memory cost (~270 MiB on large batches).
131    ///
132    /// # Panics
133    /// Panics if any storage slot has already been read or written — toggling
134    /// mid-execution would leave a partial trace or partial dedup state.
135    pub fn set_record_storage_logs(&mut self, record: bool) {
136        assert!(
137            self.storage_logs.is_empty()
138                && self.storage_writes.as_ref().is_empty()
139                && self.slot_flags.as_ref().is_empty()
140                && self.storage_initial_values.is_empty(),
141            "set_record_storage_logs must be called before any storage access"
142        );
143        self.skip_storage_logs = !record;
144    }
145
146    /// Reserve capacity for the auxiliary log Vecs (events, `pubdata_costs`,
147    /// `storage_refunds`). Each of these doubles during execution and the
148    /// transient peak is non-trivial inside the verifier guest.
149    pub fn reserve_auxiliary_log_capacity(
150        &mut self,
151        events: usize,
152        pubdata_costs: usize,
153        storage_refunds: usize,
154    ) {
155        self.events.reserve(events);
156        self.pubdata_costs.reserve(pubdata_costs);
157        self.storage_refunds.reserve(storage_refunds);
158    }
159
160    /// Set `flag` on a slot's access-flags entry, returning `true` iff the flag
161    /// was newly set (mirrors the former per-set `RollbackableSet::add`). The
162    /// 52-byte `(address, key)` is stored once across all three flags. Single
163    /// map traversal — see [`RollbackableMap::add_flags`].
164    fn slot_add_flag(&mut self, key: (H160, U256), flag: u8) -> bool {
165        self.slot_flags.add_flags(key, flag)
166    }
167
168    /// Returns the storage slot's value and a refund based on its hot/cold status.
169    pub(crate) fn read_storage(
170        &mut self,
171        world: &mut impl StorageInterface,
172        tracer: &mut impl Tracer,
173        contract: H160,
174        key: U256,
175        tx_number_in_block: u16,
176    ) -> (U256, u32) {
177        let (value, newly_added) =
178            self.read_storage_inner(world, tracer, contract, key, tx_number_in_block);
179        let refund = if !newly_added || world.is_free_storage_slot(&contract, &key) {
180            WARM_READ_REFUND
181        } else {
182            0
183        };
184        self.storage_refunds.push(refund);
185        (value, refund)
186    }
187
188    /// Same as [`Self::read_storage()`], but without recording the refund value (which is important
189    /// because the storage is read not only from the `sload` op handler, but also from the `farcall` op handler;
190    /// the latter must not record a refund as per previous VM versions).
191    pub(crate) fn read_storage_without_refund(
192        &mut self,
193        world: &mut impl StorageInterface,
194        tracer: &mut impl Tracer,
195        contract: H160,
196        key: U256,
197        tx_number_in_block: u16,
198    ) -> U256 {
199        self.read_storage_inner(world, tracer, contract, key, tx_number_in_block)
200            .0
201    }
202
203    fn read_storage_inner(
204        &mut self,
205        world: &mut impl StorageInterface,
206        tracer: &mut impl Tracer,
207        contract: H160,
208        key: U256,
209        tx_number_in_block: u16,
210    ) -> (U256, bool) {
211        let newly_added = self.slot_add_flag((contract, key), SLOT_READ);
212        if newly_added {
213            tracer.on_extra_prover_cycles(CycleStats::StorageRead);
214        }
215
216        self.pubdata_costs.push(0);
217        let value = if self.skip_storage_logs {
218            // Opt-out mode: no trace kept; record the dedup inputs instead.
219            let live_write = self
220                .storage_writes
221                .as_ref()
222                .get(&(contract, key))
223                .map(|e| e.value);
224            if let Some(value) = live_write {
225                value
226            } else {
227                // No pending write: cache the initial value (writes already
228                // cache it) and flag the depth-zero read.
229                let initial_value = self
230                    .storage_initial_values
231                    .entry((contract, key))
232                    .or_insert_with(|| world.read_storage(contract, key))
233                    .value;
234                self.slot_add_flag((contract, key), SLOT_PROTECTIVE_READ);
235                initial_value
236            }
237        } else {
238            // Recording mode: read like the pre-optimization base, without
239            // caching read-only slots (keeps Boojum's memory unchanged).
240            let value = self.just_read_storage(world, contract, key);
241            // Record the per-access trace; read_value == written_value for a read.
242            // Note: timestamp logic does not match `zk_evm`; we only ensure
243            // timestamps are unique, which is fine as the witness is not
244            // generated from these logs.
245            self.storage_logs.push(LogQuery {
246                timestamp: Timestamp(
247                    u32::try_from(self.storage_logs.len()).expect("Too many storage logs"),
248                ),
249                tx_number_in_block,
250                aux_byte: STORAGE_AUX_BYTE,
251                shard_id: 0,
252                address: contract,
253                key,
254                read_value: value,
255                written_value: value,
256                rw_flag: false,
257                rollback: false,
258                is_service: false,
259            });
260            value
261        };
262        (value, newly_added)
263    }
264
265    /// Reads the value of a storage slot without any extra bookkeeping.
266    /// Should only be used for tracers.
267    pub(crate) fn just_read_storage(
268        &self,
269        world: &mut impl StorageInterface,
270        contract: H160,
271        key: U256,
272    ) -> U256 {
273        self.storage_writes
274            .as_ref()
275            .get(&(contract, key))
276            .map_or_else(|| world.read_storage_value(contract, key), |e| e.value)
277    }
278
279    /// Returns the refund based the hot/cold status of the storage slot and the change in pubdata.
280    pub(crate) fn write_storage(
281        &mut self,
282        world: &mut impl StorageInterface,
283        tracer: &mut impl Tracer,
284        contract: H160,
285        key: U256,
286        value: U256,
287        tx_number_in_block: u16,
288    ) -> u32 {
289        if !self.skip_storage_logs {
290            // Boojum mode: record the write and its rollback twin before the
291            // change lands, so `read_value` is the pre-write value (matching
292            // the legacy trace shape).
293            let read_value = self.just_read_storage(world, contract, key);
294            let log_query = LogQuery {
295                timestamp: Timestamp(u32::try_from(self.storage_logs.len()).unwrap_or(u32::MAX)),
296                tx_number_in_block,
297                aux_byte: STORAGE_AUX_BYTE,
298                shard_id: 0,
299                address: contract,
300                key,
301                read_value,
302                written_value: value,
303                rw_flag: true,
304                rollback: false,
305                is_service: false,
306            };
307            self.storage_logs.push(log_query);
308            self.rollback_storage_logs.push(LogQuery {
309                rollback: true,
310                ..log_query
311            });
312        }
313        let initial_value = self
314            .storage_initial_values
315            .entry((contract, key))
316            .or_insert_with(|| world.read_storage(contract, key));
317
318        if world.is_free_storage_slot(&contract, &key) {
319            // Free write: the value changes but no pubdata is paid, so the entry
320            // keeps its prior paid amount. One journaling traversal — no
321            // separate read-back of the prior entry.
322            self.storage_writes
323                .update((contract, key), |prev| StorageWriteEntry {
324                    value,
325                    paid: prev.map_or(0, |e| e.paid),
326                });
327            if self.slot_add_flag((contract, key), SLOT_WRITTEN) {
328                tracer.on_extra_prover_cycles(CycleStats::StorageWrite);
329            }
330            self.slot_add_flag((contract, key), SLOT_READ);
331
332            self.storage_refunds.push(WARM_WRITE_REFUND);
333            self.pubdata_costs.push(0);
334            return WARM_WRITE_REFUND;
335        }
336
337        let update_cost = world.cost_of_writing_storage(*initial_value, value);
338        // Single insert with the final paid amount; `prepaid` (the prior paid)
339        // comes from the replaced entry, avoiding a separate lookup.
340        let prepaid = self
341            .storage_writes
342            .insert(
343                (contract, key),
344                StorageWriteEntry {
345                    value,
346                    paid: update_cost,
347                },
348            )
349            .map_or(0, |e| e.paid);
350
351        let refund = if self.slot_add_flag((contract, key), SLOT_WRITTEN) {
352            tracer.on_extra_prover_cycles(CycleStats::StorageWrite);
353
354            if self.slot_add_flag((contract, key), SLOT_READ) {
355                0
356            } else {
357                COLD_WRITE_AFTER_WARM_READ_REFUND
358            }
359        } else {
360            WARM_WRITE_REFUND
361        };
362
363        #[allow(clippy::cast_possible_wrap)]
364        {
365            let pubdata_cost = (update_cost as i32) - (prepaid as i32);
366            self.pubdata.0 += pubdata_cost;
367            self.storage_refunds.push(refund);
368            self.pubdata_costs.push(pubdata_cost);
369        }
370        refund
371    }
372
373    pub(crate) fn pubdata(&self) -> i32 {
374        self.pubdata.0
375    }
376
377    /// Returns recorded refunds for all storage operations.
378    pub fn storage_refunds(&self) -> &[u32] {
379        self.storage_refunds.as_ref()
380    }
381
382    /// Returns recorded pubdata costs for all storage operations.
383    pub fn pubdata_costs(&self) -> &[i32] {
384        self.pubdata_costs.as_ref()
385    }
386
387    /// Iterates over slots that need a *protective read* — read at rollback-depth
388    /// zero (the dedup's `did_read_at_depth_zero` set). Combined with
389    /// `storage_writes` this is the set of slots that appear in the deduplicated
390    /// storage logs. Sorted by (address, key) via the `slot_flags` map's
391    /// `BTreeMap` backing.
392    pub fn protective_reads_iter(&self) -> impl Iterator<Item = (H160, U256)> + '_ {
393        self.slot_flags
394            .as_ref()
395            .iter()
396            .filter(|(_, f)| **f & SLOT_PROTECTIVE_READ != 0)
397            .map(|(k, _)| *k)
398    }
399
400    /// Returns the initial (pre-batch) value of a slot if it has been
401    /// touched by a read or write during execution. Used by per-slot summary
402    /// derivation in place of walking the `storage_logs` trace.
403    ///
404    /// The set of slots returning `Some` depends on the recording mode (see
405    /// [`Self::set_record_storage_logs`]): with the trace disabled, read-only
406    /// slots are cached and reported here; in the default mode only written
407    /// slots are.
408    pub fn initial_storage_value(&self, contract: H160, key: U256) -> Option<crate::StorageSlot> {
409        self.storage_initial_values.get(&(contract, key)).copied()
410    }
411
412    /// Returns all recorded storage log queries.
413    ///
414    /// These logs are sufficient for vm2 state-transition checks and diagnostics.
415    // TODO: We don't fill all the `zk_evm` witness metadata, so this is not suitable for
416    // generating EraVM prover witness data. This is not the goal, however, as we only need
417    // to emit enough data to verify the correctness of the state transition.
418    pub fn storage_log_queries(&self) -> &[LogQuery] {
419        &self.storage_logs
420    }
421
422    /// Returns storage log queries recorded after the specified `snapshot` was created.
423    pub fn storage_log_queries_after(&self, snapshot: &Snapshot) -> &[LogQuery] {
424        &self.storage_logs[snapshot.storage_logs_len..]
425    }
426
427    #[doc(hidden)] // like `StateInterface::get_storage_state()` but exposes the full `StorageWriteEntry` (value + paid) for random access
428    pub fn get_storage_state(&self) -> &BTreeMap<(H160, U256), StorageWriteEntry> {
429        self.storage_writes.as_ref()
430    }
431
432    /// Gets changes for all touched storage slots.
433    pub fn get_storage_changes(&self) -> impl Iterator<Item = ((H160, U256), StorageChange)> + '_ {
434        self.storage_writes
435            .as_ref()
436            .iter()
437            .filter_map(|(key, entry)| {
438                let initial_slot = &self.storage_initial_values[key];
439                if initial_slot.value == entry.value {
440                    None
441                } else {
442                    Some((
443                        *key,
444                        StorageChange {
445                            before: initial_slot.value,
446                            after: entry.value,
447                            is_initial: initial_slot.is_write_initial,
448                        },
449                    ))
450                }
451            })
452    }
453
454    /// Gets changes for storage slots touched after the specified `snapshot` was created.
455    pub fn get_storage_changes_after(
456        &self,
457        snapshot: &Snapshot,
458    ) -> impl Iterator<Item = ((H160, U256), StorageChange)> + '_ {
459        self.storage_writes
460            .changes_after(snapshot.storage_writes)
461            .into_iter()
462            .map(|(key, (before, after))| {
463                let initial = self.storage_initial_values[&key];
464                (
465                    key,
466                    StorageChange {
467                        before: before.map_or(initial.value, |e| e.value),
468                        after: after.value,
469                        is_initial: initial.is_write_initial,
470                    },
471                )
472            })
473    }
474
475    pub(crate) fn read_transient_storage(&mut self, contract: H160, key: U256) -> U256 {
476        self.pubdata_costs.push(0);
477        self.transient_storage_changes
478            .as_ref()
479            .get(&(contract, key))
480            .copied()
481            .unwrap_or_default()
482    }
483
484    pub(crate) fn write_transient_storage(&mut self, contract: H160, key: U256, value: U256) {
485        self.pubdata_costs.push(0);
486        self.transient_storage_changes
487            .insert((contract, key), value);
488    }
489
490    pub(crate) fn get_transient_storage_state(&self) -> &BTreeMap<(H160, U256), U256> {
491        self.transient_storage_changes.as_ref()
492    }
493
494    pub(crate) fn record_event(&mut self, event: Event) {
495        self.events.push(event);
496    }
497
498    pub(crate) fn events(&self) -> &[Event] {
499        self.events.as_ref()
500    }
501
502    /// Returns events emitted after the specified `snapshot` was created.
503    pub fn events_after(&self, snapshot: &Snapshot) -> &[Event] {
504        self.events.logs_after(snapshot.events)
505    }
506
507    pub(crate) fn record_l2_to_l1_log(&mut self, log: L2ToL1Log) {
508        self.l2_to_l1_logs.push(log);
509    }
510
511    pub(crate) fn l2_to_l1_logs(&self) -> &[L2ToL1Log] {
512        self.l2_to_l1_logs.as_ref()
513    }
514
515    /// Returns L2-to-L1 logs emitted after the specified `snapshot` was created.
516    pub fn l2_to_l1_logs_after(&self, snapshot: &Snapshot) -> &[L2ToL1Log] {
517        self.l2_to_l1_logs.logs_after(snapshot.l2_to_l1_logs)
518    }
519
520    /// Returns hashes of contract bytecodes that were observed by decommit bookkeeping in no
521    /// particular order.
522    ///
523    /// This includes successful decommits and far-call out-of-gas attempts recorded as
524    /// [`DecommitState::Unsuccessful`] for legacy `used_contract_hashes` compatibility.
525    pub fn decommitted_hashes(&self) -> impl Iterator<Item = U256> + '_ {
526        self.decommitted_hashes.as_ref().keys().copied()
527    }
528
529    pub(crate) fn decommit_page(&self, code_hash: U256) -> Option<HeapId> {
530        self.decommitted_hashes
531            .as_ref()
532            .get(&code_hash)
533            .and_then(|state| {
534                if let DecommitState::Succeeded(page) = state {
535                    Some(HeapId::from_u32_unchecked(*page))
536                } else {
537                    None
538                }
539            })
540    }
541
542    pub(crate) fn is_decommit_page_pinned(&self, page: HeapId) -> bool {
543        self.decommit_pinned_pages.as_ref().contains(&page.as_u32())
544    }
545
546    pub(crate) fn set_decommit_page(&mut self, code_hash: U256, page: HeapId) {
547        self.decommitted_hashes
548            .insert(code_hash, DecommitState::Succeeded(page.as_u32()));
549        self.decommit_pinned_pages.add(page.as_u32());
550    }
551
552    /// Get a snapshot for selecting which logs & co. to output using [`Self::events_after()`] and other methods.
553    pub fn snapshot(&self) -> Snapshot {
554        Snapshot {
555            storage_writes: self.storage_writes.snapshot(),
556            events: self.events.snapshot(),
557            l2_to_l1_logs: self.l2_to_l1_logs.snapshot(),
558            transient_storage_changes: self.transient_storage_changes.snapshot(),
559            pubdata: self.pubdata.snapshot(),
560            storage_logs_len: self.storage_logs.len(),
561            rollback_storage_logs_len: self.rollback_storage_logs.len(),
562        }
563    }
564
565    /// Appends rollback storage logs recorded after `snapshot` to `storage_logs`.
566    ///
567    /// This is needed for failed frame returns (revert / panic) where rolled-back writes
568    /// must remain observable in the storage log stream.
569    pub(crate) fn append_rollback_logs(&mut self, snapshot: &Snapshot) {
570        if self.rollback_storage_logs.len() > snapshot.rollback_storage_logs_len {
571            let rollback_logs = self
572                .rollback_storage_logs
573                .split_off(snapshot.rollback_storage_logs_len);
574            for log in rollback_logs.into_iter().rev() {
575                self.storage_logs.push(log);
576            }
577        }
578    }
579
580    #[allow(clippy::needless_pass_by_value)] // intentional: we require a snapshot to be rolled back to no more than once
581    pub(crate) fn rollback(&mut self, snapshot: Snapshot) {
582        self.storage_writes.rollback(snapshot.storage_writes);
583        self.events.rollback(snapshot.events);
584        self.l2_to_l1_logs.rollback(snapshot.l2_to_l1_logs);
585        self.transient_storage_changes
586            .rollback(snapshot.transient_storage_changes);
587        self.pubdata.rollback(snapshot.pubdata);
588    }
589
590    /// This function must only be called during the initial frame
591    /// because otherwise internal rollbacks can roll back past the external snapshot.
592    pub(crate) fn external_snapshot(&self) -> ExternalSnapshot {
593        // Rolling back to this snapshot will clear transient storage even though it is not empty
594        // after a transaction. This is ok because the next instruction in the bootloader
595        // (IncrementTxNumber) clears the transient storage anyway.
596        // This is necessary because clear_transient_storage cannot be undone.
597        ExternalSnapshot {
598            internal_snapshot: Snapshot {
599                transient_storage_changes: 0,
600                ..self.snapshot()
601            },
602            decommitted_hashes: self.decommitted_hashes.snapshot(),
603            decommit_pinned_pages: self.decommit_pinned_pages.snapshot(),
604            slot_flags: self.slot_flags.snapshot(),
605            storage_refunds: self.storage_refunds.snapshot(),
606            pubdata_costs: self.pubdata_costs.snapshot(),
607        }
608    }
609
610    pub(crate) fn external_rollback(&mut self, snapshot: ExternalSnapshot) {
611        let storage_logs_len = snapshot.internal_snapshot.storage_logs_len;
612        let rollback_storage_logs_len = snapshot.internal_snapshot.rollback_storage_logs_len;
613
614        self.rollback(snapshot.internal_snapshot);
615        self.storage_refunds.rollback(snapshot.storage_refunds);
616        self.pubdata_costs.rollback(snapshot.pubdata_costs);
617        self.decommitted_hashes
618            .rollback(snapshot.decommitted_hashes);
619        self.decommit_pinned_pages
620            .rollback(snapshot.decommit_pinned_pages);
621        self.slot_flags.rollback(snapshot.slot_flags);
622        self.storage_logs.truncate(storage_logs_len);
623        self.rollback_storage_logs
624            .truncate(rollback_storage_logs_len);
625    }
626
627    pub(crate) fn delete_history(&mut self) {
628        self.storage_writes.delete_history();
629        self.transient_storage_changes.delete_history();
630        self.events.delete_history();
631        self.l2_to_l1_logs.delete_history();
632        self.pubdata.delete_history();
633        self.storage_refunds.delete_history();
634        self.pubdata_costs.delete_history();
635        self.decommitted_hashes.delete_history();
636        self.decommit_pinned_pages.delete_history();
637        self.slot_flags.delete_history();
638    }
639
640    pub(crate) fn clear_transient_storage(&mut self) {
641        self.transient_storage_changes = RollbackableMap::default();
642    }
643}
644
645/// Opaque snapshot of a [`WorldDiff`] output by its [eponymous method](WorldDiff::snapshot()).
646/// Can be provided to [`WorldDiff::events_after()`] etc. to get data after the snapshot was created.
647#[derive(Clone, PartialEq, Debug)]
648pub struct Snapshot {
649    storage_writes: <RollbackableMap<(H160, U256), StorageWriteEntry> as Rollback>::Snapshot,
650    events: <RollbackableLog<Event> as Rollback>::Snapshot,
651    l2_to_l1_logs: <RollbackableLog<L2ToL1Log> as Rollback>::Snapshot,
652    transient_storage_changes: <RollbackableMap<(H160, U256), U256> as Rollback>::Snapshot,
653    pubdata: <RollbackablePod<i32> as Rollback>::Snapshot,
654    storage_logs_len: usize,
655    rollback_storage_logs_len: usize,
656}
657
658/// Change in a single storage slot.
659#[derive(Debug, PartialEq)]
660pub struct StorageChange {
661    /// Value before the slot was written to.
662    pub before: U256,
663    /// Value written to the slot.
664    pub after: U256,
665    /// `true` if the slot is not set in the [`World`](crate::World).
666    /// A write may be initial even if it isn't the first write to a slot!
667    pub is_initial: bool,
668}
669
670const WARM_READ_REFUND: u32 = STORAGE_ACCESS_COLD_READ_COST - STORAGE_ACCESS_WARM_READ_COST;
671const WARM_WRITE_REFUND: u32 = STORAGE_ACCESS_COLD_WRITE_COST - STORAGE_ACCESS_WARM_WRITE_COST;
672const COLD_WRITE_AFTER_WARM_READ_REFUND: u32 = STORAGE_ACCESS_COLD_READ_COST;
673
674#[cfg(test)]
675mod tests {
676    use std::collections::BTreeSet;
677
678    use proptest::{bits, collection::btree_map, prelude::*};
679
680    use super::*;
681    use crate::StorageSlot;
682
683    fn test_storage_changes(
684        initial_values: &BTreeMap<(H160, U256), StorageSlot>,
685        first_changes: BTreeMap<(H160, U256), U256>,
686        second_changes: BTreeMap<(H160, U256), U256>,
687    ) {
688        let mut world_diff = WorldDiff {
689            storage_initial_values: initial_values.clone(),
690            ..WorldDiff::default()
691        };
692
693        let checkpoint1 = world_diff.snapshot();
694        for (key, value) in &first_changes {
695            world_diff.write_storage(&mut NoWorld, &mut (), key.0, key.1, *value, 0);
696        }
697        let actual_changes = world_diff
698            .get_storage_changes_after(&checkpoint1)
699            .collect::<BTreeMap<_, _>>();
700        let expected_changes = first_changes
701            .iter()
702            .map(|(key, value)| {
703                let before = initial_values
704                    .get(key)
705                    .map_or_else(U256::zero, |slot| slot.value);
706                let is_initial = initial_values
707                    .get(key)
708                    .is_none_or(|slot| slot.is_write_initial);
709                (
710                    *key,
711                    StorageChange {
712                        before,
713                        after: *value,
714                        is_initial,
715                    },
716                )
717            })
718            .collect();
719        assert_eq!(actual_changes, expected_changes);
720
721        let checkpoint2 = world_diff.snapshot();
722        for (key, value) in &second_changes {
723            world_diff.write_storage(&mut NoWorld, &mut (), key.0, key.1, *value, 0);
724        }
725        let actual_changes = world_diff
726            .get_storage_changes_after(&checkpoint2)
727            .collect::<BTreeMap<_, _>>();
728        let expected_changes = second_changes
729            .iter()
730            .map(|(key, value)| {
731                let before = first_changes
732                    .get(key)
733                    .or(initial_values.get(key).map(|slot| &slot.value))
734                    .copied()
735                    .unwrap_or_default();
736                let is_initial = initial_values
737                    .get(key)
738                    .is_none_or(|slot| slot.is_write_initial);
739                (
740                    *key,
741                    StorageChange {
742                        before,
743                        after: *value,
744                        is_initial,
745                    },
746                )
747            })
748            .collect();
749        assert_eq!(actual_changes, expected_changes);
750
751        let mut combined = first_changes
752            .into_iter()
753            .filter_map(|(key, value)| {
754                let initial = initial_values
755                    .get(&key)
756                    .copied()
757                    .unwrap_or(StorageSlot::EMPTY);
758                (initial.value != value).then_some((
759                    key,
760                    StorageChange {
761                        before: initial.value,
762                        after: value,
763                        is_initial: initial.is_write_initial,
764                    },
765                ))
766            })
767            .collect::<BTreeMap<_, _>>();
768        for (key, value) in second_changes {
769            let initial = initial_values
770                .get(&key)
771                .copied()
772                .unwrap_or(StorageSlot::EMPTY);
773            if initial.value == value {
774                combined.remove(&key);
775            } else {
776                combined.insert(
777                    key,
778                    StorageChange {
779                        before: initial.value,
780                        after: value,
781                        is_initial: initial.is_write_initial,
782                    },
783                );
784            }
785        }
786
787        assert_eq!(combined, world_diff.get_storage_changes().collect());
788    }
789
790    proptest! {
791        #[test]
792        fn storage_changes_work_as_expected(
793            initial_values in arbitrary_initial_storage(),
794            first_changes in arbitrary_storage_changes(),
795            second_changes in arbitrary_storage_changes(),
796        ) {
797            test_storage_changes(&initial_values, first_changes, second_changes);
798        }
799
800        #[test]
801        fn storage_changes_work_with_constrained_changes(
802            initial_values in constrained_initial_storage(),
803            first_changes in constrained_storage_changes(),
804            second_changes in constrained_storage_changes(),
805        ) {
806            test_storage_changes(&initial_values, first_changes, second_changes);
807        }
808    }
809
810    /// Max items in generated initial storage / changes.
811    const MAX_ITEMS: usize = 5;
812    /// Bit mask for bytes in constrained `U256` / `H160` values.
813    const BIT_MASK: u8 = 0b_1111;
814
815    fn arbitrary_initial_storage() -> impl Strategy<Value = BTreeMap<(H160, U256), StorageSlot>> {
816        btree_map(
817            any::<([u8; 20], [u8; 32])>()
818                .prop_map(|(contract, key)| (H160::from(contract), U256::from(key))),
819            any::<([u8; 32], bool)>().prop_map(|(value, is_write_initial)| StorageSlot {
820                value: U256::from(value),
821                is_write_initial,
822            }),
823            0..=MAX_ITEMS,
824        )
825    }
826
827    fn constrained_initial_storage() -> impl Strategy<Value = BTreeMap<(H160, U256), StorageSlot>> {
828        btree_map(
829            (bits::u8::masked(BIT_MASK), bits::u8::masked(BIT_MASK))
830                .prop_map(|(contract, key)| (H160::repeat_byte(contract), U256::from(key))),
831            (bits::u8::masked(BIT_MASK), any::<bool>()).prop_map(|(value, is_write_initial)| {
832                StorageSlot {
833                    value: U256::from(value),
834                    is_write_initial,
835                }
836            }),
837            0..=MAX_ITEMS,
838        )
839    }
840
841    fn arbitrary_storage_changes() -> impl Strategy<Value = BTreeMap<(H160, U256), U256>> {
842        btree_map(
843            any::<([u8; 20], [u8; 32])>()
844                .prop_map(|(contract, key)| (H160::from(contract), U256::from(key))),
845            any::<[u8; 32]>().prop_map(U256::from),
846            0..=MAX_ITEMS,
847        )
848    }
849
850    fn constrained_storage_changes() -> impl Strategy<Value = BTreeMap<(H160, U256), U256>> {
851        btree_map(
852            (bits::u8::masked(BIT_MASK), bits::u8::masked(BIT_MASK))
853                .prop_map(|(contract, key)| (H160::repeat_byte(contract), U256::from(key))),
854            bits::u8::masked(BIT_MASK).prop_map(U256::from),
855            0..=MAX_ITEMS,
856        )
857    }
858
859    struct NoWorld;
860
861    impl StorageInterface for NoWorld {
862        fn read_storage(&mut self, _: H160, _: U256) -> StorageSlot {
863            StorageSlot::EMPTY
864        }
865
866        fn cost_of_writing_storage(&mut self, _: StorageSlot, _: U256) -> u32 {
867            0
868        }
869
870        fn is_free_storage_slot(&self, _: &H160, _: &U256) -> bool {
871            false
872        }
873    }
874
875    /// A world with a fixed non-zero write cost, to exercise the merged `paid`
876    /// accounting (`prior_paid` / `prepaid`) that `NoWorld`/`TestWorld` (cost 0)
877    /// leave untested.
878    struct CostWorld;
879
880    impl StorageInterface for CostWorld {
881        fn read_storage(&mut self, _: H160, _: U256) -> StorageSlot {
882            StorageSlot::EMPTY
883        }
884
885        fn cost_of_writing_storage(&mut self, _: StorageSlot, _: U256) -> u32 {
886            100
887        }
888
889        fn is_free_storage_slot(&self, _: &H160, _: &U256) -> bool {
890            false
891        }
892    }
893
894    #[test]
895    fn merged_storage_write_tracks_paid_and_rolls_back() {
896        let mut world_diff = WorldDiff::default();
897        let (contract, key) = (H160::zero(), U256::from(1));
898
899        // First write: prior_paid = 0, entry's paid set to the write cost.
900        world_diff.write_storage(&mut CostWorld, &mut (), contract, key, U256::from(5), 0);
901        let e = world_diff.storage_writes.as_ref()[&(contract, key)];
902        assert_eq!((e.value, e.paid), (U256::from(5), 100));
903
904        // Second write reuses prior_paid (=100) as `prepaid`; entry re-set.
905        let snapshot = world_diff.snapshot();
906        world_diff.write_storage(&mut CostWorld, &mut (), contract, key, U256::from(6), 0);
907        let e = world_diff.storage_writes.as_ref()[&(contract, key)];
908        assert_eq!((e.value, e.paid), (U256::from(6), 100));
909
910        // Rollback restores both the value and the paid amount of the merged entry.
911        world_diff.rollback(snapshot);
912        let e = world_diff.storage_writes.as_ref()[&(contract, key)];
913        assert_eq!((e.value, e.paid), (U256::from(5), 100));
914    }
915
916    #[derive(Default)]
917    struct TestWorld {
918        values: BTreeMap<(H160, U256), U256>,
919    }
920
921    impl StorageInterface for TestWorld {
922        fn read_storage(&mut self, contract: H160, key: U256) -> StorageSlot {
923            let value = self
924                .values
925                .get(&(contract, key))
926                .copied()
927                .unwrap_or_default();
928            StorageSlot {
929                value,
930                is_write_initial: !self.values.contains_key(&(contract, key)),
931            }
932        }
933
934        fn cost_of_writing_storage(&mut self, _: StorageSlot, _: U256) -> u32 {
935            0
936        }
937
938        fn is_free_storage_slot(&self, _: &H160, _: &U256) -> bool {
939            false
940        }
941    }
942
943    #[test]
944    fn storage_logs_include_reads_writes_and_rollbacks() {
945        let mut world_diff = WorldDiff::default();
946        let mut world = TestWorld::default();
947        let contract = H160::zero();
948        let key = U256::from(1);
949
950        let (value, _) = world_diff.read_storage(&mut world, &mut (), contract, key, 0);
951        assert_eq!(value, U256::zero());
952
953        world_diff.write_storage(&mut world, &mut (), contract, key, U256::from(10), 0);
954        let snapshot = world_diff.snapshot();
955        world_diff.write_storage(&mut world, &mut (), contract, key, U256::from(20), 0);
956        world_diff.append_rollback_logs(&snapshot);
957        world_diff.rollback(snapshot);
958
959        let logs = world_diff.storage_log_queries();
960        assert_eq!(logs.len(), 4);
961        assert!(!logs[0].rw_flag);
962        assert!(logs[1].rw_flag && !logs[1].rollback);
963        assert!(logs[2].rw_flag && !logs[2].rollback);
964        assert!(logs[3].rw_flag && logs[3].rollback);
965        assert_eq!(logs[3].read_value, logs[2].read_value);
966        assert_eq!(logs[3].written_value, logs[2].written_value);
967    }
968
969    #[test]
970    fn skip_storage_logs_drops_trace_but_keeps_dedup_inputs() {
971        let mut world_diff = WorldDiff::default();
972        world_diff.set_record_storage_logs(false);
973        let mut world = TestWorld::default();
974        let contract = H160::zero();
975        let key = U256::from(1);
976
977        // Same access pattern as `storage_logs_include_reads_writes_and_rollbacks`.
978        let (value, _) = world_diff.read_storage(&mut world, &mut (), contract, key, 0);
979        assert_eq!(value, U256::zero());
980        world_diff.write_storage(&mut world, &mut (), contract, key, U256::from(10), 0);
981        let snapshot = world_diff.snapshot();
982        world_diff.write_storage(&mut world, &mut (), contract, key, U256::from(20), 0);
983        world_diff.append_rollback_logs(&snapshot);
984        world_diff.rollback(snapshot);
985
986        // The per-access trace is empty — the whole point of opting out.
987        assert!(world_diff.storage_log_queries().is_empty());
988        assert!(world_diff.rollback_storage_logs.is_empty());
989
990        // ...but the maps a re-execution verifier derives the deduplicated set
991        // from are still populated: the slot needs a protective read (read at
992        // depth zero) and its initial value cached.
993        assert!(world_diff
994            .protective_reads_iter()
995            .any(|slot| slot == (contract, key)));
996        assert_eq!(
997            world_diff
998                .initial_storage_value(contract, key)
999                .map(|s| s.value),
1000            Some(U256::zero())
1001        );
1002    }
1003
1004    #[test]
1005    fn recording_mode_does_not_cache_read_only_slots() {
1006        // P2 regression: in default (recording / Boojum) mode a read-only slot
1007        // must not be added to `storage_initial_values`, so memory behavior
1008        // matches the pre-optimization base. The trace still records the read.
1009        let mut world_diff = WorldDiff::default();
1010        let mut world = TestWorld::default();
1011        let (contract, key) = (H160::zero(), U256::from(1));
1012
1013        let (value, _) = world_diff.read_storage(&mut world, &mut (), contract, key, 0);
1014        assert_eq!(value, U256::zero());
1015
1016        assert_eq!(world_diff.storage_log_queries().len(), 1);
1017        assert!(world_diff.initial_storage_value(contract, key).is_none());
1018        assert!(world_diff.protective_reads_iter().next().is_none());
1019    }
1020
1021    /// The set of slots a re-execution verifier surfaces in the deduplicated
1022    /// storage log when the trace is dropped: protective reads plus writes.
1023    fn dedup_input_set(world_diff: &WorldDiff) -> BTreeSet<(H160, U256)> {
1024        let mut set: BTreeSet<(H160, U256)> = world_diff.protective_reads_iter().collect();
1025        set.extend(world_diff.storage_writes.as_ref().keys().copied());
1026        set
1027    }
1028
1029    #[test]
1030    fn external_rollback_restores_opt_out_dedup_inputs_for_retry() {
1031        // Models the airbender verifier's bytecode-compression retry in
1032        // `execute_tx`: make_snapshot -> run tx -> external_rollback -> re-run
1033        // the same tx. In opt-out mode the deduplicated-set inputs (protective
1034        // reads + writes) reconstructed after the retry must match a clean run
1035        // that never did the discarded first attempt -- i.e. external rollback
1036        // fully erases the first attempt's effect on `slot_flags`.
1037        let a = (H160::zero(), U256::from(1));
1038        let b = (H160::zero(), U256::from(2));
1039        let c = (H160::zero(), U256::from(3));
1040
1041        let mut world = TestWorld::default();
1042        world.values.insert(b, U256::from(200));
1043        world.values.insert(c, U256::from(300));
1044
1045        // The kept (second) attempt: read b at depth zero (protective), write c.
1046        let run_kept_attempt = |wd: &mut WorldDiff, world: &mut TestWorld| {
1047            wd.read_storage(world, &mut (), b.0, b.1, 0);
1048            wd.write_storage(world, &mut (), c.0, c.1, U256::from(777), 0);
1049        };
1050
1051        // Retried: snapshot, discarded first attempt (read a, write b), roll the
1052        // whole tx back, then run the kept attempt.
1053        let mut retried = WorldDiff::default();
1054        retried.set_record_storage_logs(false);
1055        let snapshot = retried.external_snapshot();
1056        retried.read_storage(&mut world, &mut (), a.0, a.1, 0);
1057        retried.write_storage(&mut world, &mut (), b.0, b.1, U256::from(999), 0);
1058        retried.external_rollback(snapshot);
1059        run_kept_attempt(&mut retried, &mut world);
1060
1061        // Clean: only the kept attempt, no discarded first attempt at all.
1062        let mut clean = WorldDiff::default();
1063        clean.set_record_storage_logs(false);
1064        run_kept_attempt(&mut clean, &mut world);
1065
1066        // The reconstructed dedup input set matches, and slot `a` (touched only
1067        // in the discarded attempt) is absent -- external rollback erased it.
1068        assert_eq!(dedup_input_set(&retried), dedup_input_set(&clean));
1069        assert_eq!(dedup_input_set(&clean), BTreeSet::from([b, c]));
1070        assert!(!dedup_input_set(&retried).contains(&a));
1071
1072        // Written values and cached initial values agree for every slot in the set.
1073        for slot in dedup_input_set(&clean) {
1074            assert_eq!(
1075                retried.storage_writes.as_ref().get(&slot).map(|e| e.value),
1076                clean.storage_writes.as_ref().get(&slot).map(|e| e.value),
1077            );
1078            assert_eq!(
1079                retried
1080                    .initial_storage_value(slot.0, slot.1)
1081                    .map(|s| s.value),
1082                clean.initial_storage_value(slot.0, slot.1).map(|s| s.value),
1083            );
1084        }
1085    }
1086
1087    #[test]
1088    fn protective_read_survives_internal_rollback_but_not_external() {
1089        // The protective-read (`did_read_at_depth_zero`) bit is global within a
1090        // run: a frame revert (internal rollback) must NOT clear it, but a
1091        // whole-tx discard (external rollback) must.
1092        let a = (H160::zero(), U256::from(1));
1093        let mut world = TestWorld::default();
1094        world.values.insert(a, U256::from(100));
1095
1096        let mut wd = WorldDiff::default();
1097        wd.set_record_storage_logs(false);
1098
1099        let external = wd.external_snapshot();
1100        wd.read_storage(&mut world, &mut (), a.0, a.1, 0); // protective read at depth zero
1101
1102        // A nested write that reverts (internal rollback) must leave the bit set.
1103        let internal = wd.snapshot();
1104        wd.write_storage(&mut world, &mut (), a.0, a.1, U256::from(999), 0);
1105        wd.rollback(internal);
1106        assert!(
1107            wd.protective_reads_iter().any(|s| s == a),
1108            "protective read must survive an internal frame rollback",
1109        );
1110
1111        // A whole-tx external rollback must erase it.
1112        wd.external_rollback(external);
1113        assert!(
1114            !wd.protective_reads_iter().any(|s| s == a),
1115            "external rollback must clear the protective read",
1116        );
1117    }
1118
1119    #[test]
1120    fn protective_read_not_set_when_write_is_pending() {
1121        // A read is a protective read only at rollback-depth zero: if a write to
1122        // the slot is already pending, the read observes that write rather than
1123        // the committed value, so the bit must not be set. The slot still enters
1124        // the dedup set -- via `storage_writes`, not as a protective read.
1125        let a = (H160::zero(), U256::from(1));
1126        let mut world = TestWorld::default();
1127        world.values.insert(a, U256::from(100));
1128
1129        let mut wd = WorldDiff::default();
1130        wd.set_record_storage_logs(false);
1131
1132        wd.write_storage(&mut world, &mut (), a.0, a.1, U256::from(5), 0);
1133        let (value, _) = wd.read_storage(&mut world, &mut (), a.0, a.1, 0);
1134
1135        assert_eq!(value, U256::from(5), "read must observe the pending write");
1136        assert!(
1137            !wd.protective_reads_iter().any(|s| s == a),
1138            "a read with a pending write must not be a protective read",
1139        );
1140        assert!(
1141            dedup_input_set(&wd).contains(&a),
1142            "slot is still in the dedup set as a write"
1143        );
1144    }
1145
1146    #[test]
1147    #[should_panic(expected = "before any storage access")]
1148    fn set_record_storage_logs_after_access_panics() {
1149        // P3 regression: the mode switch must reject being toggled once any
1150        // storage access has happened, rather than silently corrupting state.
1151        let mut world_diff = WorldDiff::default();
1152        let mut world = TestWorld::default();
1153        world_diff.read_storage(&mut world, &mut (), H160::zero(), U256::from(1), 0);
1154        world_diff.set_record_storage_logs(false);
1155    }
1156
1157    #[test]
1158    fn rollback_without_append_keeps_storage_log_stream_unchanged() {
1159        let mut world_diff = WorldDiff::default();
1160        let mut world = TestWorld::default();
1161        let contract = H160::zero();
1162        let key = U256::from(1);
1163
1164        world_diff.write_storage(&mut world, &mut (), contract, key, U256::from(10), 0);
1165        let snapshot = world_diff.snapshot();
1166        world_diff.write_storage(&mut world, &mut (), contract, key, U256::from(20), 0);
1167        world_diff.rollback(snapshot);
1168
1169        let logs = world_diff.storage_log_queries();
1170        assert_eq!(logs.len(), 2);
1171        assert!(logs.iter().all(|log| log.rw_flag && !log.rollback));
1172        assert_eq!(world_diff.rollback_storage_logs.len(), 2);
1173    }
1174
1175    #[test]
1176    fn external_rollback_truncates_storage_logs_to_internal_snapshot() {
1177        let mut world_diff = WorldDiff::default();
1178        let mut world = TestWorld::default();
1179        let contract = H160::zero();
1180        let key = U256::from(1);
1181
1182        world_diff.write_storage(&mut world, &mut (), contract, key, U256::from(10), 0);
1183        let snapshot = world_diff.external_snapshot();
1184        world_diff.write_storage(&mut world, &mut (), contract, key, U256::from(20), 0);
1185
1186        world_diff.external_rollback(snapshot);
1187
1188        let logs = world_diff.storage_log_queries();
1189        assert_eq!(logs.len(), 1);
1190        assert!(logs[0].rw_flag && !logs[0].rollback);
1191        assert_eq!(world_diff.rollback_storage_logs.len(), 1);
1192    }
1193
1194    #[test]
1195    fn storage_read_log_sets_written_value_to_read_value() {
1196        let mut world_diff = WorldDiff::default();
1197        let mut world = TestWorld::default();
1198        let contract = H160::repeat_byte(1);
1199        let key = U256::from(7);
1200        let value = U256::from(33);
1201        world.values.insert((contract, key), value);
1202
1203        let (read_value, _) = world_diff.read_storage(&mut world, &mut (), contract, key, 0);
1204        assert_eq!(read_value, value);
1205
1206        let logs = world_diff.storage_log_queries();
1207        assert_eq!(logs.len(), 1);
1208        assert!(!logs[0].rw_flag);
1209        assert_eq!(logs[0].read_value, value);
1210        assert_eq!(logs[0].written_value, value);
1211    }
1212}