sui_adapter_latest/
temporary_store.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::execution_mode::ExecutionMode;
5use crate::gas_charger::{GasCharger, PaymentLocation};
6use mysten_common::{ZipDebugEqIteratorExt, debug_fatal};
7use mysten_metrics::monitored_scope;
8use parking_lot::RwLock;
9use std::collections::{BTreeMap, BTreeSet, HashSet};
10use sui_protocol_config::ProtocolConfig;
11use sui_types::accumulator_event::AccumulatorEvent;
12use sui_types::accumulator_root::AccumulatorObjId;
13use sui_types::base_types::VersionDigest;
14use sui_types::committee::EpochId;
15use sui_types::deny_list_v2::check_coin_deny_list_v2_during_execution;
16use sui_types::effects::{
17    AccumulatorOperation, AccumulatorValue, AccumulatorWriteV1, TransactionEffects,
18    TransactionEvents,
19};
20use sui_types::execution::{
21    DynamicallyLoadedObjectMetadata, ExecutionResults, ExecutionResultsV2, SharedInput,
22};
23use sui_types::execution_status::{ExecutionErrorKind, ExecutionStatus};
24use sui_types::inner_temporary_store::InnerTemporaryStore;
25use sui_types::layout_resolver::LayoutResolver;
26use sui_types::object::{Data, ObjectPermissions};
27use sui_types::storage::{BackingStore, DenyListResult, PackageObject};
28use sui_types::sui_system_state::{AdvanceEpochParams, get_sui_system_state_wrapper};
29use sui_types::{
30    SUI_DENY_LIST_OBJECT_ID,
31    base_types::{ObjectID, ObjectRef, SequenceNumber, SuiAddress, TransactionDigest},
32    effects::EffectsObjectChange,
33    error::{ExecutionError, SuiResult},
34    gas::GasCostSummary,
35    object::Object,
36    object::Owner,
37    storage::{BackingPackageStore, ChildObjectResolver, ParentSync, Storage},
38    transaction::InputObjects,
39};
40use sui_types::{SUI_SYSTEM_STATE_OBJECT_ID, TypeTag, is_system_package};
41
42pub struct TemporaryStore<'backing> {
43    // The backing store for retrieving Move packages onchain.
44    // When executing a Move call, the dependent packages are not going to be
45    // in the input objects. They will be fetched from the backing store.
46    // Also used for fetching the backing parent_sync to get the last known version for wrapped
47    // objects
48    store: &'backing dyn BackingStore,
49    tx_digest: TransactionDigest,
50    input_objects: BTreeMap<ObjectID, Object>,
51
52    /// Store the original versions of the non-exclusive write inputs, in order to detect
53    /// mutations (which are illegal, but not prevented by the type system).
54    non_exclusive_input_original_versions: BTreeMap<ObjectID, Object>,
55
56    stream_ended_consensus_objects: BTreeMap<ObjectID, SequenceNumber /* start_version */>,
57    /// The version to assign to all objects written by the transaction using this store.
58    lamport_timestamp: SequenceNumber,
59    /// Inputs that will be mutated by the transaction. Does not include NonExclusiveWrite inputs,
60    /// which can be taken as `&mut T` but cannot be directly mutated.
61    mutable_input_refs: BTreeMap<ObjectID, (VersionDigest, Owner)>,
62    execution_results: ExecutionResultsV2,
63    /// Objects that were loaded during execution (dynamic fields + received objects).
64    loaded_runtime_objects: BTreeMap<ObjectID, DynamicallyLoadedObjectMetadata>,
65    /// A map from wrapped object to its container. Used during expensive invariant checks.
66    wrapped_object_containers: BTreeMap<ObjectID, ObjectID>,
67    protocol_config: &'backing ProtocolConfig,
68
69    /// Every package that was loaded from DB store during execution.
70    /// These packages were not previously loaded into the temporary store.
71    runtime_packages_loaded_from_db: RwLock<BTreeMap<ObjectID, PackageObject>>,
72
73    /// The set of objects that we may receive during execution. Not guaranteed to receive all, or
74    /// any of the objects referenced in this set.
75    receiving_objects: Vec<ObjectRef>,
76
77    /// The set of all generated object IDs from the object runtime during the transaction. This includes any
78    /// created-and-then-deleted objects in addition to any `new_ids` which contains only the set
79    /// of created (but not deleted) IDs in the transaction.
80    generated_runtime_ids: BTreeSet<ObjectID>,
81
82    // TODO: Now that we track epoch here, there are a few places we don't need to pass it around.
83    /// The current epoch.
84    cur_epoch: EpochId,
85
86    /// The set of per-epoch config objects that were loaded during execution, and are not in the
87    /// input objects. This allows us to commit them to the effects.
88    loaded_per_epoch_config_objects: RwLock<BTreeSet<ObjectID>>,
89
90    /// Index ranges into `execution_results.accumulator_events` for events emitted from PTB
91    /// (Move) execution. Each `record_execution_results` call appends a contiguous range
92    /// bracketing the merge. Any index outside these ranges was emitted by the runtime outside
93    /// of PTB execution (currently only `gas_charger`'s `add_accumulator_event`). Consumed by
94    /// `check_sui_address_balance_changes` inside `run_conservation_checks` to gate
95    /// non-PTB-emitted events behind input reservations. Cleared on `drop_writes` since the
96    /// underlying events are also cleared. A `Vec` is sufficient because real transactions run
97    /// the PTB at most a handful of times.
98    ptb_emitted_accumulator_event_ranges: Vec<std::ops::Range<usize>>,
99}
100
101impl<'backing> TemporaryStore<'backing> {
102    /// Creates a new store associated with an authority store, and populates it with
103    /// initial objects.
104    pub fn new(
105        store: &'backing dyn BackingStore,
106        input_objects: InputObjects,
107        receiving_objects: Vec<ObjectRef>,
108        tx_digest: TransactionDigest,
109        protocol_config: &'backing ProtocolConfig,
110        cur_epoch: EpochId,
111    ) -> Self {
112        let mutable_input_refs = input_objects.exclusive_mutable_inputs();
113        let non_exclusive_input_original_versions = input_objects.non_exclusive_input_objects();
114
115        let lamport_timestamp = input_objects.lamport_timestamp(&receiving_objects);
116        let stream_ended_consensus_objects = input_objects.consensus_stream_ended_objects();
117        let objects = input_objects.into_object_map();
118        #[cfg(debug_assertions)]
119        {
120            // Ensure that input objects and receiving objects must not overlap.
121            assert!(
122                objects
123                    .keys()
124                    .collect::<HashSet<_>>()
125                    .intersection(
126                        &receiving_objects
127                            .iter()
128                            .map(|oref| &oref.0)
129                            .collect::<HashSet<_>>()
130                    )
131                    .next()
132                    .is_none()
133            );
134        }
135        Self {
136            store,
137            tx_digest,
138            input_objects: objects,
139            non_exclusive_input_original_versions,
140            stream_ended_consensus_objects,
141            lamport_timestamp,
142            mutable_input_refs,
143            execution_results: ExecutionResultsV2::default(),
144            protocol_config,
145            loaded_runtime_objects: BTreeMap::new(),
146            wrapped_object_containers: BTreeMap::new(),
147            runtime_packages_loaded_from_db: RwLock::new(BTreeMap::new()),
148            receiving_objects,
149            generated_runtime_ids: BTreeSet::new(),
150            cur_epoch,
151            loaded_per_epoch_config_objects: RwLock::new(BTreeSet::new()),
152            ptb_emitted_accumulator_event_ranges: Vec::new(),
153        }
154    }
155
156    // Helpers to access private fields
157    pub fn objects(&self) -> &BTreeMap<ObjectID, Object> {
158        &self.input_objects
159    }
160
161    pub fn update_object_version_and_prev_tx(&mut self) {
162        self.execution_results.update_version_and_previous_tx(
163            self.lamport_timestamp,
164            self.tx_digest,
165            &self.input_objects,
166            self.protocol_config.reshare_at_same_initial_version(),
167        );
168
169        #[cfg(debug_assertions)]
170        {
171            self.check_invariants();
172        }
173    }
174
175    fn calculate_accumulator_running_max_withdraws(&self) -> BTreeMap<AccumulatorObjId, u128> {
176        let mut running_net_withdraws: BTreeMap<AccumulatorObjId, i128> = BTreeMap::new();
177        let mut running_max_withdraws: BTreeMap<AccumulatorObjId, u128> = BTreeMap::new();
178        for event in &self.execution_results.accumulator_events {
179            match &event.write.value {
180                AccumulatorValue::Integer(amount) => match event.write.operation {
181                    AccumulatorOperation::Split => {
182                        let entry = running_net_withdraws
183                            .entry(event.accumulator_obj)
184                            .or_default();
185                        *entry += *amount as i128;
186                        if *entry > 0 {
187                            let max_entry = running_max_withdraws
188                                .entry(event.accumulator_obj)
189                                .or_default();
190                            *max_entry = (*max_entry).max(*entry as u128);
191                        }
192                    }
193                    AccumulatorOperation::Merge => {
194                        let entry = running_net_withdraws
195                            .entry(event.accumulator_obj)
196                            .or_default();
197                        *entry -= *amount as i128;
198                    }
199                },
200                AccumulatorValue::IntegerTuple(_, _) | AccumulatorValue::EventDigest(_) => {}
201            }
202        }
203        running_max_withdraws
204    }
205
206    /// Ensure that, per accumulator object, the gross Merge total and gross Split total are
207    /// representable: bounded by the total SUI supply for `Balance<SUI>` keys, and by `u64::MAX`
208    /// otherwise.
209    ///
210    /// `AccumulatorWriteV1::merge` folds all writes for a key by summing Merge amounts and Split
211    /// amounts separately into `u64`s. The object runtime caps Move-native merges per key at
212    /// `u64::MAX`, but the gas charger emits additional, uncapped SUI deposit/withdraw events during
213    /// gas smashing and gas charging (e.g. a refund Merge to an address balance), so a per-key SUI
214    /// total could be pushed past `u64::MAX`, overflowing that fold (and the SUI-conservation sum).
215    /// Reaching such a total requires SUI from an object-sourced withdrawal whose backing is only
216    /// verified at settlement.
217    ///
218    /// Bounding SUI to `TOTAL_SUPPLY_MIST` rejects any such amount here, *before* gas is charged, so
219    /// the rejected PTB-emitted writes are dropped on gas reset and only the (bounded) gas events
220    /// remain. Crucially, `TOTAL_SUPPLY_MIST` is ~8.4B SUI below `u64::MAX`, so the gas events emitted
221    /// after this check (which move only real SUI) cannot push any per-key total past `u64::MAX` —
222    /// hence they need not be re-checked. Non-SUI balances have no uncapped gas path, so the
223    /// object-runtime per-key `u64::MAX` cap is the binding guard there and we only backstop u64
224    /// representability.
225    ///
226    /// The per-key limits are not sufficient on their own: withdrawn SUI can be spread across several
227    /// object keys (each withdrawal `<= TOTAL_SUPPLY_MIST`) and then recombined *outside* the
228    /// accumulator — e.g. each withdrawal redeemed to a `Coin<SUI>` and merged into the PTB gas coin
229    /// via `MergeCoins`, which is an object mutation, not an accumulator event. The recombined coin
230    /// can then reach `u64::MAX` and overflow `deduct_gas` on a refund. So we also bound the
231    /// *cross-key* total SUI withdrawn (gross Split) to the supply, capping the total SUI a single
232    /// transaction can withdraw regardless of how it is later recombined.
233    pub fn check_accumulator_amounts_representable(&self) -> Result<(), ExecutionError> {
234        let supply = sui_types::gas_coin::TOTAL_SUPPLY_MIST as u128;
235        let mut merge_totals: BTreeMap<AccumulatorObjId, u128> = BTreeMap::new();
236        let mut split_totals: BTreeMap<AccumulatorObjId, u128> = BTreeMap::new();
237        // Cross-key total of SUI withdrawn (gross Split), bounded to the supply (see above).
238        let mut total_sui_split: u128 = 0;
239        for event in &self.execution_results.accumulator_events {
240            let AccumulatorValue::Integer(amount) = event.write.value else {
241                continue;
242            };
243            let amount = amount as u128;
244            // SUI cannot exceed its total supply through any single balance. Bounding to the supply
245            // (rather than u64::MAX) leaves headroom for the not-yet-emitted gas events.
246            let is_sui = sui_types::gas_coin::GasCoin::is_gas_balance_type(&event.write.address.ty);
247            let limit = if is_sui { supply } else { u64::MAX as u128 };
248            let total = match event.write.operation {
249                AccumulatorOperation::Merge => {
250                    merge_totals.entry(event.accumulator_obj).or_default()
251                }
252                AccumulatorOperation::Split => {
253                    split_totals.entry(event.accumulator_obj).or_default()
254                }
255            };
256            *total += amount;
257            if *total > limit {
258                return Err(ExecutionError::new_with_source(
259                    ExecutionErrorKind::CoinBalanceOverflow,
260                    format!(
261                        "accumulator balance change for {:?} exceeds the representable limit \
262                         (gross total {}, limit {})",
263                        event.accumulator_obj, *total, limit
264                    ),
265                ));
266            }
267            if is_sui && matches!(event.write.operation, AccumulatorOperation::Split) {
268                total_sui_split += amount;
269                if total_sui_split > supply {
270                    return Err(ExecutionError::new_with_source(
271                        ExecutionErrorKind::CoinBalanceOverflow,
272                        format!(
273                            "total SUI withdrawn across all accumulators ({total_sui_split}) \
274                             exceeds the total supply ({supply})"
275                        ),
276                    ));
277                }
278            }
279        }
280        Ok(())
281    }
282
283    /// Ensure that there is one entry for each accumulator object in the accumulator events.
284    fn merge_accumulator_events(&mut self) {
285        self.execution_results.accumulator_events = self
286            .execution_results
287            .accumulator_events
288            .iter()
289            .fold(
290                BTreeMap::<AccumulatorObjId, Vec<AccumulatorWriteV1>>::new(),
291                |mut map, event| {
292                    map.entry(event.accumulator_obj)
293                        .or_default()
294                        .push(event.write.clone());
295                    map
296                },
297            )
298            .into_iter()
299            .map(|(obj_id, writes)| {
300                AccumulatorEvent::new(obj_id, AccumulatorWriteV1::merge(writes))
301            })
302            .collect();
303    }
304
305    /// Break up the structure and return its internal stores (objects, active_inputs, written, deleted)
306    pub fn into_inner(
307        self,
308        accumulator_running_max_withdraws: BTreeMap<AccumulatorObjId, u128>,
309    ) -> InnerTemporaryStore {
310        let results = self.execution_results;
311        InnerTemporaryStore {
312            input_objects: self.input_objects,
313            stream_ended_consensus_objects: self.stream_ended_consensus_objects,
314            mutable_inputs: self.mutable_input_refs,
315            written: results.written_objects,
316            events: TransactionEvents {
317                data: results.user_events,
318            },
319            accumulator_events: results.accumulator_events,
320            loaded_runtime_objects: self.loaded_runtime_objects,
321            runtime_packages_loaded_from_db: self.runtime_packages_loaded_from_db.into_inner(),
322            lamport_version: self.lamport_timestamp,
323            binary_config: self.protocol_config.binary_config(None),
324            accumulator_running_max_withdraws,
325        }
326    }
327
328    /// For every object from active_inputs (i.e. all mutable objects), if they are not
329    /// mutated during the transaction execution, force mutating them by incrementing the
330    /// sequence number. This is required to achieve safety.
331    pub(crate) fn ensure_active_inputs_mutated(&mut self) {
332        let mut to_be_updated = vec![];
333        // Note: we do not mutate input objects if they are non-exclusive write
334        for id in self.mutable_input_refs.keys() {
335            if !self.execution_results.modified_objects.contains(id) {
336                // We cannot update here but have to push to `to_be_updated` and update later
337                // because the for loop is holding a reference to `self`, and calling
338                // `self.mutate_input_object` requires a mutable reference to `self`.
339                to_be_updated.push(self.input_objects[id].clone());
340            }
341        }
342        for object in to_be_updated {
343            // The object must be mutated as it was present in the input objects
344            self.mutate_input_object(object.clone());
345        }
346    }
347
348    fn get_object_changes(&self) -> BTreeMap<ObjectID, EffectsObjectChange> {
349        let results = &self.execution_results;
350        let all_ids = results
351            .created_object_ids
352            .iter()
353            .chain(&results.deleted_object_ids)
354            .chain(&results.modified_objects)
355            .chain(results.written_objects.keys())
356            .collect::<BTreeSet<_>>();
357        all_ids
358            .into_iter()
359            .map(|id| {
360                (
361                    *id,
362                    EffectsObjectChange::new(
363                        self.get_object_modified_at(id)
364                            .map(|metadata| ((metadata.version, metadata.digest), metadata.owner)),
365                        results.written_objects.get(id),
366                        results.created_object_ids.contains(id),
367                        results.deleted_object_ids.contains(id),
368                    ),
369                )
370            })
371            .chain(results.accumulator_events.iter().cloned().map(
372                |AccumulatorEvent {
373                     accumulator_obj,
374                     write,
375                 }| {
376                    (
377                        *accumulator_obj.inner(),
378                        EffectsObjectChange::new_from_accumulator_write(write),
379                    )
380                },
381            ))
382            .collect()
383    }
384
385    pub fn into_effects(
386        mut self,
387        shared_object_refs: Vec<SharedInput>,
388        transaction_digest: &TransactionDigest,
389        mut transaction_dependencies: BTreeSet<TransactionDigest>,
390        gas_cost_summary: GasCostSummary,
391        status: ExecutionStatus,
392        gas_charger: &mut GasCharger,
393        epoch: EpochId,
394    ) -> (InnerTemporaryStore, TransactionEffects) {
395        // Defense-in-depth: Owner::Party is not yet supported as an effect output. There are
396        // no constructions of `Owner::Party` yet so a hard assert should be safe.
397        for (id, obj) in &self.execution_results.written_objects {
398            assert!(
399                !matches!(obj.owner, Owner::Party { .. }),
400                "Party-owned objects are not yet supported (object {id})"
401            );
402        }
403
404        self.update_object_version_and_prev_tx();
405        // This must happens before merge_accumulator_events.
406        let accumulator_running_max_withdraws = self.calculate_accumulator_running_max_withdraws();
407        self.merge_accumulator_events();
408
409        // Regardless of execution status (including aborts), we insert the previous transaction
410        // for any successfully received objects during the transaction.
411        for (id, expected_version, expected_digest) in &self.receiving_objects {
412            // If the receiving object is in the loaded runtime objects, then that means that it
413            // was actually successfully loaded (so existed, and there was authenticated mutable
414            // access to it). So we insert the previous transaction as a dependency.
415            if let Some(obj_meta) = self.loaded_runtime_objects.get(id) {
416                // Check that the expected version, digest, and owner match the loaded version,
417                // digest, and owner. If they don't then don't register a dependency.
418                // This is because this could be "spoofed" by loading a dynamic object field.
419                let loaded_via_receive = obj_meta.version == *expected_version
420                    && obj_meta.digest == *expected_digest
421                    && obj_meta.owner.is_address_owned();
422                if loaded_via_receive {
423                    transaction_dependencies.insert(obj_meta.previous_transaction);
424                }
425            }
426        }
427
428        assert!(self.protocol_config.enable_effects_v2());
429
430        // In the case of special transactions that don't require a gas object,
431        // we don't really care about the effects to gas, just use the input for it.
432        // Gas coins are guaranteed to be at least size 1 and if more than 1
433        // the first coin is where all the others are merged.
434        let gas_coin = gas_charger
435            .gas_payment_amount()
436            .and_then(|gp| match gp.location {
437                PaymentLocation::Coin(coin_id) => Some(coin_id),
438                PaymentLocation::AddressBalance(_) => None,
439            });
440
441        let object_changes = self.get_object_changes();
442
443        let lamport_version = self.lamport_timestamp;
444        // TODO: Cleanup this clone. Potentially add unchanged_shraed_objects directly to InnerTempStore.
445        let loaded_per_epoch_config_objects = self.loaded_per_epoch_config_objects.read().clone();
446        let inner = self.into_inner(accumulator_running_max_withdraws);
447
448        let effects = TransactionEffects::new_from_execution_v2(
449            status,
450            epoch,
451            gas_cost_summary,
452            // TODO: Provide the list of read-only shared objects directly.
453            shared_object_refs,
454            loaded_per_epoch_config_objects,
455            *transaction_digest,
456            lamport_version,
457            object_changes,
458            gas_coin,
459            if inner.events.data.is_empty() {
460                None
461            } else {
462                Some(inner.events.digest())
463            },
464            transaction_dependencies.into_iter().collect(),
465        );
466
467        (inner, effects)
468    }
469
470    /// An internal check of the invariants (will only fire in debug)
471    #[cfg(debug_assertions)]
472    fn check_invariants(&self) {
473        // Check not both deleted and written
474        debug_assert!(
475            {
476                self.execution_results
477                    .written_objects
478                    .keys()
479                    .all(|id| !self.execution_results.deleted_object_ids.contains(id))
480            },
481            "Object both written and deleted."
482        );
483
484        // Check all mutable inputs are modified
485        debug_assert!(
486            {
487                self.mutable_input_refs
488                    .keys()
489                    .all(|id| self.execution_results.modified_objects.contains(id))
490            },
491            "Mutable input not modified."
492        );
493
494        debug_assert!(
495            {
496                self.execution_results
497                    .written_objects
498                    .values()
499                    .all(|obj| obj.previous_transaction == self.tx_digest)
500            },
501            "Object previous transaction not properly set",
502        );
503    }
504
505    /// Mutate a mutable input object. This is used to mutate input objects outside of PT execution.
506    pub fn mutate_input_object(&mut self, object: Object) {
507        let id = object.id();
508        debug_assert!(self.input_objects.contains_key(&id));
509        debug_assert!(!object.is_immutable());
510        self.execution_results.modified_objects.insert(id);
511        self.execution_results.written_objects.insert(id, object);
512    }
513
514    pub fn mutate_new_or_input_object(&mut self, object: Object) {
515        let id = object.id();
516        debug_assert!(!object.is_immutable());
517        if self.input_objects.contains_key(&id) {
518            self.execution_results.modified_objects.insert(id);
519        }
520        self.execution_results.written_objects.insert(id, object);
521    }
522
523    /// Mutate a child object outside of PT. This should be used extremely rarely.
524    /// Currently it's only used by advance_epoch_safe_mode because it's all native
525    /// without PT. This should almost never be used otherwise.
526    pub fn mutate_child_object(&mut self, old_object: Object, new_object: Object) {
527        let id = new_object.id();
528        let old_ref = old_object.compute_object_reference();
529        debug_assert_eq!(old_ref.0, id);
530        self.loaded_runtime_objects.insert(
531            id,
532            DynamicallyLoadedObjectMetadata {
533                version: old_ref.1,
534                digest: old_ref.2,
535                owner: old_object.owner.clone(),
536                storage_rebate: old_object.storage_rebate,
537                previous_transaction: old_object.previous_transaction,
538            },
539        );
540        self.execution_results.modified_objects.insert(id);
541        self.execution_results
542            .written_objects
543            .insert(id, new_object);
544    }
545
546    /// Upgrade system package during epoch change. This requires special treatment
547    /// since the system package to be upgraded is not in the input objects.
548    /// We could probably fix above to make it less special.
549    pub fn upgrade_system_package(&mut self, package: Object) {
550        let id = package.id();
551        assert!(package.is_package() && is_system_package(id));
552        self.execution_results.modified_objects.insert(id);
553        self.execution_results.written_objects.insert(id, package);
554    }
555
556    /// Crate a new objcet. This is used to create objects outside of PT execution.
557    pub fn create_object(&mut self, object: Object) {
558        // Created mutable objects' versions are set to the store's lamport timestamp when it is
559        // committed to effects. Creating an object at a non-zero version risks violating the
560        // lamport timestamp invariant (that a transaction's lamport timestamp is strictly greater
561        // than all versions witnessed by the transaction).
562        debug_assert!(
563            object.is_immutable() || object.version() == SequenceNumber::MIN,
564            "Created mutable objects should not have a version set",
565        );
566        let id = object.id();
567        self.execution_results.created_object_ids.insert(id);
568        self.execution_results.written_objects.insert(id, object);
569    }
570
571    /// Delete a mutable input object. This is used to delete input objects outside of PT execution.
572    pub fn delete_input_object(&mut self, id: &ObjectID) {
573        // there should be no deletion after write
574        debug_assert!(!self.execution_results.written_objects.contains_key(id));
575        debug_assert!(self.input_objects.contains_key(id));
576        self.execution_results.modified_objects.insert(*id);
577        self.execution_results.deleted_object_ids.insert(*id);
578    }
579
580    pub fn drop_writes(&mut self) {
581        self.execution_results.drop_writes();
582        // The PTB-emitted ranges pointed into the now-cleared accumulator_events vec.
583        self.ptb_emitted_accumulator_event_ranges.clear();
584    }
585
586    pub fn read_object(&self, id: &ObjectID) -> Option<&Object> {
587        // there should be no read after delete
588        debug_assert!(!self.execution_results.deleted_object_ids.contains(id));
589        self.execution_results
590            .written_objects
591            .get(id)
592            .or_else(|| self.input_objects.get(id))
593    }
594
595    pub fn save_loaded_runtime_objects(
596        &mut self,
597        loaded_runtime_objects: BTreeMap<ObjectID, DynamicallyLoadedObjectMetadata>,
598    ) {
599        #[cfg(debug_assertions)]
600        {
601            for (id, v1) in &loaded_runtime_objects {
602                if let Some(v2) = self.loaded_runtime_objects.get(id) {
603                    assert_eq!(v1, v2);
604                }
605            }
606            for (id, v1) in &self.loaded_runtime_objects {
607                if let Some(v2) = loaded_runtime_objects.get(id) {
608                    assert_eq!(v1, v2);
609                }
610            }
611        }
612        // Merge the two maps because we may be calling the execution engine more than once
613        // (e.g. in advance epoch transaction, where we may be publishing a new system package).
614        self.loaded_runtime_objects.extend(loaded_runtime_objects);
615    }
616
617    pub fn save_wrapped_object_containers(
618        &mut self,
619        wrapped_object_containers: BTreeMap<ObjectID, ObjectID>,
620    ) {
621        #[cfg(debug_assertions)]
622        {
623            for (id, container1) in &wrapped_object_containers {
624                if let Some(container2) = self.wrapped_object_containers.get(id) {
625                    assert_eq!(container1, container2);
626                }
627            }
628            for (id, container1) in &self.wrapped_object_containers {
629                if let Some(container2) = wrapped_object_containers.get(id) {
630                    assert_eq!(container1, container2);
631                }
632            }
633        }
634        // Merge the two maps because we may be calling the execution engine more than once
635        // (e.g. in advance epoch transaction, where we may be publishing a new system package).
636        self.wrapped_object_containers
637            .extend(wrapped_object_containers);
638    }
639
640    pub fn save_generated_object_ids(&mut self, generated_ids: BTreeSet<ObjectID>) {
641        #[cfg(debug_assertions)]
642        {
643            for id in &self.generated_runtime_ids {
644                assert!(!generated_ids.contains(id))
645            }
646            for id in &generated_ids {
647                assert!(!self.generated_runtime_ids.contains(id));
648            }
649        }
650        self.generated_runtime_ids.extend(generated_ids);
651    }
652
653    pub fn estimate_effects_size_upperbound(&self) -> usize {
654        TransactionEffects::estimate_effects_size_upperbound_v2(
655            self.execution_results.written_objects.len(),
656            self.execution_results.modified_objects.len(),
657            self.input_objects.len(),
658        )
659    }
660
661    pub fn written_objects_size(&self) -> usize {
662        self.execution_results
663            .written_objects
664            .values()
665            .fold(0, |sum, obj| sum + obj.object_size_for_gas_metering())
666    }
667
668    /// Validates gasless post-execution invariants:
669    /// - No new objects were created or existing objects mutated (written_objects is empty)
670    /// - The set of deleted objects exactly equals the set of input Coin objects
671    /// - Each recipient receives at least the minimum transfer amount per token type
672    /// - Unused withdrawal reservation (reservation - actual split) is 0 or >= min_amount
673    pub fn check_gasless_execution_requirements(
674        &self,
675        withdrawal_reservations: Option<&BTreeMap<(SuiAddress, TypeTag), u64>>,
676    ) -> Result<(), String> {
677        if !self.execution_results.written_objects.is_empty() {
678            return Err("Gasless transactions cannot create or mutate objects".to_string());
679        }
680
681        let input_coin_ids: BTreeSet<ObjectID> = self
682            .input_objects
683            .iter()
684            .filter(|(_, obj)| obj.coin_type_maybe().is_some())
685            .map(|(id, _)| *id)
686            .collect();
687        if self.execution_results.deleted_object_ids != input_coin_ids {
688            return Err(format!(
689                "Gasless transaction must destroy exactly its input Coins. \
690                 Expected: {input_coin_ids:?}, deleted: {:?}",
691                self.execution_results.deleted_object_ids
692            ));
693        }
694
695        let allowed_types =
696            sui_types::transaction::get_gasless_allowed_token_types(self.protocol_config);
697
698        // Aggregate signed balance changes per (address, token_type).
699        // Positive nets are recipient deposits that must meet the minimum transfer amount.
700        let net_totals = sui_types::balance_change::signed_balance_changes_from_events(
701            &self.execution_results.accumulator_events,
702        )
703        .fold(
704            BTreeMap::<(SuiAddress, TypeTag), i128>::new(),
705            |mut totals, (address, token_type, signed_amount)| {
706                *totals.entry((address, token_type)).or_default() += signed_amount;
707                totals
708            },
709        );
710
711        for ((recipient, token_type), net_amount) in &net_totals {
712            if *net_amount <= 0 {
713                continue;
714            }
715            if let Some(&min_amount) = allowed_types.get(token_type)
716                && *net_amount < i128::from(min_amount)
717            {
718                return Err(format!(
719                    "Gasless transfer of {net_amount} to {recipient} is below \
720                     minimum {min_amount} for token type {token_type}"
721                ));
722            }
723        }
724
725        if let Some(reservations) = withdrawal_reservations {
726            for ((owner, token_type), &reserved) in reservations {
727                let net = net_totals
728                    .get(&(*owner, token_type.clone()))
729                    .copied()
730                    .unwrap_or(0);
731                let remaining = (reserved as i128).saturating_add(net);
732                if remaining > 0
733                    && let Some(&min_balance_remaining) = allowed_types.get(token_type)
734                    && min_balance_remaining > 0
735                    && remaining < min_balance_remaining as i128
736                {
737                    return Err(format!(
738                        "Gasless withdrawal leaves {remaining} unused for {owner}, \
739                         below minimum {min_balance_remaining} for token type {token_type}"
740                    ));
741                }
742            }
743        }
744
745        Ok(())
746    }
747
748    /// If there are unmetered storage rebate (due to system transaction), we put them into
749    /// the storage rebate of 0x5 object.
750    /// TODO: This will not work for potential future new system transactions if 0x5 is not in the input.
751    /// We should fix this.
752    pub fn conserve_unmetered_storage_rebate(&mut self, unmetered_storage_rebate: u64) {
753        if unmetered_storage_rebate == 0 {
754            // If unmetered_storage_rebate is 0, we are most likely executing the genesis transaction.
755            // And in that case we cannot mutate the 0x5 object because it's newly created.
756            // And there is no storage rebate that needs distribution anyway.
757            return;
758        }
759        tracing::debug!(
760            "Amount of unmetered storage rebate from system tx: {:?}",
761            unmetered_storage_rebate
762        );
763        let mut system_state_wrapper = self
764            .read_object(&SUI_SYSTEM_STATE_OBJECT_ID)
765            .expect("0x5 object must be mutated in system tx with unmetered storage rebate")
766            .clone();
767        // In unmetered execution, storage_rebate field of mutated object must be 0.
768        // If not, we would be dropping SUI on the floor by overriding it.
769        assert_eq!(system_state_wrapper.storage_rebate, 0);
770        system_state_wrapper.storage_rebate = unmetered_storage_rebate;
771        self.mutate_input_object(system_state_wrapper);
772    }
773
774    /// Add an accumulator event to the execution results.
775    pub fn add_accumulator_event(&mut self, event: AccumulatorEvent) {
776        self.execution_results.accumulator_events.push(event);
777    }
778
779    /// Given an object ID, if it's not modified, returns None.
780    /// Otherwise returns its metadata, including version, digest, owner and storage rebate.
781    /// A modified object must be either a mutable input, or a loaded child object.
782    /// The only exception is when we upgrade system packages, in which case the upgraded
783    /// system packages are not part of input, but are modified.
784    fn get_object_modified_at(
785        &self,
786        object_id: &ObjectID,
787    ) -> Option<DynamicallyLoadedObjectMetadata> {
788        if self.execution_results.modified_objects.contains(object_id) {
789            Some(
790                self.mutable_input_refs
791                    .get(object_id)
792                    .map(
793                        |((version, digest), owner)| DynamicallyLoadedObjectMetadata {
794                            version: *version,
795                            digest: *digest,
796                            owner: owner.clone(),
797                            // It's guaranteed that a mutable input object is an input object.
798                            storage_rebate: self.input_objects[object_id].storage_rebate,
799                            previous_transaction: self.input_objects[object_id]
800                                .previous_transaction,
801                        },
802                    )
803                    .or_else(|| self.loaded_runtime_objects.get(object_id).cloned())
804                    .unwrap_or_else(|| {
805                        debug_assert!(is_system_package(*object_id));
806                        let package_obj =
807                            self.store.get_package_object(object_id).unwrap().unwrap();
808                        let obj = package_obj.object();
809                        DynamicallyLoadedObjectMetadata {
810                            version: obj.version(),
811                            digest: obj.digest(),
812                            owner: obj.owner.clone(),
813                            storage_rebate: obj.storage_rebate,
814                            previous_transaction: obj.previous_transaction,
815                        }
816                    }),
817            )
818        } else {
819            None
820        }
821    }
822
823    pub fn protocol_config(&self) -> &'backing ProtocolConfig {
824        self.protocol_config
825    }
826}
827
828impl TemporaryStore<'_> {
829    // check that every object read is owned directly or indirectly by sender, sponsor,
830    // or a shared object input
831    pub fn check_ownership_invariants(
832        &self,
833        sender: &SuiAddress,
834        sponsor: &Option<SuiAddress>,
835        gas_charger: &GasCharger,
836        mutable_inputs: &HashSet<ObjectID>,
837        input_reservations: &BTreeMap<(SuiAddress, TypeTag), u64>,
838        is_epoch_change: bool,
839    ) -> SuiResult<()> {
840        let gas_objs: HashSet<&ObjectID> = gas_charger.used_coins().map(|g| &g.0).collect();
841        let gas_owner = sponsor.as_ref().unwrap_or(sender);
842
843        // mark input objects as authenticated
844        let objects_authenticated_for_mutation: HashSet<SuiAddress> = self
845            .input_objects
846            .iter()
847            .filter_map(|(id, obj)| {
848                match &obj.owner {
849                    Owner::AddressOwner(a) => {
850                        if gas_objs.contains(id) {
851                            // gas object must be owned by sender or sponsor
852                            assert!(
853                                a == gas_owner,
854                                "Gas object must be owned by sender or sponsor"
855                            );
856                        } else {
857                            assert!(sender == a, "Input object must be owned by sender");
858                        }
859                        Some(id)
860                    }
861                    Owner::Shared { .. } | Owner::ConsensusAddressOwner { .. } => Some(id),
862                    Owner::Immutable => {
863                        // object is authenticated, but it cannot own other objects,
864                        // so we should not add it to `authenticated_objs`
865                        // However, we would definitely want to add immutable objects
866                        // to the set of authenticated roots if we were doing runtime
867                        // checks inside the VM instead of after-the-fact in the temporary
868                        // store. Here, we choose not to add them because this will catch a
869                        // bug where we mutate or delete an object that belongs to an immutable
870                        // object (though it will show up somewhat opaquely as an authentication
871                        // failure), whereas adding the immutable object to the roots will prevent
872                        // us from catching this.
873                        None
874                    }
875                    Owner::Party { permissions, .. } => {
876                        let sender_permissions = permissions.permissions_for(sender);
877                        let sponsor_permissions = sponsor
878                            .as_ref()
879                            .map(|s| permissions.permissions_for(s))
880                            .unwrap_or(ObjectPermissions::NONE);
881                        (sender_permissions | sponsor_permissions)
882                            .can_use_mutably()
883                            .then_some(id)
884                    }
885                    Owner::ObjectOwner(_parent) => {
886                        unreachable!(
887                            "Input objects must be address owned, shared, consensus, or immutable"
888                        )
889                    }
890                }
891            })
892            .filter(|id| {
893                // remove any non-mutable inputs. This will remove deleted or readonly shared
894                // objects
895                mutable_inputs.contains(id)
896            })
897            .copied()
898            // Add any object IDs generated in the object runtime during execution to the
899            // authenticated set (i.e., new (non-package) objects, and possibly ephemeral UIDs).
900            .chain(self.generated_runtime_ids.iter().copied())
901            .map(SuiAddress::from)
902            .collect();
903
904        // Add sender and sponsor (if present) to authenticated set
905        let mut authenticated_for_mutation = {
906            assert!(
907                !objects_authenticated_for_mutation.contains(sender),
908                "Sender cannot be an object"
909            );
910            assert!(
911                sponsor
912                    .is_none_or(|sponsor| !objects_authenticated_for_mutation.contains(&sponsor)),
913                "Sponsor cannot be an object"
914            );
915            let mut s = objects_authenticated_for_mutation.clone();
916            s.insert(*sender);
917            if let Some(sponsor) = sponsor {
918                s.insert(*sponsor);
919            }
920            s
921        };
922
923        // check all modified objects are authenticated
924        let mut objects_to_authenticate = self
925            .execution_results
926            .modified_objects
927            .iter()
928            .copied()
929            .collect::<Vec<_>>();
930
931        while let Some(to_authenticate) = objects_to_authenticate.pop() {
932            if authenticated_for_mutation.contains(&to_authenticate.into()) {
933                // object has already been authenticated
934                continue;
935            }
936
937            let parent = if let Some(container_id) =
938                self.wrapped_object_containers.get(&to_authenticate)
939            {
940                // It's a wrapped object, so check that the container is authenticated
941                *container_id
942            } else {
943                // It's non-wrapped, so check the owner -- we can load the object from the
944                // store.
945                let Some(old_obj) = self.store.get_object(&to_authenticate) else {
946                    panic!(
947                        "Failed to load object {to_authenticate:?}.\n \
948                         If it cannot be loaded, we would expect it to be in the wrapped object map: {:#?}",
949                        &self.wrapped_object_containers
950                    )
951                };
952
953                match &old_obj.owner {
954                    // We mutated a dynamic field, we can continue to trace this back to verify
955                    // proper ownership.
956                    Owner::ObjectOwner(parent) => ObjectID::from(*parent),
957                    // We mutated an address owned or sequenced address owned object -- one of two cases apply:
958                    // 1) the object is owned by an object or address in the authenticated set,
959                    // 2) the object is owned by some other address, in which case we should
960                    //    continue to trace this back.
961                    Owner::AddressOwner(parent)
962                    | Owner::ConsensusAddressOwner { owner: parent, .. } => {
963                        // For Receiving<_> objects, the address owner is actually an object.
964                        // If it was actually an address, we should have caught it as an input and
965                        // it would already have been in authenticated_for_mutation
966                        ObjectID::from(*parent)
967                    }
968                    // We mutated a shared object -- we checked if this object was in the
969                    // authenticated set at the top of this loop and it wasn't so this is a failure.
970                    owner @ Owner::Shared { .. } | owner @ Owner::Party { .. } => {
971                        panic!(
972                            "Unauthenticated root at {to_authenticate:?} with owner {owner:?}\n\
973                             Potentially covering objects in: {authenticated_for_mutation:#?}"
974                        );
975                    }
976
977                    Owner::Immutable => {
978                        assert!(
979                            is_epoch_change,
980                            "Immutable objects cannot be written, except for \
981                             Sui Framework/Move stdlib upgrades at epoch change boundaries"
982                        );
983                        // Note: this assumes that the only immutable objects an epoch change
984                        // tx can update are system packages,
985                        // but in principle we could allow others.
986                        assert!(
987                            is_system_package(to_authenticate),
988                            "Only system packages can be upgraded"
989                        );
990                        continue;
991                    }
992                }
993            };
994
995            // we now assume the object is authenticated and check the parent
996            authenticated_for_mutation.insert(to_authenticate.into());
997            objects_to_authenticate.push(parent);
998        }
999
1000        // Check that all funds accumulator splits are authorized
1001        let sui_balance_type =
1002            sui_types::balance::Balance::type_tag(sui_types::gas_coin::GAS::type_tag());
1003        let gas_payment_address_balance =
1004            gas_charger
1005                .gas_payment_location()
1006                .and_then(|location| match location {
1007                    PaymentLocation::Coin(_) => None,
1008                    PaymentLocation::AddressBalance(address) => Some(address),
1009                });
1010        let mut funds_net_changes: BTreeMap<(SuiAddress, TypeTag), i128> = BTreeMap::new();
1011        for event in self.execution_results.accumulator_events.iter() {
1012            let amount = match event.write.value {
1013                AccumulatorValue::Integer(a) => a as i128,
1014                AccumulatorValue::IntegerTuple(_, _) | AccumulatorValue::EventDigest(_) => {
1015                    assert!(
1016                        !sui_types::balance::Balance::is_balance_type(&event.write.address.ty),
1017                        "Non-integer accumulator changes should not be balances"
1018                    );
1019                    continue;
1020                }
1021            };
1022            let signed = match event.write.operation {
1023                AccumulatorOperation::Split => -amount,
1024                AccumulatorOperation::Merge => amount,
1025            };
1026            let address = event.write.address.address;
1027            let type_tag = &event.write.address.ty;
1028            let key = (address, type_tag.clone());
1029            *funds_net_changes.entry(key.clone()).or_insert(0) += signed;
1030            // Authorized if it is:
1031            // - A merge/deposit (anyone can deposit)
1032            // - A withdrawal
1033            //   - with a corresponding input reservation
1034            //   - from an object authenticated for mutation
1035            //   - for the gas payment (potentially from a GasCoin send_funds transfer)
1036            let authorized = match event.write.operation {
1037                AccumulatorOperation::Merge => true,
1038                AccumulatorOperation::Split => {
1039                    input_reservations.contains_key(&key)
1040                        || objects_authenticated_for_mutation.contains(&address)
1041                        || (*type_tag == sui_balance_type
1042                            && gas_payment_address_balance
1043                                .is_some_and(|gas_addr| gas_addr == address))
1044                }
1045            };
1046            assert!(
1047                authorized,
1048                "Unauthenticated funds-accumulator Split at address {address} for type \
1049                 {type_tag}: no input reservation, address is not an authenticated object, and \
1050                 it is not the final gas payment address balance"
1051            );
1052        }
1053
1054        // For all net negative changes (net withdrawals), the net changes _must_ be less than the
1055        // reservation amount, or it must be from an object. This excludes the final gas payment
1056        // address since the case where that withdrawal is allowed should be a net positive (or
1057        // zero) since it occurs only in the case where the gas coin is transferred via send_funds
1058        for (key, change) in funds_net_changes {
1059            // skip if deposit or for an object
1060            if change >= 0 || objects_authenticated_for_mutation.contains(&key.0) {
1061                continue;
1062            }
1063            let reservation = input_reservations.get(&key).copied().unwrap_or(0) as u128;
1064            let withdrawn = change.unsigned_abs();
1065            assert!(
1066                withdrawn <= reservation,
1067                "Net withdrawal of {withdrawn} for {key:?} exceeds input reservation of \
1068                 {reservation}"
1069            );
1070        }
1071
1072        Ok(())
1073    }
1074}
1075
1076impl TemporaryStore<'_> {
1077    /// Track storage gas for each mutable input object (including the gas coin)
1078    /// and each created object. Compute storage refunds for each deleted object.
1079    /// Will *not* charge anything, gas status keeps track of storage cost and rebate.
1080    /// All objects will be updated with their new (current) storage rebate/cost.
1081    /// `SuiGasStatus` `storage_rebate` and `storage_gas_units` track the transaction
1082    /// overall storage rebate and cost.
1083    pub(crate) fn collect_storage_and_rebate(&mut self, gas_charger: &mut GasCharger) {
1084        // Use two loops because we cannot mut iterate written while calling get_object_modified_at.
1085        let old_storage_rebates: Vec<_> = self
1086            .execution_results
1087            .written_objects
1088            .keys()
1089            .map(|object_id| {
1090                self.get_object_modified_at(object_id)
1091                    .map(|metadata| metadata.storage_rebate)
1092                    .unwrap_or_default()
1093            })
1094            .collect();
1095        for (object, old_storage_rebate) in self
1096            .execution_results
1097            .written_objects
1098            .values_mut()
1099            .zip_debug_eq(old_storage_rebates)
1100        {
1101            // new object size
1102            let new_object_size = object.object_size_for_gas_metering();
1103            // track changes and compute the new object `storage_rebate`
1104            let new_storage_rebate = gas_charger.track_storage_mutation(
1105                object.id(),
1106                new_object_size,
1107                old_storage_rebate,
1108            );
1109            object.storage_rebate = new_storage_rebate;
1110        }
1111
1112        self.collect_rebate(gas_charger);
1113    }
1114
1115    pub(crate) fn collect_rebate(&self, gas_charger: &mut GasCharger) {
1116        for object_id in &self.execution_results.modified_objects {
1117            if self
1118                .execution_results
1119                .written_objects
1120                .contains_key(object_id)
1121            {
1122                continue;
1123            }
1124            // get and track the deleted object `storage_rebate`
1125            let storage_rebate = self
1126                .get_object_modified_at(object_id)
1127                // Unwrap is safe because this loop iterates through all modified objects.
1128                .unwrap()
1129                .storage_rebate;
1130            gas_charger.track_storage_mutation(*object_id, 0, storage_rebate);
1131        }
1132    }
1133
1134    pub fn check_execution_results_consistency<Mode: ExecutionMode>(
1135        &self,
1136    ) -> Result<(), Mode::Error> {
1137        assert_invariant!(
1138            self.execution_results
1139                .created_object_ids
1140                .iter()
1141                .all(|id| !self.execution_results.deleted_object_ids.contains(id)
1142                    && !self.execution_results.modified_objects.contains(id)),
1143            "Created object IDs cannot also be deleted or modified"
1144        );
1145        assert_invariant!(
1146            self.execution_results.modified_objects.iter().all(|id| {
1147                self.mutable_input_refs.contains_key(id)
1148                    || self.loaded_runtime_objects.contains_key(id)
1149                    || is_system_package(*id)
1150            }),
1151            "A modified object must be either a mutable input, a loaded child object, or a system package"
1152        );
1153        Ok(())
1154    }
1155}
1156//==============================================================================
1157// Charge gas current - end
1158//==============================================================================
1159
1160impl TemporaryStore<'_> {
1161    pub fn advance_epoch_safe_mode(
1162        &mut self,
1163        params: &AdvanceEpochParams,
1164        protocol_config: &ProtocolConfig,
1165    ) {
1166        let wrapper = get_sui_system_state_wrapper(self.store.as_object_store())
1167            .expect("System state wrapper object must exist");
1168        let (old_object, new_object) =
1169            wrapper.advance_epoch_safe_mode(params, self.store.as_object_store(), protocol_config);
1170        self.mutate_child_object(old_object, new_object);
1171    }
1172}
1173
1174type ModifiedObjectInfo<'a> = (
1175    ObjectID,
1176    // old object metadata, including version, digest, owner, and storage rebate.
1177    Option<DynamicallyLoadedObjectMetadata>,
1178    Option<&'a Object>,
1179);
1180
1181impl TemporaryStore<'_> {
1182    fn get_input_sui(
1183        &self,
1184        id: &ObjectID,
1185        expected_version: SequenceNumber,
1186        layout_resolver: &mut impl LayoutResolver,
1187    ) -> Result<u64, ExecutionError> {
1188        if let Some(obj) = self.input_objects.get(id) {
1189            // the assumption here is that if it is in the input objects must be the right one
1190            if obj.version() != expected_version {
1191                invariant_violation!(
1192                    "Version mismatching when resolving input object to check conservation--\
1193                     expected {}, got {}",
1194                    expected_version,
1195                    obj.version(),
1196                );
1197            }
1198            obj.get_total_sui(layout_resolver).map_err(|e| {
1199                make_invariant_violation!(
1200                    "Failed looking up input SUI in SUI conservation checking for input with \
1201                         type {:?}: {e:#?}",
1202                    obj.struct_tag(),
1203                )
1204            })
1205        } else {
1206            // not in input objects, must be a dynamic field
1207            let Some(obj) = self.store.get_object_by_key(id, expected_version) else {
1208                invariant_violation!(
1209                    "Failed looking up dynamic field {id} in SUI conservation checking"
1210                );
1211            };
1212            obj.get_total_sui(layout_resolver).map_err(|e| {
1213                make_invariant_violation!(
1214                    "Failed looking up input SUI in SUI conservation checking for type \
1215                         {:?}: {e:#?}",
1216                    obj.struct_tag(),
1217                )
1218            })
1219        }
1220    }
1221
1222    /// Return the list of all modified objects, for each object, returns
1223    /// - Object ID,
1224    /// - Input: If the object existed prior to this transaction, include their version and storage_rebate,
1225    /// - Output: If a new version of the object is written, include the new object.
1226    fn get_modified_objects(&self) -> Vec<ModifiedObjectInfo<'_>> {
1227        self.execution_results
1228            .modified_objects
1229            .iter()
1230            .map(|id| {
1231                let metadata = self.get_object_modified_at(id);
1232                let output = self.execution_results.written_objects.get(id);
1233                (*id, metadata, output)
1234            })
1235            .chain(
1236                self.execution_results
1237                    .written_objects
1238                    .iter()
1239                    .filter_map(|(id, object)| {
1240                        if self.execution_results.modified_objects.contains(id) {
1241                            None
1242                        } else {
1243                            Some((*id, None, Some(object)))
1244                        }
1245                    }),
1246            )
1247            .collect()
1248    }
1249
1250    /// Check that this transaction neither creates nor destroys SUI. This should hold for all txes
1251    /// except the epoch change tx, which mints staking rewards equal to the gas fees burned in the
1252    /// previous epoch.  Specifically, this checks two key invariants about storage
1253    /// fees and storage rebate:
1254    ///
1255    /// 1. all SUI in storage rebate fields of input objects should flow either to the transaction
1256    ///    storage rebate, or the transaction non-refundable storage rebate
1257    /// 2. all SUI charged for storage should flow into the storage rebate field of some output
1258    ///    object
1259    ///
1260    /// This function is intended to be called *after* we have charged for
1261    /// gas + applied the storage rebate to the gas object, but *before* we
1262    /// have updated object versions.
1263    pub fn check_sui_conserved(
1264        &self,
1265        simple_conservation_checks: bool,
1266        gas_summary: &GasCostSummary,
1267    ) -> Result<(), ExecutionError> {
1268        if !simple_conservation_checks {
1269            return Ok(());
1270        }
1271        // total amount of SUI in storage rebate of input objects
1272        let mut total_input_rebate = 0;
1273        // total amount of SUI in storage rebate of output objects
1274        let mut total_output_rebate = 0;
1275        for (_id, input, output) in self.get_modified_objects() {
1276            if let Some(input) = input {
1277                total_input_rebate += input.storage_rebate;
1278            }
1279            if let Some(object) = output {
1280                total_output_rebate += object.storage_rebate;
1281            }
1282        }
1283
1284        if gas_summary.storage_cost == 0 {
1285            // this condition is usually true when the transaction went OOG and no
1286            // gas is left for storage charges.
1287            // The storage cost has to be there at least for the gas coin which
1288            // will not be deleted even when going to 0.
1289            // However if the storage cost is 0 and if there is any object touched
1290            // or deleted the value in input must be equal to the output plus rebate and
1291            // non refundable.
1292            // Rebate and non refundable will be positive when there are object deleted
1293            // (gas smashing being the primary and possibly only example).
1294            // A more typical condition is for all storage charges in summary to be 0 and
1295            // then input and output must be the same value
1296            if total_input_rebate
1297                != total_output_rebate
1298                    + gas_summary.storage_rebate
1299                    + gas_summary.non_refundable_storage_fee
1300            {
1301                return Err(ExecutionError::invariant_violation(format!(
1302                    "SUI conservation failed -- no storage charges in gas summary \
1303                        and total storage input rebate {} not equal  \
1304                        to total storage output rebate {}",
1305                    total_input_rebate, total_output_rebate,
1306                )));
1307            }
1308        } else {
1309            // all SUI in storage rebate fields of input objects should flow either to
1310            // the transaction storage rebate, or the non-refundable storage rebate pool
1311            if total_input_rebate
1312                != gas_summary.storage_rebate + gas_summary.non_refundable_storage_fee
1313            {
1314                return Err(ExecutionError::invariant_violation(format!(
1315                    "SUI conservation failed -- {} SUI in storage rebate field of input objects, \
1316                        {} SUI in tx storage rebate or tx non-refundable storage rebate",
1317                    total_input_rebate, gas_summary.non_refundable_storage_fee,
1318                )));
1319            }
1320
1321            // all SUI charged for storage should flow into the storage rebate field
1322            // of some output object
1323            if gas_summary.storage_cost != total_output_rebate {
1324                return Err(ExecutionError::invariant_violation(format!(
1325                    "SUI conservation failed -- {} SUI charged for storage, \
1326                        {} SUI in storage rebate field of output objects",
1327                    gas_summary.storage_cost, total_output_rebate
1328                )));
1329            }
1330        }
1331        Ok(())
1332    }
1333
1334    /// Defense-in-depth invariant on funds-accumulator events. Per `(address, type)`:
1335    /// - If the pair is in `input_reservations`, net withdrawal <= budget.
1336    /// - Else if the PTB emitted a Split at this key, we assume there must be an object withdrawal.
1337    ///   As such, any net change is acceptable.
1338    /// - Else if the PTB emitted only Merges at this key, we can assume there might not be an
1339    ///   object withdrawal. In any case, the net balance at the end of the transaction should be
1340    ///   non-negative, since there could be additional withdrawals from gas, but they should
1341    ///   not exceed the deposits.
1342    /// - Else, any event is unauthorized.
1343    ///
1344    /// Currently the only funds-accumulator type is `Balance<T>`, so the check is scoped to
1345    /// those events. As more accumulator shapes are added the filter and the integer
1346    /// arithmetic in `check_address_balance_changes_impl` will need to grow with them.
1347    ///
1348    /// PTB-emitted events are identified via `ptb_emitted_accumulator_event_ranges`, populated
1349    /// at `record_execution_results` time. They are trusted because Move enforces `&mut UID`
1350    /// and the native checks the actual balance.
1351    ///
1352    /// `protocol_config.enforce_address_balance_change_invariant()` selects the failure mode:
1353    /// - On (post-flag): violations are returned as `Err` so the caller's
1354    ///   conservation-recovery flow can abort the tx cleanly.
1355    /// - Off (pre-flag): the check still runs, but a violation panics so unexpected
1356    ///   violations surface loudly during rollout.
1357    pub fn check_address_balance_changes(
1358        &self,
1359        _protocol_config: &ProtocolConfig,
1360        input_reservations: &BTreeMap<(SuiAddress, TypeTag), u64>,
1361    ) -> Result<(), ExecutionError> {
1362        use sui_types::balance::Balance;
1363
1364        let mut actual_changes: BTreeMap<(SuiAddress, TypeTag), i128> = BTreeMap::new();
1365        let mut has_ptb_withdrawals: BTreeSet<(SuiAddress, TypeTag)> = BTreeSet::new();
1366        let mut has_ptb_deposits: BTreeSet<(SuiAddress, TypeTag)> = BTreeSet::new();
1367        for (idx, event) in self.execution_results.accumulator_events.iter().enumerate() {
1368            // Filter on the value shape first: only `Integer` carries the funds-flow we care
1369            // about. Other shapes (e.g. `EventDigest` for event-stream heads) belong to
1370            // non-Balance accumulators and are out of scope here. If we ever see an `Integer`
1371            // value at a non-`Balance<T>` type, the accounting invariants below don't apply
1372            // -- debug_fatal so that case is surfaced instead of silently accepted.
1373            let amount = match event.write.value {
1374                AccumulatorValue::Integer(amount) => amount as i128,
1375                AccumulatorValue::IntegerTuple(_, _) | AccumulatorValue::EventDigest(_) => {
1376                    assert_invariant!(
1377                        !sui_types::balance::Balance::is_balance_type(&event.write.address.ty),
1378                        "Non-integer accumulator changes should not be balances"
1379                    );
1380                    continue;
1381                }
1382            };
1383            if !Balance::is_balance_type(&event.write.address.ty) {
1384                debug_fatal!(
1385                    "Integer accumulator value at non-Balance type: {:?}",
1386                    event.write.address.ty
1387                );
1388                continue;
1389            }
1390            let is_ptb_emitted = self
1391                .ptb_emitted_accumulator_event_ranges
1392                .iter()
1393                .any(|range| range.contains(&idx));
1394            let key = (event.write.address.address, event.write.address.ty.clone());
1395            let change = match event.write.operation {
1396                AccumulatorOperation::Split => {
1397                    if is_ptb_emitted {
1398                        has_ptb_withdrawals.insert(key.clone());
1399                    }
1400                    -amount
1401                }
1402                AccumulatorOperation::Merge => {
1403                    if is_ptb_emitted {
1404                        has_ptb_deposits.insert(key.clone());
1405                    }
1406                    amount
1407                }
1408            };
1409            *actual_changes.entry(key.clone()).or_insert(0) += change;
1410        }
1411
1412        for (key, actual) in actual_changes {
1413            let (address, type_tag) = &key;
1414            if let Some(budget) = input_reservations.get(&key).copied() {
1415                let net_withdrawn = -actual.min(0) as u128;
1416                assert_invariant!(
1417                    net_withdrawn <= budget as u128,
1418                    "Balance accumulator withdrawal exceeds reservation budget at address \
1419                    {address} for type {type_tag}: net Split {net_withdrawn}, budget {budget}"
1420                );
1421            } else if has_ptb_withdrawals.contains(&key) {
1422                // Move authorized the PTB Split against the on-chain balance, so any
1423                // resulting net (including a net withdrawal beyond any PTB Merges here)
1424                // is trusted.
1425            } else if has_ptb_deposits.contains(&key) {
1426                // PTB only deposited at this key. As such, the final net change must be
1427                // non-negative, since there was no authorization for any withdrawal.
1428                // We cannot compare this value to the sum of the PTB deposits due to intricacies
1429                // with gas charging and storage rebate.
1430                assert_invariant!(
1431                    actual >= 0,
1432                    "PTB-emitted Balance accumulator deposits do not cover the runtime \
1433                    withdrawal at address {address} for type {type_tag}: net change {actual}"
1434                );
1435            } else {
1436                invariant_violation!(
1437                    "Unauthorized runtime Balance accumulator event at address {address} for \
1438                    type {type_tag}: net change {actual} (no input reservation, no PTB-emitted \
1439                    events)"
1440                );
1441            }
1442        }
1443
1444        Ok(())
1445    }
1446
1447    /// Check that this transaction neither creates nor destroys SUI.
1448    /// This more expensive check will check a third invariant on top of the 2 performed
1449    /// by `check_sui_conserved` above:
1450    ///
1451    /// * all SUI in input objects (including coins etc in the Move part of an object) should flow
1452    ///   either to an output object, or be burned as part of computation fees or non-refundable
1453    ///   storage rebate
1454    ///
1455    /// This function is intended to be called *after* we have charged for gas + applied the
1456    /// storage rebate to the gas object, but *before* we have updated object versions. The
1457    /// advance epoch transaction would mint `epoch_fees` amount of SUI, and burn `epoch_rebates`
1458    /// amount of SUI. We need these information for this check.
1459    pub fn check_sui_conserved_expensive(
1460        &self,
1461        gas_summary: &GasCostSummary,
1462        advance_epoch_gas_summary: Option<(u64, u64)>,
1463        layout_resolver: &mut impl LayoutResolver,
1464    ) -> Result<(), ExecutionError> {
1465        // Accumulate in u128. The per-object SUI totals are bounded by the real supply, but the
1466        // accumulator-event terms below are not: an object-sourced withdrawal/deposit (backing
1467        // verified only at settlement) can contribute up to u64::MAX on each side, and a transaction
1468        // can stack several across distinct keys, so a u64 running total could overflow. These
1469        // amounts net out, so a u128 sum stays exact and conservation is decided correctly.
1470        // total amount of SUI in input objects, including both coins and storage rebates
1471        let mut total_input_sui: u128 = 0;
1472        // total amount of SUI in output objects, including both coins and storage rebates
1473        let mut total_output_sui: u128 = 0;
1474
1475        // settlement input/output sui is used by the settlement transactions to account for
1476        // Sui that has been gathered from the accumulator writes of transactions which it is
1477        // settling.
1478        total_input_sui += self.execution_results.settlement_input_sui as u128;
1479        total_output_sui += self.execution_results.settlement_output_sui as u128;
1480
1481        for (id, input, output) in self.get_modified_objects() {
1482            if let Some(input) = input {
1483                total_input_sui += self.get_input_sui(&id, input.version, layout_resolver)? as u128;
1484            }
1485            if let Some(object) = output {
1486                total_output_sui += object.get_total_sui(layout_resolver).map_err(|e| {
1487                    make_invariant_violation!(
1488                        "Failed looking up output SUI in SUI conservation checking for \
1489                         mutated type {:?}: {e:#?}",
1490                        object.struct_tag(),
1491                    )
1492                })? as u128;
1493            }
1494        }
1495
1496        for event in &self.execution_results.accumulator_events {
1497            let (input, output) = event.total_sui_in_event();
1498            total_input_sui += input as u128;
1499            total_output_sui += output as u128;
1500        }
1501
1502        // note: storage_cost flows into the storage_rebate field of the output objects, which is
1503        // why it is not accounted for here.
1504        // similarly, all of the storage_rebate *except* the storage_fund_rebate_inflow
1505        // gets credited to the gas coin both computation costs and storage rebate inflow are
1506        total_output_sui +=
1507            gas_summary.computation_cost as u128 + gas_summary.non_refundable_storage_fee as u128;
1508        if let Some((epoch_fees, epoch_rebates)) = advance_epoch_gas_summary {
1509            total_input_sui += epoch_fees as u128;
1510            total_output_sui += epoch_rebates as u128;
1511        }
1512        if total_input_sui != total_output_sui {
1513            return Err(ExecutionError::invariant_violation(format!(
1514                "SUI conservation failed: input={}, output={}, \
1515                    this transaction either mints or burns SUI",
1516                total_input_sui, total_output_sui,
1517            )));
1518        }
1519        Ok(())
1520    }
1521}
1522
1523impl ChildObjectResolver for TemporaryStore<'_> {
1524    fn read_child_object(
1525        &self,
1526        parent: &ObjectID,
1527        child: &ObjectID,
1528        child_version_upper_bound: SequenceNumber,
1529    ) -> SuiResult<Option<Object>> {
1530        let obj_opt = self.execution_results.written_objects.get(child);
1531        if obj_opt.is_some() {
1532            Ok(obj_opt.cloned())
1533        } else {
1534            let _scope = monitored_scope("Execution::read_child_object");
1535            self.store
1536                .read_child_object(parent, child, child_version_upper_bound)
1537        }
1538    }
1539
1540    fn get_object_received_at_version(
1541        &self,
1542        owner: &ObjectID,
1543        receiving_object_id: &ObjectID,
1544        receive_object_at_version: SequenceNumber,
1545        epoch_id: EpochId,
1546    ) -> SuiResult<Option<Object>> {
1547        // You should never be able to try and receive an object after deleting it or writing it in the same
1548        // transaction since `Receiving` doesn't have copy.
1549        debug_assert!(
1550            !self
1551                .execution_results
1552                .written_objects
1553                .contains_key(receiving_object_id)
1554        );
1555        debug_assert!(
1556            !self
1557                .execution_results
1558                .deleted_object_ids
1559                .contains(receiving_object_id)
1560        );
1561        self.store.get_object_received_at_version(
1562            owner,
1563            receiving_object_id,
1564            receive_object_at_version,
1565            epoch_id,
1566        )
1567    }
1568}
1569
1570/// Compares the owner and payload of an object.
1571/// This is used to detect illegal writes to non-exclusive write objects.
1572fn was_object_mutated(object: &Object, original: &Object) -> bool {
1573    let data_equal = match (&object.data, &original.data) {
1574        (Data::Move(a), Data::Move(b)) => a.contents_and_type_equal(b),
1575        // We don't have a use for package content-equality, so we remain as strict as
1576        // possible for now.
1577        (Data::Package(a), Data::Package(b)) => a == b,
1578        _ => false,
1579    };
1580
1581    let owner_equal = match (&object.owner, &original.owner) {
1582        // We don't compare initial shared versions, because re-shared objects do not have the
1583        // correct initial shared version at this point in time, and this field is not something
1584        // that can be modified by a single transaction anyway.
1585        (Owner::Shared { .. }, Owner::Shared { .. }) => true,
1586        (
1587            Owner::ConsensusAddressOwner { owner: a, .. },
1588            Owner::ConsensusAddressOwner { owner: b, .. },
1589        ) => a == b,
1590        (Owner::AddressOwner(a), Owner::AddressOwner(b)) => a == b,
1591        (Owner::Immutable, Owner::Immutable) => true,
1592        (Owner::ObjectOwner(a), Owner::ObjectOwner(b)) => a == b,
1593        (
1594            Owner::Party {
1595                permissions: a,
1596                start_version: _,
1597            },
1598            Owner::Party {
1599                permissions: b,
1600                start_version: _,
1601            },
1602        ) => a == b,
1603
1604        // Keep the left hand side of the match exhaustive to catch future
1605        // changes to Owner
1606        (Owner::AddressOwner(_), _)
1607        | (Owner::Immutable, _)
1608        | (Owner::ObjectOwner(_), _)
1609        | (Owner::Shared { .. }, _)
1610        | (Owner::ConsensusAddressOwner { .. }, _)
1611        | (Owner::Party { .. }, _) => false,
1612    };
1613
1614    !data_equal || !owner_equal
1615}
1616
1617impl Storage for TemporaryStore<'_> {
1618    fn reset(&mut self) {
1619        self.drop_writes();
1620    }
1621
1622    fn read_object(&self, id: &ObjectID) -> Option<&Object> {
1623        TemporaryStore::read_object(self, id)
1624    }
1625
1626    /// Take execution results v2, and translate it back to be compatible with effects v1.
1627    fn record_execution_results(
1628        &mut self,
1629        results: ExecutionResults,
1630    ) -> Result<(), ExecutionError> {
1631        let ExecutionResults::V2(mut results) = results else {
1632            panic!("ExecutionResults::V2 expected in sui-execution v1 and above");
1633        };
1634
1635        // for all non-exclusive write inputs, remove them from written objects
1636        let mut to_remove = Vec::new();
1637        for (id, original) in &self.non_exclusive_input_original_versions {
1638            // Object must be present in `written_objects` and identical
1639            if results
1640                .written_objects
1641                .get(id)
1642                .map(|obj| was_object_mutated(obj, original))
1643                .unwrap_or(true)
1644            {
1645                return Err(ExecutionError::new_with_source(
1646                    ExecutionErrorKind::NonExclusiveWriteInputObjectModified { id: *id },
1647                    "Non-exclusive write input object has been modified or deleted",
1648                ));
1649            }
1650            to_remove.push(*id);
1651        }
1652
1653        for id in to_remove {
1654            results.written_objects.remove(&id);
1655            results.modified_objects.remove(&id);
1656        }
1657
1658        // It's important to merge instead of override results because it's
1659        // possible to execute PT more than once during tx execution.
1660        // Track the index range of accumulator events brought in here as PTB-emitted; the
1661        // address-balance change invariant (run inside `run_conservation_checks`) uses this
1662        // set to distinguish trusted PTB-emitted events from runtime-emitted ones.
1663        let event_start = self.execution_results.accumulator_events.len();
1664        self.execution_results.merge_results(
1665            results, /* consistent_merge */ true, /* invariant_checks */ true,
1666        )?;
1667        let event_end = self.execution_results.accumulator_events.len();
1668        debug_assert!(
1669            event_start <= event_end,
1670            "merge_results should not shrink accumulator_events"
1671        );
1672        let (event_start, event_end) = (event_start.min(event_end), event_start.max(event_end));
1673        let range = event_start..event_end;
1674        match self.ptb_emitted_accumulator_event_ranges.last_mut() {
1675            // Coalesce with the previous PTB range if no runtime events were added in between.
1676            Some(last) if last.end == range.start => last.end = range.end,
1677            _ => self.ptb_emitted_accumulator_event_ranges.push(range),
1678        }
1679
1680        Ok(())
1681    }
1682
1683    fn save_loaded_runtime_objects(
1684        &mut self,
1685        loaded_runtime_objects: BTreeMap<ObjectID, DynamicallyLoadedObjectMetadata>,
1686    ) {
1687        TemporaryStore::save_loaded_runtime_objects(self, loaded_runtime_objects)
1688    }
1689
1690    fn save_wrapped_object_containers(
1691        &mut self,
1692        wrapped_object_containers: BTreeMap<ObjectID, ObjectID>,
1693    ) {
1694        TemporaryStore::save_wrapped_object_containers(self, wrapped_object_containers)
1695    }
1696
1697    fn check_coin_deny_list(
1698        &self,
1699        receiving_funds_type_and_owners: BTreeMap<TypeTag, BTreeSet<SuiAddress>>,
1700    ) -> DenyListResult {
1701        let result = check_coin_deny_list_v2_during_execution(
1702            receiving_funds_type_and_owners,
1703            self.cur_epoch,
1704            self.store.as_object_store(),
1705        );
1706        // The denylist object is only loaded if there are regulated transfers.
1707        // And also if we already have it in the input there is no need to commit it again in the effects.
1708        if result.num_non_gas_coin_owners > 0
1709            && !self.input_objects.contains_key(&SUI_DENY_LIST_OBJECT_ID)
1710        {
1711            self.loaded_per_epoch_config_objects
1712                .write()
1713                .insert(SUI_DENY_LIST_OBJECT_ID);
1714        }
1715        result
1716    }
1717
1718    fn record_generated_object_ids(&mut self, generated_ids: BTreeSet<ObjectID>) {
1719        TemporaryStore::save_generated_object_ids(self, generated_ids)
1720    }
1721}
1722
1723impl BackingPackageStore for TemporaryStore<'_> {
1724    fn get_package_object(&self, package_id: &ObjectID) -> SuiResult<Option<PackageObject>> {
1725        // We first check the objects in the temporary store because in non-production code path,
1726        // it is possible to read packages that are just written in the same transaction.
1727        // This can happen for example when we run the expensive conservation checks, where we may
1728        // look into the types of each written object in the output, and some of them need the
1729        // newly written packages for type checking.
1730        // In production path though, this should never happen.
1731        if let Some(obj) = self.execution_results.written_objects.get(package_id) {
1732            Ok(Some(PackageObject::new(obj.clone())))
1733        } else {
1734            self.store.get_package_object(package_id).inspect(|obj| {
1735                // Track object but leave unchanged
1736                if let Some(v) = obj
1737                    && !self
1738                        .runtime_packages_loaded_from_db
1739                        .read()
1740                        .contains_key(package_id)
1741                {
1742                    // TODO: Can this lock ever block execution?
1743                    // TODO: Another way to avoid the cost of maintaining this map is to not
1744                    // enable it in normal runs, and if a fork is detected, rerun it with a flag
1745                    // turned on and start populating this field.
1746                    self.runtime_packages_loaded_from_db
1747                        .write()
1748                        .insert(*package_id, v.clone());
1749                }
1750            })
1751        }
1752    }
1753}
1754
1755impl ParentSync for TemporaryStore<'_> {
1756    fn get_latest_parent_entry_ref_deprecated(&self, _object_id: ObjectID) -> Option<ObjectRef> {
1757        unreachable!("Never called in newer protocol versions")
1758    }
1759}