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