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