Skip to main content

sui_types/effects/
effects_v2.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use super::object_change::{AccumulatorWriteV1, ObjectIn, ObjectOut};
5use super::{EffectsObjectChange, IDOperation, ObjectChange};
6use crate::accumulator_event::AccumulatorEvent;
7use crate::accumulator_root::AccumulatorObjId;
8use crate::base_types::{
9    EpochId, ObjectDigest, ObjectID, ObjectRef, SequenceNumber, SuiAddress, TransactionDigest,
10    VersionDigest,
11};
12use crate::digests::{EffectsAuxDataDigest, TransactionEventsDigest};
13use crate::effects::{InputConsensusObject, TransactionEffectsAPI};
14use crate::execution::SharedInput;
15use crate::execution_status::{
16    ExecutionErrorKind, ExecutionFailure, ExecutionStatus, MoveLocation,
17};
18use crate::gas::GasCostSummary;
19#[cfg(debug_assertions)]
20use crate::is_system_package;
21use crate::object::{OBJECT_START_VERSION, Owner};
22use crate::transaction::SharedObjectMutability;
23use serde::{Deserialize, Serialize};
24#[cfg(debug_assertions)]
25use std::collections::HashSet;
26use std::collections::{BTreeMap, BTreeSet};
27
28/// The response from processing a transaction or a certified transaction
29#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize)]
30pub struct TransactionEffectsV2 {
31    /// The status of the execution
32    pub(crate) status: ExecutionStatus,
33    /// The epoch when this transaction was executed.
34    pub(crate) executed_epoch: EpochId,
35    pub(crate) gas_used: GasCostSummary,
36    /// The transaction digest
37    pub(crate) transaction_digest: TransactionDigest,
38    /// The updated gas object reference, as an index into the `changed_objects` vector.
39    /// Having a dedicated field for convenient access.
40    /// System transaction that don't require gas will leave this as None.
41    pub(crate) gas_object_index: Option<u32>,
42    /// The digest of the events emitted during execution,
43    /// can be None if the transaction does not emit any event.
44    pub(crate) events_digest: Option<TransactionEventsDigest>,
45    /// The set of transaction digests this transaction depends on.
46    /// Empty when the protocol disables effects dependencies.
47    pub(crate) dependencies: Vec<TransactionDigest>,
48
49    /// The version number of all the written Move objects by this transaction.
50    pub(crate) lamport_version: SequenceNumber,
51    /// Objects whose state are changed in the object store.
52    /// This field should not be exposed to the public API.
53    /// Otherwise it will make it harder to use effects of different versions.
54    /// Note that for accumulator writes, the ObjectID here will be the dynamic field object ID
55    /// that stores the accumulator value. However this object is not really mutated
56    /// in this transaction. We just have to use an ObjectID that is unique so that
57    /// it does not conflict with any other object IDs in the changed_objects.
58    pub(crate) changed_objects: Vec<(ObjectID, EffectsObjectChange)>,
59    /// Consensus objects that are not mutated in this transaction. Unlike owned objects,
60    /// read-only consensus objects' version are not committed in the transaction,
61    /// and in order for a node to catch up and execute it without consensus sequencing,
62    /// the version needs to be committed in the effects.
63    pub(crate) unchanged_consensus_objects: Vec<(ObjectID, UnchangedConsensusKind)>,
64    /// Auxiliary data that are not protocol-critical, generated as part of the effects but are stored separately.
65    /// Storing it separately allows us to avoid bloating the effects with data that are not critical.
66    /// It also provides more flexibility on the format and type of the data.
67    pub(crate) aux_data_digest: Option<EffectsAuxDataDigest>,
68}
69
70impl TransactionEffectsAPI for TransactionEffectsV2 {
71    fn status(&self) -> &ExecutionStatus {
72        &self.status
73    }
74
75    fn into_status(self) -> ExecutionStatus {
76        self.status
77    }
78
79    fn executed_epoch(&self) -> EpochId {
80        self.executed_epoch
81    }
82
83    fn modified_at_versions(&self) -> Vec<(ObjectID, SequenceNumber)> {
84        self.changed_objects
85            .iter()
86            .filter_map(|(id, change)| {
87                if let ObjectIn::Exist(((version, _digest), _owner)) = &change.input_state {
88                    Some((*id, *version))
89                } else {
90                    None
91                }
92            })
93            .collect()
94    }
95
96    fn move_abort(&self) -> Option<(MoveLocation, u64)> {
97        let ExecutionStatus::Failure(ExecutionFailure {
98            error: ExecutionErrorKind::MoveAbort(move_location, code),
99            ..
100        }) = self.status()
101        else {
102            return None;
103        };
104        Some((move_location.clone(), *code))
105    }
106
107    fn lamport_version(&self) -> SequenceNumber {
108        self.lamport_version
109    }
110
111    fn old_object_metadata(&self) -> Vec<(ObjectRef, Owner)> {
112        self.changed_objects
113            .iter()
114            .filter_map(|(id, change)| {
115                if let ObjectIn::Exist(((version, digest), owner)) = &change.input_state {
116                    Some(((*id, *version, *digest), owner.clone()))
117                } else {
118                    None
119                }
120            })
121            .collect()
122    }
123
124    fn accessed_consensus_objects(&self) -> Vec<InputConsensusObject> {
125        self.changed_objects
126            .iter()
127            .filter_map(|(id, change)| match &change.input_state {
128                ObjectIn::Exist(((version, digest), owner)) if owner.is_consensus() => {
129                    Some(InputConsensusObject::Mutate((*id, *version, *digest)))
130                }
131                _ => None,
132            })
133            .chain(
134                self.unchanged_consensus_objects
135                    .iter()
136                    .filter_map(|(id, change_kind)| match change_kind {
137                        UnchangedConsensusKind::ReadOnlyRoot((version, digest)) => {
138                            Some(InputConsensusObject::ReadOnly((*id, *version, *digest)))
139                        }
140                        UnchangedConsensusKind::MutateConsensusStreamEnded(seqno) => Some(
141                            InputConsensusObject::MutateConsensusStreamEnded(*id, *seqno),
142                        ),
143                        UnchangedConsensusKind::ReadConsensusStreamEnded(seqno) => {
144                            Some(InputConsensusObject::ReadConsensusStreamEnded(*id, *seqno))
145                        }
146                        UnchangedConsensusKind::Cancelled(seqno) => {
147                            Some(InputConsensusObject::Cancelled(*id, *seqno))
148                        }
149                        // We can not expose the per epoch config object as input consensus object,
150                        // since it does not require sequencing, and hence shall not be considered
151                        // as a normal input consensus object.
152                        UnchangedConsensusKind::PerEpochConfig => None,
153                    }),
154            )
155            .collect()
156    }
157
158    fn created(&self) -> Vec<(ObjectRef, Owner)> {
159        self.changed_objects
160            .iter()
161            .filter_map(|(id, change)| {
162                match (
163                    &change.input_state,
164                    &change.output_state,
165                    &change.id_operation,
166                ) {
167                    (
168                        ObjectIn::NotExist,
169                        ObjectOut::ObjectWrite((digest, owner)),
170                        IDOperation::Created,
171                    ) => Some(((*id, self.lamport_version, *digest), owner.clone())),
172                    (
173                        ObjectIn::NotExist,
174                        ObjectOut::PackageWrite((version, digest)),
175                        IDOperation::Created,
176                    ) => Some(((*id, *version, *digest), Owner::Immutable)),
177                    _ => None,
178                }
179            })
180            .collect()
181    }
182
183    fn mutated(&self) -> Vec<(ObjectRef, Owner)> {
184        self.changed_objects
185            .iter()
186            .filter_map(
187                |(id, change)| match (&change.input_state, &change.output_state) {
188                    (ObjectIn::Exist(_), ObjectOut::ObjectWrite((digest, owner))) => {
189                        Some(((*id, self.lamport_version, *digest), owner.clone()))
190                    }
191                    (ObjectIn::Exist(_), ObjectOut::PackageWrite((version, digest))) => {
192                        Some(((*id, *version, *digest), Owner::Immutable))
193                    }
194                    _ => None,
195                },
196            )
197            .collect()
198    }
199
200    fn unwrapped(&self) -> Vec<(ObjectRef, Owner)> {
201        self.changed_objects
202            .iter()
203            .filter_map(|(id, change)| {
204                match (
205                    &change.input_state,
206                    &change.output_state,
207                    &change.id_operation,
208                ) {
209                    (
210                        ObjectIn::NotExist,
211                        ObjectOut::ObjectWrite((digest, owner)),
212                        IDOperation::None,
213                    ) => Some(((*id, self.lamport_version, *digest), owner.clone())),
214                    _ => None,
215                }
216            })
217            .collect()
218    }
219
220    fn deleted(&self) -> Vec<ObjectRef> {
221        self.changed_objects
222            .iter()
223            .filter_map(|(id, change)| {
224                match (
225                    &change.input_state,
226                    &change.output_state,
227                    &change.id_operation,
228                ) {
229                    (ObjectIn::Exist(_), ObjectOut::NotExist, IDOperation::Deleted) => Some((
230                        *id,
231                        self.lamport_version,
232                        ObjectDigest::OBJECT_DIGEST_DELETED,
233                    )),
234                    _ => None,
235                }
236            })
237            .collect()
238    }
239
240    fn unwrapped_then_deleted(&self) -> Vec<ObjectRef> {
241        self.changed_objects
242            .iter()
243            .filter_map(|(id, change)| {
244                match (
245                    &change.input_state,
246                    &change.output_state,
247                    &change.id_operation,
248                ) {
249                    (ObjectIn::NotExist, ObjectOut::NotExist, IDOperation::Deleted) => Some((
250                        *id,
251                        self.lamport_version,
252                        ObjectDigest::OBJECT_DIGEST_DELETED,
253                    )),
254                    _ => None,
255                }
256            })
257            .collect()
258    }
259
260    fn wrapped(&self) -> Vec<ObjectRef> {
261        self.changed_objects
262            .iter()
263            .filter_map(|(id, change)| {
264                match (
265                    &change.input_state,
266                    &change.output_state,
267                    &change.id_operation,
268                ) {
269                    (ObjectIn::Exist(_), ObjectOut::NotExist, IDOperation::None) => Some((
270                        *id,
271                        self.lamport_version,
272                        ObjectDigest::OBJECT_DIGEST_WRAPPED,
273                    )),
274                    _ => None,
275                }
276            })
277            .collect()
278    }
279
280    fn written(&self) -> Vec<ObjectRef> {
281        self.changed_objects
282            .iter()
283            .filter_map(
284                |(id, change)| match (&change.output_state, &change.id_operation) {
285                    (ObjectOut::NotExist, IDOperation::Deleted) => Some((
286                        *id,
287                        self.lamport_version,
288                        ObjectDigest::OBJECT_DIGEST_DELETED,
289                    )),
290                    (ObjectOut::NotExist, IDOperation::None) => Some((
291                        *id,
292                        self.lamport_version,
293                        ObjectDigest::OBJECT_DIGEST_WRAPPED,
294                    )),
295                    (ObjectOut::ObjectWrite((d, _)), _) => Some((*id, self.lamport_version, *d)),
296                    (ObjectOut::PackageWrite(vd), _) => Some((*id, vd.0, vd.1)),
297                    (ObjectOut::AccumulatorWriteV1(_), _) => None,
298                    _ => None,
299                },
300            )
301            .collect()
302    }
303
304    fn transferred_from_consensus(&self) -> Vec<ObjectRef> {
305        self.changed_objects
306            .iter()
307            .filter_map(|(id, change)| {
308                match (
309                    &change.input_state,
310                    &change.output_state,
311                    &change.id_operation,
312                ) {
313                    (
314                        ObjectIn::Exist((_, Owner::ConsensusAddressOwner { .. })),
315                        ObjectOut::ObjectWrite((
316                            object_digest,
317                            Owner::AddressOwner(_) | Owner::ObjectOwner(_) | Owner::Immutable,
318                        )),
319                        IDOperation::None,
320                    ) => Some((*id, self.lamport_version, *object_digest)),
321                    _ => None,
322                }
323            })
324            .collect()
325    }
326
327    fn transferred_to_consensus(&self) -> Vec<ObjectRef> {
328        self.changed_objects
329            .iter()
330            .filter_map(|(id, change)| {
331                match (
332                    &change.input_state,
333                    &change.output_state,
334                    &change.id_operation,
335                ) {
336                    (
337                        ObjectIn::Exist((_, Owner::AddressOwner(_) | Owner::ObjectOwner(_))),
338                        ObjectOut::ObjectWrite((
339                            object_digest,
340                            Owner::ConsensusAddressOwner { .. },
341                        )),
342                        IDOperation::None,
343                    ) => Some((*id, self.lamport_version, *object_digest)),
344                    _ => None,
345                }
346            })
347            .collect()
348    }
349
350    fn consensus_owner_changed(&self) -> Vec<ObjectRef> {
351        self.changed_objects
352            .iter()
353            .filter_map(|(id, change)| {
354                match (
355                    &change.input_state,
356                    &change.output_state,
357                    &change.id_operation,
358                ) {
359                    (
360                        ObjectIn::Exist((
361                            _,
362                            Owner::ConsensusAddressOwner {
363                                owner: old_owner, ..
364                            },
365                        )),
366                        ObjectOut::ObjectWrite((
367                            object_digest,
368                            Owner::ConsensusAddressOwner {
369                                owner: new_owner, ..
370                            },
371                        )),
372                        IDOperation::None,
373                    ) if old_owner != new_owner => {
374                        Some((*id, self.lamport_version, *object_digest))
375                    }
376                    _ => None,
377                }
378            })
379            .collect()
380    }
381
382    fn object_changes(&self) -> Vec<ObjectChange> {
383        self.changed_objects
384            .iter()
385            .filter_map(|(id, change)| {
386                let input_version_digest = match &change.input_state {
387                    ObjectIn::NotExist => None,
388                    ObjectIn::Exist((vd, _)) => Some(*vd),
389                };
390
391                let output_version_digest = match &change.output_state {
392                    ObjectOut::NotExist => None,
393                    ObjectOut::ObjectWrite((d, _)) => Some((self.lamport_version, *d)),
394                    ObjectOut::PackageWrite(vd) => Some(*vd),
395                    ObjectOut::AccumulatorWriteV1(_) => {
396                        return None;
397                    }
398                };
399
400                Some(ObjectChange {
401                    id: *id,
402
403                    input_version: input_version_digest.map(|k| k.0),
404                    input_digest: input_version_digest.map(|k| k.1),
405
406                    output_version: output_version_digest.map(|k| k.0),
407                    output_digest: output_version_digest.map(|k| k.1),
408
409                    id_operation: change.id_operation,
410                })
411            })
412            .collect()
413    }
414
415    fn published_packages(&self) -> Vec<ObjectID> {
416        self.changed_objects
417            .iter()
418            .filter_map(|(id, change)| {
419                if matches!(&change.output_state, ObjectOut::PackageWrite(_)) {
420                    Some(*id)
421                } else {
422                    None
423                }
424            })
425            .collect()
426    }
427
428    fn accumulator_events(&self) -> Vec<AccumulatorEvent> {
429        self.changed_objects
430            .iter()
431            .filter_map(|(id, change)| match &change.output_state {
432                ObjectOut::AccumulatorWriteV1(write) => Some(AccumulatorEvent::new(
433                    AccumulatorObjId::new_unchecked(*id),
434                    write.clone(),
435                )),
436                _ => None,
437            })
438            .collect()
439    }
440
441    fn gas_object(&self) -> Option<(ObjectRef, Owner)> {
442        self.gas_object_index.map(|index| {
443            let entry = &self.changed_objects[index as usize];
444            match &entry.1.output_state {
445                ObjectOut::ObjectWrite((digest, owner)) => {
446                    ((entry.0, self.lamport_version, *digest), owner.clone())
447                }
448                ObjectOut::NotExist => {
449                    // Gas coin was deleted. Preserve the ID but return the marker digest and a
450                    // dummy owner.
451                    (
452                        (
453                            entry.0,
454                            self.lamport_version,
455                            ObjectDigest::OBJECT_DIGEST_DELETED,
456                        ),
457                        Owner::AddressOwner(SuiAddress::default()),
458                    )
459                }
460                _ => panic!("Gas object must be an ObjectWrite or Deleted in changed_objects"),
461            }
462        })
463    }
464
465    fn events_digest(&self) -> Option<&TransactionEventsDigest> {
466        self.events_digest.as_ref()
467    }
468
469    fn dependencies(&self) -> &[TransactionDigest] {
470        &self.dependencies
471    }
472
473    fn transaction_digest(&self) -> &TransactionDigest {
474        &self.transaction_digest
475    }
476
477    fn gas_cost_summary(&self) -> &GasCostSummary {
478        &self.gas_used
479    }
480
481    fn unchanged_consensus_objects(&self) -> Vec<(ObjectID, UnchangedConsensusKind)> {
482        self.unchanged_consensus_objects.clone()
483    }
484
485    fn accumulator_updates(&self) -> Vec<(ObjectID, AccumulatorWriteV1)> {
486        self.changed_objects
487            .iter()
488            .filter_map(|(id, change)| match &change.output_state {
489                ObjectOut::AccumulatorWriteV1(update) => Some((*id, update.clone())),
490                _ => None,
491            })
492            .collect()
493    }
494
495    fn status_mut_for_testing(&mut self) -> &mut ExecutionStatus {
496        &mut self.status
497    }
498
499    fn gas_cost_summary_mut_for_testing(&mut self) -> &mut GasCostSummary {
500        &mut self.gas_used
501    }
502
503    fn transaction_digest_mut_for_testing(&mut self) -> &mut TransactionDigest {
504        &mut self.transaction_digest
505    }
506
507    fn dependencies_mut_for_testing(&mut self) -> &mut Vec<TransactionDigest> {
508        &mut self.dependencies
509    }
510
511    fn unsafe_add_input_consensus_object_for_testing(&mut self, kind: InputConsensusObject) {
512        match kind {
513            InputConsensusObject::Mutate(obj_ref) => self.changed_objects.push((
514                obj_ref.0,
515                EffectsObjectChange {
516                    input_state: ObjectIn::Exist((
517                        (obj_ref.1, obj_ref.2),
518                        Owner::Shared {
519                            initial_shared_version: OBJECT_START_VERSION,
520                        },
521                    )),
522                    output_state: ObjectOut::ObjectWrite((
523                        obj_ref.2,
524                        Owner::Shared {
525                            initial_shared_version: obj_ref.1,
526                        },
527                    )),
528                    id_operation: IDOperation::None,
529                },
530            )),
531            InputConsensusObject::ReadOnly(obj_ref) => self.unchanged_consensus_objects.push((
532                obj_ref.0,
533                UnchangedConsensusKind::ReadOnlyRoot((obj_ref.1, obj_ref.2)),
534            )),
535            InputConsensusObject::ReadConsensusStreamEnded(obj_id, seqno) => {
536                self.unchanged_consensus_objects.push((
537                    obj_id,
538                    UnchangedConsensusKind::ReadConsensusStreamEnded(seqno),
539                ))
540            }
541            InputConsensusObject::MutateConsensusStreamEnded(obj_id, seqno) => {
542                self.unchanged_consensus_objects.push((
543                    obj_id,
544                    UnchangedConsensusKind::MutateConsensusStreamEnded(seqno),
545                ))
546            }
547            InputConsensusObject::Cancelled(obj_id, seqno) => self
548                .unchanged_consensus_objects
549                .push((obj_id, UnchangedConsensusKind::Cancelled(seqno))),
550        }
551    }
552
553    fn unsafe_add_deleted_live_object_for_testing(&mut self, obj_ref: ObjectRef) {
554        self.changed_objects.push((
555            obj_ref.0,
556            EffectsObjectChange {
557                input_state: ObjectIn::Exist((
558                    (obj_ref.1, obj_ref.2),
559                    Owner::AddressOwner(SuiAddress::default()),
560                )),
561                output_state: ObjectOut::ObjectWrite((
562                    obj_ref.2,
563                    Owner::AddressOwner(SuiAddress::default()),
564                )),
565                id_operation: IDOperation::None,
566            },
567        ))
568    }
569
570    fn unsafe_add_object_tombstone_for_testing(&mut self, obj_ref: ObjectRef) {
571        self.changed_objects.push((
572            obj_ref.0,
573            EffectsObjectChange {
574                input_state: ObjectIn::Exist((
575                    (obj_ref.1, obj_ref.2),
576                    Owner::AddressOwner(SuiAddress::default()),
577                )),
578                output_state: ObjectOut::NotExist,
579                id_operation: IDOperation::Deleted,
580            },
581        ))
582    }
583}
584
585impl TransactionEffectsV2 {
586    /// Derive the unchanged consensus objects of a transaction from its shared inputs, the
587    /// per-epoch config objects it read, and the system objects it read during execution, given
588    /// the set of objects it changed.
589    pub fn compute_unchanged_consensus_objects(
590        shared_objects: Vec<SharedInput>,
591        loaded_per_epoch_config_objects: BTreeSet<ObjectID>,
592        changed_objects: &BTreeMap<ObjectID, EffectsObjectChange>,
593        loaded_system_objects: BTreeMap<ObjectID, VersionDigest>,
594    ) -> Vec<(ObjectID, UnchangedConsensusKind)> {
595        let mut unchanged_consensus_objects: Vec<_> = shared_objects
596            .into_iter()
597            .filter_map(|shared_input| match shared_input {
598                SharedInput::Existing((id, version, digest)) => {
599                    if changed_objects.contains_key(&id) {
600                        None
601                    } else {
602                        Some((id, UnchangedConsensusKind::ReadOnlyRoot((version, digest))))
603                    }
604                }
605                SharedInput::ConsensusStreamEnded((id, version, mutability, _)) => {
606                    debug_assert!(!changed_objects.contains_key(&id));
607                    match mutability {
608                        SharedObjectMutability::Mutable => Some((
609                            id,
610                            UnchangedConsensusKind::MutateConsensusStreamEnded(version),
611                        )),
612                        SharedObjectMutability::Immutable => Some((
613                            id,
614                            UnchangedConsensusKind::ReadConsensusStreamEnded(version),
615                        )),
616                        // This is current unreachable, because non exclusive writes are not exposed to
617                        // user transactions yet, and so there is no way for their inputs to be deleted.
618                        SharedObjectMutability::NonExclusiveWrite => Some((
619                            id,
620                            UnchangedConsensusKind::MutateConsensusStreamEnded(version),
621                        )),
622                    }
623                }
624                SharedInput::Cancelled((id, version)) => {
625                    debug_assert!(!changed_objects.contains_key(&id));
626                    Some((id, UnchangedConsensusKind::Cancelled(version)))
627                }
628            })
629            .chain(
630                loaded_per_epoch_config_objects
631                    .into_iter()
632                    .map(|id| (id, UnchangedConsensusKind::PerEpochConfig)),
633            )
634            .collect();
635
636        // Record system objects read during execution (e.g. the accumulator root) as read-only
637        // consensus objects, so nodes executing from these effects (checkpoint execution, crash
638        // recovery) can reproduce the read. Skip any that already appear
639        // as a changed object or as an unchanged consensus object — those versions are already
640        // recorded. Alongside each already-recorded id, keep the version (and digest) its entry
641        // carries — the input version for a changed object, the recorded version for a read-only
642        // one — so we can check it matches what the in-execution read observed.
643        let already_recorded: BTreeMap<ObjectID, Option<VersionDigest>> = changed_objects
644            .iter()
645            .map(|(id, change)| {
646                let recorded = match &change.input_state {
647                    ObjectIn::Exist((version_digest, _)) => Some(*version_digest),
648                    _ => None,
649                };
650                (*id, recorded)
651            })
652            .chain(unchanged_consensus_objects.iter().map(|(id, kind)| {
653                let recorded = match kind {
654                    UnchangedConsensusKind::ReadOnlyRoot(version_digest) => Some(*version_digest),
655                    _ => None,
656                };
657                (*id, recorded)
658            }))
659            .collect();
660        for (id, version_digest) in loaded_system_objects {
661            match already_recorded.get(&id) {
662                None => {
663                    unchanged_consensus_objects
664                        .push((id, UnchangedConsensusKind::ReadOnlyRoot(version_digest)));
665                }
666                Some(recorded) => {
667                    // The existing entry must record the same version the in-execution read
668                    // observed — both come from the version this transaction was sequenced
669                    // against. `None` means the entry's kind carries no version to compare
670                    // (e.g. a created object or a per-epoch-config read), which no implicitly
671                    // readable system object should ever coincide with.
672                    if *recorded != Some(version_digest) {
673                        mysten_common::debug_fatal!(
674                            "system object {id} read at version {version_digest:?} but its \
675                             effects entry records {recorded:?}"
676                        );
677                    }
678                }
679            }
680        }
681        unchanged_consensus_objects
682    }
683
684    pub fn new(
685        status: ExecutionStatus,
686        executed_epoch: EpochId,
687        gas_used: GasCostSummary,
688        unchanged_consensus_objects: Vec<(ObjectID, UnchangedConsensusKind)>,
689        transaction_digest: TransactionDigest,
690        lamport_version: SequenceNumber,
691        changed_objects: BTreeMap<ObjectID, EffectsObjectChange>,
692        gas_object: Option<ObjectID>,
693        events_digest: Option<TransactionEventsDigest>,
694        dependencies: Vec<TransactionDigest>,
695    ) -> Self {
696        let changed_objects: Vec<_> = changed_objects.into_iter().collect();
697
698        let gas_object_index = gas_object.map(|gas_id| {
699            changed_objects
700                .iter()
701                .position(|(id, _)| id == &gas_id)
702                .unwrap() as u32
703        });
704
705        let result = Self {
706            status,
707            executed_epoch,
708            gas_used,
709            transaction_digest,
710            lamport_version,
711            changed_objects,
712            unchanged_consensus_objects,
713            gas_object_index,
714            events_digest,
715            dependencies,
716            aux_data_digest: None,
717        };
718        #[cfg(debug_assertions)]
719        result.check_invariant();
720
721        result
722    }
723
724    /// This function demonstrates what's the invariant of the effects.
725    /// It also documents the semantics of different combinations in object changes.
726    #[cfg(debug_assertions)]
727    fn check_invariant(&self) {
728        let mut unique_ids = HashSet::new();
729        for (id, change) in &self.changed_objects {
730            assert!(unique_ids.insert(*id));
731            match (
732                &change.input_state,
733                &change.output_state,
734                &change.id_operation,
735            ) {
736                (ObjectIn::NotExist, ObjectOut::NotExist, IDOperation::Created) => {
737                    // created and then wrapped Move object.
738                }
739                (ObjectIn::NotExist, ObjectOut::NotExist, IDOperation::Deleted) => {
740                    // unwrapped and then deleted Move object.
741                }
742                (ObjectIn::NotExist, ObjectOut::ObjectWrite((_, owner)), IDOperation::None) => {
743                    // unwrapped Move object.
744                    // It's not allowed to make an object shared after unwrapping.
745                    assert!(!owner.is_shared());
746                }
747                (ObjectIn::NotExist, ObjectOut::ObjectWrite(..), IDOperation::Created) => {
748                    // created Move object.
749                }
750                (ObjectIn::NotExist, ObjectOut::PackageWrite(_), IDOperation::Created) => {
751                    // created Move package or user Move package upgrade.
752                }
753                (
754                    ObjectIn::Exist(((old_version, _), old_owner)),
755                    ObjectOut::NotExist,
756                    IDOperation::None,
757                ) => {
758                    // wrapped.
759                    assert!(old_version.value() < self.lamport_version.value());
760                    assert!(
761                        !old_owner.is_shared() && !old_owner.is_immutable(),
762                        "Cannot wrap shared or immutable object"
763                    );
764                }
765                (
766                    ObjectIn::Exist(((old_version, _), old_owner)),
767                    ObjectOut::NotExist,
768                    IDOperation::Deleted,
769                ) => {
770                    // deleted.
771                    assert!(old_version.value() < self.lamport_version.value());
772                    assert!(!old_owner.is_immutable(), "Cannot delete immutable object");
773                }
774                (
775                    ObjectIn::Exist(((old_version, old_digest), old_owner)),
776                    ObjectOut::ObjectWrite((new_digest, new_owner)),
777                    IDOperation::None,
778                ) => {
779                    // mutated.
780                    assert!(old_version.value() < self.lamport_version.value());
781                    assert_ne!(old_digest, new_digest);
782                    assert!(!old_owner.is_immutable(), "Cannot mutate immutable object");
783                    if old_owner.is_shared() {
784                        assert!(new_owner.is_shared(), "Cannot un-share an object");
785                    } else {
786                        assert!(!new_owner.is_shared(), "Cannot share an existing object");
787                    }
788                }
789                (
790                    ObjectIn::Exist(((old_version, old_digest), old_owner)),
791                    ObjectOut::PackageWrite((new_version, new_digest)),
792                    IDOperation::None,
793                ) => {
794                    // system package upgrade.
795                    assert!(
796                        old_owner.is_immutable() && is_system_package(*id),
797                        "Must be a system package"
798                    );
799                    assert_eq!(old_version.value() + 1, new_version.value());
800                    assert_ne!(old_digest, new_digest);
801                }
802                (ObjectIn::NotExist, ObjectOut::AccumulatorWriteV1(_), IDOperation::None) => {
803                    // This is an accumulator write.
804                }
805                _ => {
806                    panic!("Impossible object change: {:?}, {:?}", id, change);
807                }
808            }
809        }
810        // Make sure that gas object, if present, has an address owner.
811        if let Some((_, owner)) = self.gas_object() {
812            assert!(matches!(owner, Owner::AddressOwner(_)));
813        }
814
815        for (id, _) in &self.unchanged_consensus_objects {
816            assert!(
817                unique_ids.insert(*id),
818                "Duplicate object id: {:?}\n{:#?}",
819                id,
820                self
821            );
822        }
823    }
824}
825
826impl Default for TransactionEffectsV2 {
827    fn default() -> Self {
828        Self {
829            status: ExecutionStatus::Success,
830            executed_epoch: 0,
831            gas_used: GasCostSummary::default(),
832            transaction_digest: TransactionDigest::default(),
833            lamport_version: SequenceNumber::default(),
834            changed_objects: vec![],
835            unchanged_consensus_objects: vec![],
836            gas_object_index: None,
837            events_digest: None,
838            dependencies: vec![],
839            aux_data_digest: None,
840        }
841    }
842}
843
844#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize)]
845pub enum UnchangedConsensusKind {
846    /// Read-only consensus objects from the input. We don't really need ObjectDigest
847    /// for protocol correctness, but it will make it easier to verify untrusted read.
848    ReadOnlyRoot(VersionDigest),
849    /// Objects with ended consensus streams that appear mutably/owned in the input.
850    MutateConsensusStreamEnded(SequenceNumber),
851    /// Objects with ended consensus streams objects that appear as read-only in the input.
852    ReadConsensusStreamEnded(SequenceNumber),
853    /// Consensus objects in cancelled transaction. The sequence number embed cancellation reason.
854    Cancelled(SequenceNumber),
855    /// Read of a per-epoch config object that should remain the same during an epoch.
856    PerEpochConfig,
857}