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