Skip to main content

iota_types/effects/
v1.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2026 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::collections::{BTreeMap, BTreeSet};
6
7use iota_sdk_types::Digest;
8
9use super::{
10    EffectsObjectChange, EpochId, ExecutionStatus, GasCostSummary, IDOperation, InputSharedObject,
11    ObjectChange, ObjectId, ObjectIn, ObjectOut, ObjectRef, Owner, TransactionEffectsV1,
12    UnchangedSharedKind, UnchangedSharedObject, Version,
13};
14use crate::{
15    IotaAddress,
16    digests::{TransactionDigest, TransactionEventsDigest},
17    effects::{TransactionEffectsAPI, TransactionEffectsAPIForTesting},
18    execution::SharedInput,
19    object::OBJECT_START_VERSION,
20};
21
22impl TransactionEffectsAPI for TransactionEffectsV1 {
23    fn status(&self) -> &ExecutionStatus {
24        &self.status
25    }
26
27    fn into_status(self) -> ExecutionStatus {
28        self.status
29    }
30
31    fn epoch(&self) -> EpochId {
32        self.epoch
33    }
34
35    fn modified_at_versions(&self) -> Vec<(ObjectId, Version)> {
36        self.changed_objects
37            .iter()
38            .filter_map(|change| {
39                if let ObjectIn::Data { version, .. } = &change.input_state {
40                    Some((change.object_id, *version))
41                } else {
42                    None
43                }
44            })
45            .collect()
46    }
47
48    fn lamport_version(&self) -> Version {
49        self.lamport_version
50    }
51
52    fn old_object_metadata(&self) -> Vec<(ObjectRef, Owner)> {
53        self.changed_objects
54            .iter()
55            .filter_map(|change| {
56                if let ObjectIn::Data {
57                    version,
58                    digest,
59                    owner,
60                } = change.input_state
61                {
62                    Some((ObjectRef::new(change.object_id, version, digest), owner))
63                } else {
64                    None
65                }
66            })
67            .collect()
68    }
69
70    fn input_shared_objects(&self) -> Vec<InputSharedObject> {
71        self.changed_objects
72            .iter()
73            .filter_map(|changed| {
74                if let ObjectIn::Data {
75                    version,
76                    digest,
77                    owner: Owner::Shared { .. },
78                } = changed.input_state
79                {
80                    Some(InputSharedObject::Mutate(ObjectRef::new(
81                        changed.object_id,
82                        version,
83                        digest,
84                    )))
85                } else {
86                    None
87                }
88            })
89            .chain(self.unchanged_shared_objects.iter().filter_map(
90                |unchanged| match unchanged.kind {
91                    UnchangedSharedKind::ReadOnlyRoot { version, digest } => {
92                        Some(InputSharedObject::ReadOnly(ObjectRef::new(
93                            unchanged.object_id,
94                            version,
95                            digest,
96                        )))
97                    }
98                    UnchangedSharedKind::MutateDeleted { version } => Some(
99                        InputSharedObject::MutateDeleted(unchanged.object_id, version),
100                    ),
101                    UnchangedSharedKind::ReadDeleted { version } => {
102                        Some(InputSharedObject::ReadDeleted(unchanged.object_id, version))
103                    }
104                    UnchangedSharedKind::Cancelled { version } => {
105                        Some(InputSharedObject::Cancelled(unchanged.object_id, version))
106                    }
107                    // We can not expose the per epoch config object as input shared object,
108                    // since it does not require sequencing, and hence shall not be considered
109                    // as a normal input shared object.
110                    UnchangedSharedKind::PerEpochConfig => None,
111                    _ => unimplemented!(
112                        "a new UnchangedSharedKind enum variant was added and needs to be handled"
113                    ),
114                },
115            ))
116            .collect()
117    }
118
119    fn created(&self) -> Vec<(ObjectRef, Owner)> {
120        self.changed_objects
121            .iter()
122            .filter_map(|changed| {
123                match (
124                    &changed.input_state,
125                    &changed.output_state,
126                    &changed.id_operation,
127                ) {
128                    (
129                        ObjectIn::Missing,
130                        ObjectOut::ObjectWrite { digest, owner },
131                        IDOperation::Created,
132                    ) => Some((
133                        ObjectRef::new(changed.object_id, self.lamport_version, *digest),
134                        *owner,
135                    )),
136                    (
137                        ObjectIn::Missing,
138                        ObjectOut::PackageWrite { version, digest },
139                        IDOperation::Created,
140                    ) => Some((
141                        ObjectRef::new(changed.object_id, *version, *digest),
142                        Owner::Immutable,
143                    )),
144                    _ => None,
145                }
146            })
147            .collect()
148    }
149
150    fn mutated(&self) -> Vec<(ObjectRef, Owner)> {
151        self.changed_objects
152            .iter()
153            .filter_map(
154                |changed| match (&changed.input_state, &changed.output_state) {
155                    (ObjectIn::Data { .. }, ObjectOut::ObjectWrite { digest, owner }) => Some((
156                        ObjectRef::new(changed.object_id, self.lamport_version, *digest),
157                        *owner,
158                    )),
159                    (ObjectIn::Data { .. }, ObjectOut::PackageWrite { version, digest }) => Some((
160                        ObjectRef::new(changed.object_id, *version, *digest),
161                        Owner::Immutable,
162                    )),
163                    _ => None,
164                },
165            )
166            .collect()
167    }
168
169    fn unwrapped(&self) -> Vec<(ObjectRef, Owner)> {
170        self.changed_objects
171            .iter()
172            .filter_map(|changed| {
173                match (
174                    &changed.input_state,
175                    &changed.output_state,
176                    &changed.id_operation,
177                ) {
178                    (
179                        ObjectIn::Missing,
180                        ObjectOut::ObjectWrite { digest, owner },
181                        IDOperation::None,
182                    ) => Some((
183                        ObjectRef::new(changed.object_id, self.lamport_version, *digest),
184                        *owner,
185                    )),
186                    _ => None,
187                }
188            })
189            .collect()
190    }
191
192    fn deleted(&self) -> Vec<ObjectRef> {
193        self.changed_objects
194            .iter()
195            .filter_map(|changed| {
196                match (
197                    &changed.input_state,
198                    &changed.output_state,
199                    &changed.id_operation,
200                ) {
201                    (ObjectIn::Data { .. }, ObjectOut::Missing, IDOperation::Deleted) => {
202                        Some(ObjectRef::new(
203                            changed.object_id,
204                            self.lamport_version,
205                            Digest::OBJECT_DELETED,
206                        ))
207                    }
208                    _ => None,
209                }
210            })
211            .collect()
212    }
213
214    fn unwrapped_then_deleted(&self) -> Vec<ObjectRef> {
215        self.changed_objects
216            .iter()
217            .filter_map(|changed| {
218                match (
219                    &changed.input_state,
220                    &changed.output_state,
221                    &changed.id_operation,
222                ) {
223                    (ObjectIn::Missing, ObjectOut::Missing, IDOperation::Deleted) => {
224                        Some(ObjectRef::new(
225                            changed.object_id,
226                            self.lamport_version,
227                            Digest::OBJECT_DELETED,
228                        ))
229                    }
230                    _ => None,
231                }
232            })
233            .collect()
234    }
235
236    fn wrapped(&self) -> Vec<ObjectRef> {
237        self.changed_objects
238            .iter()
239            .filter_map(|changed| {
240                match (
241                    &changed.input_state,
242                    &changed.output_state,
243                    &changed.id_operation,
244                ) {
245                    (ObjectIn::Data { .. }, ObjectOut::Missing, IDOperation::None) => {
246                        Some(ObjectRef::new(
247                            changed.object_id,
248                            self.lamport_version,
249                            Digest::OBJECT_WRAPPED,
250                        ))
251                    }
252                    _ => None,
253                }
254            })
255            .collect()
256    }
257
258    fn object_changes(&self) -> Vec<ObjectChange> {
259        self.changed_objects
260            .iter()
261            .map(|changed| {
262                let input_version_digest = match &changed.input_state {
263                    ObjectIn::Missing => None,
264                    ObjectIn::Data {
265                        version, digest, ..
266                    } => Some((version, digest)),
267                    _ => unimplemented!(
268                        "a new ObjectIn enum variant was added and needs to be handled"
269                    ),
270                };
271
272                let output_version_digest = match &changed.output_state {
273                    ObjectOut::Missing => None,
274                    ObjectOut::ObjectWrite { digest, .. } => Some((&self.lamport_version, digest)),
275                    ObjectOut::PackageWrite { version, digest } => Some((version, digest)),
276                    _ => unimplemented!(
277                        "a new ObjectOut enum variant was added and needs to be handled"
278                    ),
279                };
280
281                ObjectChange {
282                    id: changed.object_id,
283                    input_version: input_version_digest.map(|k| *k.0),
284                    input_digest: input_version_digest.map(|k| *k.1),
285                    output_version: output_version_digest.map(|k| *k.0),
286                    output_digest: output_version_digest.map(|k| *k.1),
287                    id_operation: changed.id_operation,
288                }
289            })
290            .collect()
291    }
292
293    fn gas_object(&self) -> (ObjectRef, Owner) {
294        if let Some(gas_object_index) = self.gas_object_index {
295            let changed = &self.changed_objects[gas_object_index as usize];
296            match changed.output_state {
297                ObjectOut::ObjectWrite { digest, owner } => (
298                    ObjectRef::new(changed.object_id, self.lamport_version, digest),
299                    owner,
300                ),
301                _ => panic!("Gas object must be an ObjectWrite in changed_objects"),
302            }
303        } else {
304            (
305                ObjectRef::new(ObjectId::ZERO, Version::default(), Digest::MIN),
306                Owner::Address(IotaAddress::ZERO),
307            )
308        }
309    }
310
311    fn events_digest(&self) -> Option<&TransactionEventsDigest> {
312        self.events_digest.as_ref()
313    }
314
315    fn dependencies(&self) -> &[TransactionDigest] {
316        &self.dependencies
317    }
318
319    fn transaction_digest(&self) -> &TransactionDigest {
320        &self.transaction_digest
321    }
322
323    fn gas_cost_summary(&self) -> &GasCostSummary {
324        &self.gas_cost_summary
325    }
326
327    fn unchanged_shared_objects(&self) -> Vec<(ObjectId, UnchangedSharedKind)> {
328        self.unchanged_shared_objects
329            .iter()
330            .map(|unchanged| (unchanged.object_id, unchanged.kind.clone()))
331            .collect()
332    }
333}
334
335impl TransactionEffectsAPIForTesting for TransactionEffectsV1 {
336    fn status_mut_for_testing(&mut self) -> &mut ExecutionStatus {
337        &mut self.status
338    }
339
340    fn gas_cost_summary_mut_for_testing(&mut self) -> &mut GasCostSummary {
341        &mut self.gas_cost_summary
342    }
343
344    fn transaction_digest_mut_for_testing(&mut self) -> &mut TransactionDigest {
345        &mut self.transaction_digest
346    }
347
348    fn dependencies_mut_for_testing(&mut self) -> &mut Vec<TransactionDigest> {
349        &mut self.dependencies
350    }
351
352    fn unsafe_add_input_shared_object_for_testing(&mut self, kind: InputSharedObject) {
353        match kind {
354            InputSharedObject::Mutate(object_ref) => {
355                let (object_id, version, digest) = object_ref.into_parts();
356                self.changed_objects.push(EffectsObjectChange {
357                    object_id,
358                    input_state: ObjectIn::Data {
359                        version,
360                        digest,
361                        owner: Owner::Shared(OBJECT_START_VERSION),
362                    },
363                    output_state: ObjectOut::ObjectWrite {
364                        digest,
365                        owner: Owner::Shared(version),
366                    },
367                    id_operation: IDOperation::None,
368                })
369            }
370            InputSharedObject::ReadOnly(object_ref) => {
371                let (object_id, version, digest) = object_ref.into_parts();
372                self.unchanged_shared_objects.push(UnchangedSharedObject {
373                    object_id,
374                    kind: UnchangedSharedKind::ReadOnlyRoot { version, digest },
375                })
376            }
377            InputSharedObject::ReadDeleted(object_id, version) => {
378                self.unchanged_shared_objects.push(UnchangedSharedObject {
379                    object_id,
380                    kind: UnchangedSharedKind::ReadDeleted { version },
381                })
382            }
383            InputSharedObject::MutateDeleted(object_id, version) => {
384                self.unchanged_shared_objects.push(UnchangedSharedObject {
385                    object_id,
386                    kind: UnchangedSharedKind::MutateDeleted { version },
387                })
388            }
389            InputSharedObject::Cancelled(object_id, version) => {
390                self.unchanged_shared_objects.push(UnchangedSharedObject {
391                    object_id,
392                    kind: UnchangedSharedKind::Cancelled { version },
393                })
394            }
395        }
396    }
397
398    fn unsafe_add_deleted_live_object_for_testing(&mut self, object_ref: ObjectRef) {
399        let (object_id, version, digest) = object_ref.into_parts();
400        self.changed_objects.push(EffectsObjectChange {
401            object_id,
402            input_state: ObjectIn::Data {
403                version,
404                digest,
405                owner: Owner::Address(IotaAddress::ZERO),
406            },
407            output_state: ObjectOut::ObjectWrite {
408                digest,
409                owner: Owner::Address(IotaAddress::ZERO),
410            },
411            id_operation: IDOperation::None,
412        })
413    }
414
415    fn unsafe_add_object_tombstone_for_testing(&mut self, object_ref: ObjectRef) {
416        let (object_id, version, digest) = object_ref.into_parts();
417        self.changed_objects.push(EffectsObjectChange {
418            object_id,
419            input_state: ObjectIn::Data {
420                version,
421                digest,
422                owner: Owner::Address(IotaAddress::ZERO),
423            },
424            output_state: ObjectOut::Missing,
425            id_operation: IDOperation::Deleted,
426        })
427    }
428}
429
430pub(crate) fn new_from_execution(
431    status: ExecutionStatus,
432    epoch: EpochId,
433    gas_cost_summary: GasCostSummary,
434    shared_objects: Vec<SharedInput>,
435    loaded_per_epoch_config_objects: BTreeSet<ObjectId>,
436    transaction_digest: TransactionDigest,
437    lamport_version: Version,
438    changed_objects: BTreeMap<ObjectId, EffectsObjectChange>,
439    gas_object: Option<ObjectId>,
440    events_digest: Option<TransactionEventsDigest>,
441    dependencies: Vec<TransactionDigest>,
442) -> TransactionEffectsV1 {
443    let unchanged_shared_objects = shared_objects
444        .into_iter()
445        .filter_map(|shared_input| match shared_input {
446            SharedInput::Existing(ObjectRef {
447                object_id: id,
448                version,
449                digest,
450            }) => {
451                if changed_objects.contains_key(&id) {
452                    None
453                } else {
454                    Some((id, UnchangedSharedKind::ReadOnlyRoot { version, digest }))
455                }
456            }
457            SharedInput::Deleted((id, version, mutable, _)) => {
458                debug_assert!(!changed_objects.contains_key(&id));
459                if mutable {
460                    Some((id, UnchangedSharedKind::MutateDeleted { version }))
461                } else {
462                    Some((id, UnchangedSharedKind::ReadDeleted { version }))
463                }
464            }
465            SharedInput::Cancelled((id, version)) => {
466                debug_assert!(!changed_objects.contains_key(&id));
467                Some((id, UnchangedSharedKind::Cancelled { version }))
468            }
469        })
470        .chain(
471            loaded_per_epoch_config_objects
472                .into_iter()
473                .map(|id| (id, UnchangedSharedKind::PerEpochConfig)),
474        )
475        .map(|(object_id, kind)| UnchangedSharedObject { object_id, kind })
476        .collect();
477
478    let changed_objects: Vec<_> = changed_objects.into_values().collect();
479
480    let gas_object_index = gas_object.map(|gas_id| {
481        changed_objects
482            .iter()
483            .position(|changed| changed.object_id == gas_id)
484            .unwrap() as u32
485    });
486
487    let v1 = TransactionEffectsV1 {
488        status,
489        epoch,
490        gas_cost_summary,
491        transaction_digest,
492        lamport_version,
493        changed_objects,
494        unchanged_shared_objects,
495        gas_object_index,
496        events_digest,
497        dependencies,
498        auxiliary_data_digest: None,
499    };
500
501    #[cfg(debug_assertions)]
502    check_invariant(&v1);
503
504    v1
505}
506
507/// This function demonstrates what's the invariant of the effects.
508/// It also documents the semantics of different combinations in object
509/// changes.
510#[cfg(debug_assertions)]
511fn check_invariant(v1: &TransactionEffectsV1) {
512    use std::collections::HashSet;
513
514    let mut unique_ids = HashSet::new();
515    for changed in &v1.changed_objects {
516        let id = &changed.object_id;
517        assert!(unique_ids.insert(*id));
518        match (
519            &changed.input_state,
520            &changed.output_state,
521            &changed.id_operation,
522        ) {
523            (ObjectIn::Missing, ObjectOut::Missing, IDOperation::Created) => {
524                // created and then wrapped Move object.
525            }
526            (ObjectIn::Missing, ObjectOut::Missing, IDOperation::Deleted) => {
527                // unwrapped and then deleted Move object.
528            }
529            (ObjectIn::Missing, ObjectOut::ObjectWrite { owner, .. }, IDOperation::None) => {
530                // unwrapped Move object.
531                // It's not allowed to make an object shared after unwrapping.
532                assert!(!owner.is_shared());
533            }
534            (ObjectIn::Missing, ObjectOut::ObjectWrite { .. }, IDOperation::Created) => {
535                // created Move object.
536            }
537            (ObjectIn::Missing, ObjectOut::PackageWrite { .. }, IDOperation::Created) => {
538                // created Move package or user Move package upgrade.
539            }
540            (
541                ObjectIn::Data {
542                    version: old_version,
543                    owner: old_owner,
544                    ..
545                },
546                ObjectOut::Missing,
547                IDOperation::None,
548            ) => {
549                // wrapped.
550                assert!(*old_version < v1.lamport_version);
551                assert!(
552                    !old_owner.is_shared() && !old_owner.is_immutable(),
553                    "Cannot wrap shared or immutable object"
554                );
555            }
556            (
557                ObjectIn::Data {
558                    version: old_version,
559                    owner: old_owner,
560                    ..
561                },
562                ObjectOut::Missing,
563                IDOperation::Deleted,
564            ) => {
565                // deleted.
566                assert!(*old_version < v1.lamport_version);
567                assert!(!old_owner.is_immutable(), "Cannot delete immutable object");
568            }
569            (
570                ObjectIn::Data {
571                    version: old_version,
572                    digest: old_digest,
573                    owner: old_owner,
574                },
575                ObjectOut::ObjectWrite {
576                    digest: new_digest,
577                    owner: new_owner,
578                    ..
579                },
580                IDOperation::None,
581            ) => {
582                // mutated.
583                assert!(*old_version < v1.lamport_version);
584                assert_ne!(old_digest, new_digest);
585                assert!(!old_owner.is_immutable(), "Cannot mutate immutable object");
586                if old_owner.is_shared() {
587                    assert!(new_owner.is_shared(), "Cannot un-share an object");
588                } else {
589                    assert!(!new_owner.is_shared(), "Cannot share an existing object");
590                }
591            }
592            (
593                ObjectIn::Data {
594                    version: old_version,
595                    digest: old_digest,
596                    owner: old_owner,
597                },
598                ObjectOut::PackageWrite {
599                    version: new_version,
600                    digest: new_digest,
601                    ..
602                },
603                IDOperation::None,
604            ) => {
605                // system package upgrade.
606                assert!(
607                    old_owner.is_immutable() && id.is_system_package(),
608                    "Must be a system package"
609                );
610                assert_eq!(*old_version + 1, *new_version);
611                assert_ne!(old_digest, new_digest);
612            }
613            _ => {
614                panic!("Impossible object change: {id:?}, {changed:?}");
615            }
616        }
617    }
618
619    // Make sure that gas object exists in changed_objects.
620    let (_, owner) = v1.gas_object();
621    assert!(matches!(owner, Owner::Address(_)));
622
623    for unchanged in &v1.unchanged_shared_objects {
624        let id = &unchanged.object_id;
625        assert!(
626            unique_ids.insert(*id),
627            "Duplicate object id: {id:?}\n{v1:#?}"
628        );
629    }
630}