1use crate::execution_mode::ExecutionMode;
5use crate::gas_charger::GasCharger;
6use move_vm_runtime::runtime::MoveRuntime;
7use mysten_common::ZipDebugEqIteratorExt;
8use mysten_metrics::monitored_scope;
9use parking_lot::RwLock;
10use std::collections::{BTreeMap, BTreeSet, HashSet};
11use std::sync::Arc;
12use sui_protocol_config::ProtocolConfig;
13use sui_types::accumulator_event::AccumulatorEvent;
14use sui_types::accumulator_root::AccumulatorObjId;
15use sui_types::base_types::VersionDigest;
16use sui_types::coin_reservation::ParsedDigest;
17use sui_types::committee::EpochId;
18use sui_types::deny_list_v2::check_coin_deny_list_v2_during_execution;
19use sui_types::effects::{
20 AccumulatorOperation, AccumulatorValue, AccumulatorWriteV1, TransactionEffects,
21 TransactionEffectsV2, TransactionEvents,
22};
23use sui_types::execution::{
24 DynamicallyLoadedObjectMetadata, ExecutionResults, ExecutionResultsV2, SharedInput,
25};
26use sui_types::execution_status::{ExecutionErrorKind, ExecutionStatus};
27use sui_types::inner_temporary_store::InnerTemporaryStore;
28use sui_types::object::Data;
29use sui_types::storage::{BackingStore, DenyListResult, PackageObject};
30use sui_types::sui_system_state::{AdvanceEpochParams, get_sui_system_state_wrapper};
31use sui_types::transaction::{Command, GasData, TransactionKind, is_gasless_transaction};
32use sui_types::{
33 SUI_DENY_LIST_OBJECT_ID,
34 base_types::{ObjectID, ObjectRef, SequenceNumber, SuiAddress, TransactionDigest},
35 effects::EffectsObjectChange,
36 error::{ExecutionError, SuiResult},
37 gas::GasCostSummary,
38 object::Object,
39 object::Owner,
40 storage::{BackingPackageStore, RuntimeObjectResolver, Storage},
41 transaction::InputObjects,
42};
43use sui_types::{SUI_SYSTEM_STATE_OBJECT_ID, TypeTag, is_system_package};
44
45pub(crate) mod invariants;
46use invariants::InvariantChecker;
47
48#[derive(Default)]
49struct PostExecutionCheckInputs {
50 input_reservations: BTreeMap<(SuiAddress, TypeTag), u64>,
53 advance_epoch_gas_summary: Option<(u64, u64)>,
56 is_genesis: bool,
58 declared_packages: Option<Vec<(usize, BTreeSet<ObjectID>)>>,
61}
62
63impl PostExecutionCheckInputs {
64 fn new(transaction: (&TransactionKind, &GasData, SuiAddress), enable_gasless: bool) -> Self {
65 let (transaction_kind, gas_data, transaction_signer) = transaction;
66 Self {
67 input_reservations: compute_input_reservations(
68 transaction_kind,
69 gas_data,
70 transaction_signer,
71 enable_gasless,
72 ),
73 advance_epoch_gas_summary: transaction_kind.get_advance_epoch_tx_gas_summary(),
74 is_genesis: matches!(transaction_kind, TransactionKind::Genesis(_)),
75 declared_packages: declared_packages(transaction_kind),
76 }
77 }
78}
79
80pub struct TemporaryStore<'backing> {
81 store: &'backing dyn BackingStore,
87 tx_digest: TransactionDigest,
88 input_objects: BTreeMap<ObjectID, Object>,
89 post_execution_check_inputs: PostExecutionCheckInputs,
92
93 non_exclusive_input_original_versions: BTreeMap<ObjectID, Object>,
96
97 stream_ended_consensus_objects: BTreeMap<ObjectID, SequenceNumber >,
98 lamport_timestamp: SequenceNumber,
100 mutable_input_refs: BTreeMap<ObjectID, (VersionDigest, Owner)>,
103 execution_results: ExecutionResultsV2,
104 loaded_runtime_objects: BTreeMap<ObjectID, DynamicallyLoadedObjectMetadata>,
106 protocol_config: &'backing ProtocolConfig,
107
108 runtime_packages_loaded_from_db: RwLock<BTreeMap<ObjectID, PackageObject>>,
111
112 receiving_objects: Vec<ObjectRef>,
115
116 cur_epoch: EpochId,
119
120 loaded_per_epoch_config_objects: RwLock<BTreeSet<ObjectID>>,
123
124 invariants: InvariantChecker,
126}
127
128impl<'backing> TemporaryStore<'backing> {
129 #[allow(clippy::too_many_arguments)]
132 pub(crate) fn new(
133 store: &'backing dyn BackingStore,
134 input_objects: InputObjects,
135 receiving_objects: Vec<ObjectRef>,
136 tx_digest: TransactionDigest,
137 protocol_config: &'backing ProtocolConfig,
138 cur_epoch: EpochId,
139 _system_object_versions: BTreeMap<ObjectID, SequenceNumber>,
140 transaction: (&TransactionKind, &GasData, SuiAddress),
141 ) -> Self {
142 let post_execution_check_inputs =
143 PostExecutionCheckInputs::new(transaction, protocol_config.enable_gasless());
144 Self::new_with_input_objects(
145 store,
146 input_objects,
147 receiving_objects,
148 tx_digest,
149 protocol_config,
150 cur_epoch,
151 post_execution_check_inputs,
152 )
153 }
154
155 pub(crate) fn new_for_genesis_state_update(
156 store: &'backing dyn BackingStore,
157 tx_digest: TransactionDigest,
158 protocol_config: &'backing ProtocolConfig,
159 ) -> Self {
160 Self::new_with_input_objects(
161 store,
162 InputObjects::new(vec![]),
163 vec![],
164 tx_digest,
165 protocol_config,
166 0,
167 PostExecutionCheckInputs {
168 is_genesis: true,
169 ..Default::default()
170 },
171 )
172 }
173
174 fn new_with_input_objects(
175 store: &'backing dyn BackingStore,
176 input_objects: InputObjects,
177 receiving_objects: Vec<ObjectRef>,
178 tx_digest: TransactionDigest,
179 protocol_config: &'backing ProtocolConfig,
180 cur_epoch: EpochId,
181 post_execution_check_inputs: PostExecutionCheckInputs,
182 ) -> Self {
183 let mutable_input_refs = input_objects.exclusive_mutable_inputs();
184 let non_exclusive_input_original_versions = input_objects.non_exclusive_input_objects();
185
186 let lamport_timestamp = input_objects.lamport_timestamp(&receiving_objects);
187 let stream_ended_consensus_objects = input_objects.consensus_stream_ended_objects();
188 let objects = input_objects.into_object_map();
189 #[cfg(debug_assertions)]
190 {
191 assert!(
193 objects
194 .keys()
195 .collect::<HashSet<_>>()
196 .intersection(
197 &receiving_objects
198 .iter()
199 .map(|oref| &oref.0)
200 .collect::<HashSet<_>>()
201 )
202 .next()
203 .is_none()
204 );
205 }
206 Self {
207 store,
208 tx_digest,
209 input_objects: objects,
210 non_exclusive_input_original_versions,
211 stream_ended_consensus_objects,
212 lamport_timestamp,
213 mutable_input_refs,
214 execution_results: ExecutionResultsV2::default(),
215 protocol_config,
216 loaded_runtime_objects: BTreeMap::new(),
217 runtime_packages_loaded_from_db: RwLock::new(BTreeMap::new()),
218 receiving_objects,
219 cur_epoch,
220 loaded_per_epoch_config_objects: RwLock::new(BTreeSet::new()),
221 post_execution_check_inputs,
222 invariants: InvariantChecker::default(),
223 }
224 }
225
226 pub fn objects(&self) -> &BTreeMap<ObjectID, Object> {
228 &self.input_objects
229 }
230
231 pub fn update_object_version_and_prev_tx(&mut self) {
232 self.execution_results.update_version_and_previous_tx(
233 self.lamport_timestamp,
234 self.tx_digest,
235 &self.input_objects,
236 self.protocol_config.reshare_at_same_initial_version(),
237 );
238
239 #[cfg(debug_assertions)]
240 {
241 self.check_invariants();
242 }
243 }
244
245 fn calculate_accumulator_running_max_withdraws(&self) -> BTreeMap<AccumulatorObjId, u128> {
246 let mut running_net_withdraws: BTreeMap<AccumulatorObjId, i128> = BTreeMap::new();
247 let mut running_max_withdraws: BTreeMap<AccumulatorObjId, u128> = BTreeMap::new();
248 for event in &self.execution_results.accumulator_events {
249 match &event.write.value {
250 AccumulatorValue::Integer(amount) => match event.write.operation {
251 AccumulatorOperation::Split => {
252 let entry = running_net_withdraws
253 .entry(event.accumulator_obj)
254 .or_default();
255 *entry += *amount as i128;
256 if *entry > 0 {
257 let max_entry = running_max_withdraws
258 .entry(event.accumulator_obj)
259 .or_default();
260 *max_entry = (*max_entry).max(*entry as u128);
261 }
262 }
263 AccumulatorOperation::Merge => {
264 let entry = running_net_withdraws
265 .entry(event.accumulator_obj)
266 .or_default();
267 *entry -= *amount as i128;
268 }
269 },
270 AccumulatorValue::IntegerTuple(_, _) | AccumulatorValue::EventDigest(_) => {}
271 }
272 }
273 running_max_withdraws
274 }
275
276 pub(crate) fn check_accumulator_amounts_representable(&self) -> Result<(), ExecutionError> {
304 let supply = sui_types::gas_coin::TOTAL_SUPPLY_MIST as u128;
305 let mut merge_totals: BTreeMap<AccumulatorObjId, u128> = BTreeMap::new();
306 let mut split_totals: BTreeMap<AccumulatorObjId, u128> = BTreeMap::new();
307 let mut total_sui_split: u128 = 0;
309 for event in &self.execution_results.accumulator_events {
310 let AccumulatorValue::Integer(amount) = event.write.value else {
311 continue;
312 };
313 let amount = amount as u128;
314 let is_sui = sui_types::gas_coin::GasCoin::is_gas_balance_type(&event.write.address.ty);
317 let limit = if is_sui { supply } else { u64::MAX as u128 };
318 let total = match event.write.operation {
319 AccumulatorOperation::Merge => {
320 merge_totals.entry(event.accumulator_obj).or_default()
321 }
322 AccumulatorOperation::Split => {
323 split_totals.entry(event.accumulator_obj).or_default()
324 }
325 };
326 *total += amount;
327 if *total > limit {
328 return Err(ExecutionError::new_with_source(
329 ExecutionErrorKind::CoinBalanceOverflow,
330 format!(
331 "accumulator balance change for {:?} exceeds the representable limit \
332 (gross total {}, limit {})",
333 event.accumulator_obj, *total, limit
334 ),
335 ));
336 }
337 if is_sui && matches!(event.write.operation, AccumulatorOperation::Split) {
338 total_sui_split += amount;
339 if total_sui_split > supply {
340 return Err(ExecutionError::new_with_source(
341 ExecutionErrorKind::CoinBalanceOverflow,
342 format!(
343 "total SUI withdrawn across all accumulators ({total_sui_split}) \
344 exceeds the total supply ({supply})"
345 ),
346 ));
347 }
348 }
349 }
350 Ok(())
351 }
352
353 fn merge_accumulator_events(&mut self) {
355 self.execution_results.accumulator_events = self
356 .execution_results
357 .accumulator_events
358 .iter()
359 .fold(
360 BTreeMap::<AccumulatorObjId, Vec<AccumulatorWriteV1>>::new(),
361 |mut map, event| {
362 map.entry(event.accumulator_obj)
363 .or_default()
364 .push(event.write.clone());
365 map
366 },
367 )
368 .into_iter()
369 .map(|(obj_id, writes)| {
370 AccumulatorEvent::new(obj_id, AccumulatorWriteV1::merge(writes))
371 })
372 .collect();
373 }
374
375 pub fn into_inner(
377 self,
378 accumulator_running_max_withdraws: BTreeMap<AccumulatorObjId, u128>,
379 ) -> InnerTemporaryStore {
380 let results = self.execution_results;
381 InnerTemporaryStore {
382 input_objects: self.input_objects,
383 stream_ended_consensus_objects: self.stream_ended_consensus_objects,
384 mutable_inputs: self.mutable_input_refs,
385 written: results.written_objects,
386 events: TransactionEvents {
387 data: results.user_events,
388 },
389 accumulator_events: results.accumulator_events,
390 loaded_runtime_objects: self.loaded_runtime_objects,
391 runtime_packages_loaded_from_db: self.runtime_packages_loaded_from_db.into_inner(),
392 lamport_version: self.lamport_timestamp,
393 binary_config: self.protocol_config.binary_config(None),
394 accumulator_running_max_withdraws,
395 }
396 }
397
398 pub(crate) fn ensure_active_inputs_mutated(&mut self) {
402 let mut to_be_updated = vec![];
403 for id in self.mutable_input_refs.keys() {
405 if !self.execution_results.modified_objects.contains(id) {
406 to_be_updated.push(self.input_objects[id].clone());
410 }
411 }
412 for object in to_be_updated {
413 self.mutate_input_object(object.clone());
415 }
416 }
417
418 fn get_object_changes(&self) -> BTreeMap<ObjectID, EffectsObjectChange> {
419 let results = &self.execution_results;
420 let all_ids = results
421 .created_object_ids
422 .iter()
423 .chain(&results.deleted_object_ids)
424 .chain(&results.modified_objects)
425 .chain(results.written_objects.keys())
426 .collect::<BTreeSet<_>>();
427 all_ids
428 .into_iter()
429 .map(|id| {
430 (
431 *id,
432 EffectsObjectChange::new(
433 self.get_object_modified_at(id)
434 .map(|metadata| ((metadata.version, metadata.digest), metadata.owner)),
435 results.written_objects.get(id),
436 results.created_object_ids.contains(id),
437 results.deleted_object_ids.contains(id),
438 ),
439 )
440 })
441 .chain(results.accumulator_events.iter().cloned().map(
442 |AccumulatorEvent {
443 accumulator_obj,
444 write,
445 }| {
446 (
447 *accumulator_obj.inner(),
448 EffectsObjectChange::new_from_accumulator_write(write),
449 )
450 },
451 ))
452 .collect()
453 }
454
455 pub fn into_effects(
456 mut self,
457 shared_object_refs: Vec<SharedInput>,
458 transaction_digest: &TransactionDigest,
459 mut transaction_dependencies: BTreeSet<TransactionDigest>,
460 gas_cost_summary: GasCostSummary,
461 status: ExecutionStatus,
462 gas_coin: Option<ObjectID>,
463 epoch: EpochId,
464 ) -> (InnerTemporaryStore, TransactionEffects) {
465 for (id, obj) in &self.execution_results.written_objects {
468 assert!(
469 !matches!(obj.owner, Owner::Party { .. }),
470 "Party-owned objects are not yet supported (object {id})"
471 );
472 }
473
474 self.update_object_version_and_prev_tx();
475 let accumulator_running_max_withdraws = self.calculate_accumulator_running_max_withdraws();
477 self.merge_accumulator_events();
478
479 for (id, expected_version, expected_digest) in &self.receiving_objects {
482 if let Some(obj_meta) = self.loaded_runtime_objects.get(id) {
486 let loaded_via_receive = obj_meta.version == *expected_version
490 && obj_meta.digest == *expected_digest
491 && obj_meta.owner.is_address_owned();
492 if loaded_via_receive {
493 transaction_dependencies.insert(obj_meta.previous_transaction);
494 }
495 }
496 }
497
498 assert!(self.protocol_config.enable_effects_v2());
499
500 let object_changes = self.get_object_changes();
501
502 let lamport_version = self.lamport_timestamp;
503 let loaded_per_epoch_config_objects = self.loaded_per_epoch_config_objects.read().clone();
505 let unchanged_consensus_objects = TransactionEffectsV2::compute_unchanged_consensus_objects(
506 shared_object_refs,
507 loaded_per_epoch_config_objects,
508 &object_changes,
509 );
510 let inner = self.into_inner(accumulator_running_max_withdraws);
511
512 let effects = TransactionEffects::new_from_execution_v2(
513 status,
514 epoch,
515 gas_cost_summary,
516 unchanged_consensus_objects,
517 *transaction_digest,
518 lamport_version,
519 object_changes,
520 gas_coin,
521 if inner.events.data.is_empty() {
522 None
523 } else {
524 Some(inner.events.digest())
525 },
526 transaction_dependencies.into_iter().collect(),
527 );
528
529 (inner, effects)
530 }
531
532 #[cfg(debug_assertions)]
534 fn check_invariants(&self) {
535 debug_assert!(
537 {
538 self.execution_results
539 .written_objects
540 .keys()
541 .all(|id| !self.execution_results.deleted_object_ids.contains(id))
542 },
543 "Object both written and deleted."
544 );
545
546 debug_assert!(
548 {
549 self.mutable_input_refs
550 .keys()
551 .all(|id| self.execution_results.modified_objects.contains(id))
552 },
553 "Mutable input not modified."
554 );
555
556 debug_assert!(
557 {
558 self.execution_results
559 .written_objects
560 .values()
561 .all(|obj| obj.previous_transaction == self.tx_digest)
562 },
563 "Object previous transaction not properly set",
564 );
565 }
566
567 pub fn mutate_input_object(&mut self, object: Object) {
569 let id = object.id();
570 debug_assert!(self.input_objects.contains_key(&id));
571 debug_assert!(!object.is_immutable());
572 self.execution_results.modified_objects.insert(id);
573 self.execution_results.written_objects.insert(id, object);
574 }
575
576 pub fn mutate_new_or_input_object(&mut self, object: Object) {
577 let id = object.id();
578 debug_assert!(!object.is_immutable());
579 if self.input_objects.contains_key(&id) {
580 self.execution_results.modified_objects.insert(id);
581 }
582 self.execution_results.written_objects.insert(id, object);
583 }
584
585 pub fn mutate_child_object(&mut self, old_object: Object, new_object: Object) {
589 let id = new_object.id();
590 let old_ref = old_object.compute_object_reference();
591 debug_assert_eq!(old_ref.0, id);
592 self.loaded_runtime_objects.insert(
593 id,
594 DynamicallyLoadedObjectMetadata {
595 version: old_ref.1,
596 digest: old_ref.2,
597 owner: old_object.owner.clone(),
598 storage_rebate: old_object.storage_rebate,
599 previous_transaction: old_object.previous_transaction,
600 },
601 );
602 self.execution_results.modified_objects.insert(id);
603 self.execution_results
604 .written_objects
605 .insert(id, new_object);
606 }
607
608 pub fn upgrade_system_package(&mut self, package: Object) {
612 let id = package.id();
613 assert!(package.is_package() && is_system_package(id));
614 self.execution_results.modified_objects.insert(id);
615 self.execution_results.written_objects.insert(id, package);
616 }
617
618 pub fn create_object(&mut self, object: Object) {
620 debug_assert!(
625 object.is_immutable() || object.version() == SequenceNumber::MIN,
626 "Created mutable objects should not have a version set",
627 );
628 let id = object.id();
629 self.execution_results.created_object_ids.insert(id);
630 self.execution_results.written_objects.insert(id, object);
631 }
632
633 pub fn delete_input_object(&mut self, id: &ObjectID) {
635 debug_assert!(!self.execution_results.written_objects.contains_key(id));
637 debug_assert!(self.input_objects.contains_key(id));
638 self.execution_results.modified_objects.insert(*id);
639 self.execution_results.deleted_object_ids.insert(*id);
640 }
641
642 pub fn drop_writes(&mut self) {
643 self.execution_results.drop_writes();
644 self.invariants = InvariantChecker::default();
645 }
646
647 pub(crate) fn into_bump_only(self) -> Self {
652 let Self {
653 store,
655 tx_digest,
656 input_objects,
657 non_exclusive_input_original_versions,
658 stream_ended_consensus_objects,
659 lamport_timestamp,
660 mutable_input_refs,
661 receiving_objects,
662 cur_epoch,
663 protocol_config,
664 post_execution_check_inputs,
665 loaded_runtime_objects,
667 runtime_packages_loaded_from_db,
668 loaded_per_epoch_config_objects,
669 execution_results: _,
671 invariants: _,
672 } = self;
673 let mut bump_only = Self {
674 store,
675 tx_digest,
676 input_objects,
677 non_exclusive_input_original_versions,
678 stream_ended_consensus_objects,
679 lamport_timestamp,
680 mutable_input_refs,
681 receiving_objects,
682 cur_epoch,
683 protocol_config,
684 loaded_runtime_objects,
685 runtime_packages_loaded_from_db,
686 loaded_per_epoch_config_objects,
687 post_execution_check_inputs,
688 execution_results: ExecutionResultsV2::default(),
689 invariants: InvariantChecker::default(),
690 };
691 bump_only.ensure_active_inputs_mutated();
693 bump_only
694 }
695
696 pub fn read_object(&self, id: &ObjectID) -> Option<&Object> {
697 debug_assert!(!self.execution_results.deleted_object_ids.contains(id));
699 self.execution_results
700 .written_objects
701 .get(id)
702 .or_else(|| self.input_objects.get(id))
703 }
704
705 pub fn save_loaded_runtime_objects(
706 &mut self,
707 loaded_runtime_objects: BTreeMap<ObjectID, DynamicallyLoadedObjectMetadata>,
708 ) {
709 #[cfg(debug_assertions)]
710 {
711 for (id, v1) in &loaded_runtime_objects {
712 if let Some(v2) = self.loaded_runtime_objects.get(id) {
713 assert_eq!(v1, v2);
714 }
715 }
716 for (id, v1) in &self.loaded_runtime_objects {
717 if let Some(v2) = loaded_runtime_objects.get(id) {
718 assert_eq!(v1, v2);
719 }
720 }
721 }
722 self.loaded_runtime_objects.extend(loaded_runtime_objects);
725 }
726
727 pub fn save_wrapped_object_containers(
728 &mut self,
729 wrapped_object_containers: BTreeMap<ObjectID, ObjectID>,
730 ) {
731 self.invariants
732 .save_wrapped_object_containers(wrapped_object_containers);
733 }
734
735 pub fn save_generated_object_ids(&mut self, generated_ids: BTreeSet<ObjectID>) {
736 self.invariants.save_generated_object_ids(generated_ids);
737 }
738
739 pub fn estimate_effects_size_upperbound(&self) -> usize {
740 TransactionEffects::estimate_effects_size_upperbound_v2(
741 self.execution_results.written_objects.len(),
742 self.execution_results.modified_objects.len(),
743 self.input_objects.len(),
744 )
745 }
746
747 pub fn written_objects_size(&self) -> usize {
748 self.execution_results
749 .written_objects
750 .values()
751 .fold(0, |sum, obj| sum + obj.object_size_for_gas_metering())
752 }
753
754 pub(crate) fn check_gasless_execution_requirements(&self) -> Result<(), String> {
757 use sui_types::balance::Balance;
758
759 let withdrawal_reservations = self
762 .post_execution_check_inputs
763 .input_reservations
764 .iter()
765 .filter_map(|((owner, ty), amount)| {
766 Balance::maybe_get_balance_type_param(ty)
767 .map(|coin_type| ((*owner, coin_type), *amount))
768 })
769 .collect();
770 self.check_gasless_execution_requirements_with_reservations(Some(&withdrawal_reservations))
771 }
772
773 pub(crate) fn check_gasless_execution_requirements_with_reservations(
784 &self,
785 withdrawal_reservations: Option<&BTreeMap<(SuiAddress, TypeTag), u64>>,
786 ) -> Result<(), String> {
787 if !self.execution_results.written_objects.is_empty() {
788 return Err("Gasless transactions cannot create or mutate objects".to_string());
789 }
790
791 let input_coin_ids: BTreeSet<ObjectID> = self
792 .input_objects
793 .iter()
794 .filter(|(_, obj)| obj.coin_type_maybe().is_some())
795 .map(|(id, _)| *id)
796 .collect();
797 if self.execution_results.deleted_object_ids != input_coin_ids {
798 return Err(format!(
799 "Gasless transaction must destroy exactly its input Coins. \
800 Expected: {input_coin_ids:?}, deleted: {:?}",
801 self.execution_results.deleted_object_ids
802 ));
803 }
804
805 let allowed_types =
806 sui_types::transaction::get_gasless_allowed_token_types(self.protocol_config);
807
808 let net_totals = sui_types::balance_change::signed_balance_changes_from_events(
811 &self.execution_results.accumulator_events,
812 )
813 .fold(
814 BTreeMap::<(SuiAddress, TypeTag), i128>::new(),
815 |mut totals, (address, token_type, signed_amount)| {
816 *totals.entry((address, token_type)).or_default() += signed_amount;
817 totals
818 },
819 );
820
821 for ((recipient, token_type), net_amount) in &net_totals {
822 if *net_amount <= 0 {
823 continue;
824 }
825 if let Some(&min_amount) = allowed_types.get(token_type)
826 && *net_amount < i128::from(min_amount)
827 {
828 return Err(format!(
829 "Gasless transfer of {net_amount} to {recipient} is below \
830 minimum {min_amount} for token type {token_type}"
831 ));
832 }
833 }
834
835 if let Some(reservations) = withdrawal_reservations {
836 for ((owner, token_type), &reserved) in reservations {
837 let net = net_totals
838 .get(&(*owner, token_type.clone()))
839 .copied()
840 .unwrap_or(0);
841 let remaining = (reserved as i128).saturating_add(net);
842 if remaining > 0
843 && let Some(&min_balance_remaining) = allowed_types.get(token_type)
844 && min_balance_remaining > 0
845 && remaining < min_balance_remaining as i128
846 {
847 return Err(format!(
848 "Gasless withdrawal leaves {remaining} unused for {owner}, \
849 below minimum {min_balance_remaining} for token type {token_type}"
850 ));
851 }
852 }
853 }
854
855 Ok(())
856 }
857
858 pub fn conserve_unmetered_storage_rebate(&mut self, unmetered_storage_rebate: u64) {
863 if unmetered_storage_rebate == 0 {
864 return;
868 }
869 tracing::debug!(
870 "Amount of unmetered storage rebate from system tx: {:?}",
871 unmetered_storage_rebate
872 );
873 let mut system_state_wrapper = self
874 .read_object(&SUI_SYSTEM_STATE_OBJECT_ID)
875 .expect("0x5 object must be mutated in system tx with unmetered storage rebate")
876 .clone();
877 assert_eq!(system_state_wrapper.storage_rebate, 0);
880 system_state_wrapper.storage_rebate = unmetered_storage_rebate;
881 self.mutate_input_object(system_state_wrapper);
882 }
883
884 pub fn add_accumulator_event(&mut self, event: AccumulatorEvent) {
886 self.execution_results.accumulator_events.push(event);
887 }
888
889 fn get_object_modified_at(
895 &self,
896 object_id: &ObjectID,
897 ) -> Option<DynamicallyLoadedObjectMetadata> {
898 if self.execution_results.modified_objects.contains(object_id) {
899 Some(
900 self.mutable_input_refs
901 .get(object_id)
902 .map(
903 |((version, digest), owner)| DynamicallyLoadedObjectMetadata {
904 version: *version,
905 digest: *digest,
906 owner: owner.clone(),
907 storage_rebate: self.input_objects[object_id].storage_rebate,
909 previous_transaction: self.input_objects[object_id]
910 .previous_transaction,
911 },
912 )
913 .or_else(|| self.loaded_runtime_objects.get(object_id).cloned())
914 .unwrap_or_else(|| {
915 debug_assert!(is_system_package(*object_id));
916 let package_obj =
917 self.store.get_package_object(object_id).unwrap().unwrap();
918 let obj = package_obj.object();
919 DynamicallyLoadedObjectMetadata {
920 version: obj.version(),
921 digest: obj.digest(),
922 owner: obj.owner.clone(),
923 storage_rebate: obj.storage_rebate,
924 previous_transaction: obj.previous_transaction,
925 }
926 }),
927 )
928 } else {
929 None
930 }
931 }
932
933 pub fn protocol_config(&self) -> &'backing ProtocolConfig {
934 self.protocol_config
935 }
936
937 pub(crate) fn check_conservation_invariants<Mode: ExecutionMode>(
940 &self,
941 move_vm: &Arc<MoveRuntime>,
942 enable_expensive_checks: bool,
943 cost_summary: &GasCostSummary,
944 ) -> Result<(), ExecutionError> {
945 self.invariants.check_conservation_invariants::<Mode>(
946 self,
947 move_vm,
948 enable_expensive_checks,
949 cost_summary,
950 )
951 }
952
953 pub(crate) fn check_published_packages(&self) -> Result<(), ExecutionError> {
957 self.invariants.check_published_packages(self)
958 }
959
960 pub(crate) fn check_ownership_invariants(
961 &self,
962 sender: &SuiAddress,
963 sponsor: &Option<SuiAddress>,
964 gas_charger: &GasCharger,
965 is_epoch_change: bool,
966 ) -> SuiResult<()> {
967 self.invariants.check_ownership_invariants(
968 self,
969 sender,
970 sponsor,
971 gas_charger,
972 is_epoch_change,
973 )
974 }
975}
976
977impl TemporaryStore<'_> {
978 pub(crate) fn collect_storage_and_rebate(
985 &mut self,
986 gas_charger: &mut GasCharger,
987 ) -> Result<(), ExecutionError> {
988 let old_storage_rebates: Vec<_> = self
990 .execution_results
991 .written_objects
992 .keys()
993 .map(|object_id| {
994 self.get_object_modified_at(object_id)
995 .map(|metadata| metadata.storage_rebate)
996 .unwrap_or_default()
997 })
998 .collect();
999 for (object, old_storage_rebate) in self
1000 .execution_results
1001 .written_objects
1002 .values_mut()
1003 .zip_debug_eq(old_storage_rebates)
1004 {
1005 let new_object_size = object.object_size_for_gas_metering();
1007 let new_storage_rebate = gas_charger
1009 .track_storage_mutation(object.id(), new_object_size, old_storage_rebate)
1010 .ok_or_else(|| ExecutionError::from_kind(ExecutionErrorKind::InvariantViolation))?;
1011 object.storage_rebate = new_storage_rebate;
1012 }
1013
1014 self.collect_rebate(gas_charger)
1015 }
1016
1017 pub(crate) fn collect_rebate(
1018 &self,
1019 gas_charger: &mut GasCharger,
1020 ) -> Result<(), ExecutionError> {
1021 for object_id in &self.execution_results.modified_objects {
1022 if self
1023 .execution_results
1024 .written_objects
1025 .contains_key(object_id)
1026 {
1027 continue;
1028 }
1029 let storage_rebate = self
1031 .get_object_modified_at(object_id)
1032 .unwrap()
1034 .storage_rebate;
1035 gas_charger
1036 .track_storage_mutation(*object_id, 0, storage_rebate)
1037 .ok_or_else(|| ExecutionError::from_kind(ExecutionErrorKind::InvariantViolation))?;
1038 }
1039 Ok(())
1040 }
1041
1042 pub fn check_execution_results_consistency<Mode: ExecutionMode>(
1043 &self,
1044 ) -> Result<(), Mode::Error> {
1045 assert_invariant!(
1046 self.execution_results
1047 .created_object_ids
1048 .iter()
1049 .all(|id| !self.execution_results.deleted_object_ids.contains(id)
1050 && !self.execution_results.modified_objects.contains(id)),
1051 "Created object IDs cannot also be deleted or modified"
1052 );
1053 assert_invariant!(
1054 self.execution_results.modified_objects.iter().all(|id| {
1055 self.mutable_input_refs.contains_key(id)
1056 || self.loaded_runtime_objects.contains_key(id)
1057 || is_system_package(*id)
1058 }),
1059 "A modified object must be either a mutable input, a loaded child object, or a system package"
1060 );
1061 Ok(())
1062 }
1063}
1064impl TemporaryStore<'_> {
1069 pub fn advance_epoch_safe_mode(
1070 &mut self,
1071 params: &AdvanceEpochParams,
1072 protocol_config: &ProtocolConfig,
1073 ) {
1074 let wrapper = get_sui_system_state_wrapper(self.store)
1075 .expect("System state wrapper object must exist");
1076 let (old_object, new_object) =
1077 wrapper.advance_epoch_safe_mode(params, self.store, protocol_config);
1078 self.mutate_child_object(old_object, new_object);
1079 }
1080}
1081
1082impl RuntimeObjectResolver for TemporaryStore<'_> {
1083 fn read_child_object(
1084 &self,
1085 parent: &ObjectID,
1086 child: &ObjectID,
1087 child_version_upper_bound: SequenceNumber,
1088 ) -> SuiResult<Option<Object>> {
1089 let obj_opt = self.execution_results.written_objects.get(child);
1090 if obj_opt.is_some() {
1091 Ok(obj_opt.cloned())
1092 } else {
1093 let _scope = monitored_scope("Execution::read_child_object");
1094 self.store
1095 .read_child_object(parent, child, child_version_upper_bound)
1096 }
1097 }
1098
1099 fn get_object_received_at_version(
1100 &self,
1101 owner: &ObjectID,
1102 receiving_object_id: &ObjectID,
1103 receive_object_at_version: SequenceNumber,
1104 epoch_id: EpochId,
1105 ) -> SuiResult<Option<Object>> {
1106 debug_assert!(
1109 !self
1110 .execution_results
1111 .written_objects
1112 .contains_key(receiving_object_id)
1113 );
1114 debug_assert!(
1115 !self
1116 .execution_results
1117 .deleted_object_ids
1118 .contains(receiving_object_id)
1119 );
1120 self.store.get_object_received_at_version(
1121 owner,
1122 receiving_object_id,
1123 receive_object_at_version,
1124 epoch_id,
1125 )
1126 }
1127}
1128
1129fn compute_input_reservations(
1136 transaction_kind: &TransactionKind,
1137 gas_data: &GasData,
1138 transaction_signer: SuiAddress,
1139 enable_gasless: bool,
1140) -> BTreeMap<(SuiAddress, TypeTag), u64> {
1141 use sui_types::balance::Balance;
1142 use sui_types::gas_coin::GAS;
1143 use sui_types::transaction::{Reservation, WithdrawFrom, is_gas_paid_from_address_balance};
1144
1145 let is_gasless = enable_gasless && is_gasless_transaction(gas_data, transaction_kind);
1146 let mut reservations: BTreeMap<(SuiAddress, TypeTag), u64> = BTreeMap::new();
1147 let sui_balance_type = Balance::type_tag(GAS::type_tag());
1148
1149 for arg in transaction_kind.get_funds_withdrawals() {
1150 let owner = match arg.withdraw_from {
1151 WithdrawFrom::Sender => transaction_signer,
1152 WithdrawFrom::Sponsor => gas_data.owner,
1153 };
1154 let Reservation::MaxAmountU64(reservation) = arg.reservation;
1155 let entry = reservations
1156 .entry((owner, arg.type_arg.to_type_tag()))
1157 .or_insert(0);
1158 *entry = entry.saturating_add(reservation);
1159 }
1160
1161 if !is_gasless && is_gas_paid_from_address_balance(gas_data, transaction_kind) {
1164 let entry = reservations
1165 .entry((gas_data.owner, sui_balance_type.clone()))
1166 .or_insert(0);
1167 *entry = entry.saturating_add(gas_data.budget);
1168 }
1169
1170 for entry in &gas_data.payment {
1171 if let Ok(parsed) = ParsedDigest::try_from(entry.2) {
1172 let entry = reservations
1173 .entry((gas_data.owner, sui_balance_type.clone()))
1174 .or_insert(0);
1175 *entry = entry.saturating_add(parsed.reservation_amount());
1176 }
1177 }
1178
1179 reservations
1180}
1181
1182fn declared_packages(
1185 transaction_kind: &TransactionKind,
1186) -> Option<Vec<(usize, BTreeSet<ObjectID>)>> {
1187 let TransactionKind::ProgrammableTransaction(pt) = transaction_kind else {
1188 return None;
1189 };
1190 Some(
1191 pt.commands
1192 .iter()
1193 .filter_map(|command| match command {
1194 Command::Publish(modules, dep_ids) | Command::Upgrade(modules, dep_ids, _, _) => {
1195 Some((modules.len(), dep_ids.iter().copied().collect()))
1196 }
1197 _ => None,
1198 })
1199 .collect(),
1200 )
1201}
1202
1203fn was_object_mutated(object: &Object, original: &Object) -> bool {
1206 let data_equal = match (&object.data, &original.data) {
1207 (Data::Move(a), Data::Move(b)) => a.contents_and_type_equal(b),
1208 (Data::Package(a), Data::Package(b)) => a == b,
1211 _ => false,
1212 };
1213
1214 let owner_equal = match (&object.owner, &original.owner) {
1215 (Owner::Shared { .. }, Owner::Shared { .. }) => true,
1219 (
1220 Owner::ConsensusAddressOwner { owner: a, .. },
1221 Owner::ConsensusAddressOwner { owner: b, .. },
1222 ) => a == b,
1223 (Owner::AddressOwner(a), Owner::AddressOwner(b)) => a == b,
1224 (Owner::Immutable, Owner::Immutable) => true,
1225 (Owner::ObjectOwner(a), Owner::ObjectOwner(b)) => a == b,
1226 (
1227 Owner::Party {
1228 permissions: a,
1229 start_version: _,
1230 },
1231 Owner::Party {
1232 permissions: b,
1233 start_version: _,
1234 },
1235 ) => a == b,
1236
1237 (Owner::AddressOwner(_), _)
1240 | (Owner::Immutable, _)
1241 | (Owner::ObjectOwner(_), _)
1242 | (Owner::Shared { .. }, _)
1243 | (Owner::ConsensusAddressOwner { .. }, _)
1244 | (Owner::Party { .. }, _) => false,
1245 };
1246
1247 !data_equal || !owner_equal
1248}
1249
1250impl Storage for TemporaryStore<'_> {
1251 fn reset(&mut self) {
1252 self.drop_writes();
1253 }
1254
1255 fn read_object(&self, id: &ObjectID) -> Option<&Object> {
1256 TemporaryStore::read_object(self, id)
1257 }
1258
1259 fn record_execution_results(
1261 &mut self,
1262 results: ExecutionResults,
1263 ) -> Result<(), ExecutionError> {
1264 let ExecutionResults::V2(mut results) = results else {
1265 panic!("ExecutionResults::V2 expected in sui-execution v1 and above");
1266 };
1267
1268 let mut to_remove = Vec::new();
1270 for (id, original) in &self.non_exclusive_input_original_versions {
1271 if results
1273 .written_objects
1274 .get(id)
1275 .map(|obj| was_object_mutated(obj, original))
1276 .unwrap_or(true)
1277 {
1278 return Err(ExecutionError::new_with_source(
1279 ExecutionErrorKind::NonExclusiveWriteInputObjectModified { id: *id },
1280 "Non-exclusive write input object has been modified or deleted",
1281 ));
1282 }
1283 to_remove.push(*id);
1284 }
1285
1286 for id in to_remove {
1287 results.written_objects.remove(&id);
1288 results.modified_objects.remove(&id);
1289 }
1290
1291 let event_start = self.execution_results.accumulator_events.len();
1297 self.execution_results.merge_results(
1298 results, true, true,
1299 )?;
1300 let event_end = self.execution_results.accumulator_events.len();
1301 self.invariants
1302 .record_ptb_event_range(event_start, event_end);
1303
1304 Ok(())
1305 }
1306
1307 fn save_loaded_runtime_objects(
1308 &mut self,
1309 loaded_runtime_objects: BTreeMap<ObjectID, DynamicallyLoadedObjectMetadata>,
1310 ) {
1311 TemporaryStore::save_loaded_runtime_objects(self, loaded_runtime_objects)
1312 }
1313
1314 fn save_wrapped_object_containers(
1315 &mut self,
1316 wrapped_object_containers: BTreeMap<ObjectID, ObjectID>,
1317 ) {
1318 TemporaryStore::save_wrapped_object_containers(self, wrapped_object_containers)
1319 }
1320
1321 fn check_coin_deny_list(
1322 &self,
1323 receiving_funds_type_and_owners: BTreeMap<TypeTag, BTreeSet<SuiAddress>>,
1324 ) -> DenyListResult {
1325 let result = check_coin_deny_list_v2_during_execution(
1326 receiving_funds_type_and_owners,
1327 self.cur_epoch,
1328 self.store,
1329 );
1330 if result.num_non_gas_coin_owners > 0
1333 && !self.input_objects.contains_key(&SUI_DENY_LIST_OBJECT_ID)
1334 {
1335 self.loaded_per_epoch_config_objects
1336 .write()
1337 .insert(SUI_DENY_LIST_OBJECT_ID);
1338 }
1339 result
1340 }
1341
1342 fn record_generated_object_ids(&mut self, generated_ids: BTreeSet<ObjectID>) {
1343 TemporaryStore::save_generated_object_ids(self, generated_ids)
1344 }
1345}
1346
1347impl BackingPackageStore for TemporaryStore<'_> {
1348 fn get_package_object(&self, package_id: &ObjectID) -> SuiResult<Option<PackageObject>> {
1349 if let Some(obj) = self.execution_results.written_objects.get(package_id) {
1356 Ok(Some(PackageObject::new(obj.clone())))
1357 } else {
1358 self.store.get_package_object(package_id).inspect(|obj| {
1359 if let Some(v) = obj
1361 && !self
1362 .runtime_packages_loaded_from_db
1363 .read()
1364 .contains_key(package_id)
1365 {
1366 self.runtime_packages_loaded_from_db
1371 .write()
1372 .insert(*package_id, v.clone());
1373 }
1374 })
1375 }
1376 }
1377}