Skip to main content

iota_types/
full_checkpoint_content.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::collections::BTreeMap;
6
7use iota_sdk_types::{ObjectId, ObjectReference, TransactionKind};
8use serde::{Deserialize, Serialize};
9use tap::Pipe;
10
11use crate::{
12    effects::{
13        TransactionEffects, TransactionEffectsAPI, TransactionEffectsExt, TransactionEvents,
14    },
15    iota_system_state::{IotaSystemStateTrait, get_iota_system_state},
16    messages_checkpoint::{CertifiedCheckpointSummary, CheckpointContents},
17    object::Object,
18    storage::{BackingPackageStore, EpochInfo, error::Error as StorageError},
19    transaction::{Transaction, TransactionDataAPI},
20};
21
22#[derive(Clone, Debug, Serialize, Deserialize)]
23pub struct CheckpointData {
24    pub checkpoint_summary: CertifiedCheckpointSummary,
25    pub checkpoint_contents: CheckpointContents,
26    pub transactions: Vec<CheckpointTransaction>,
27}
28
29impl CheckpointData {
30    // returns the latest versions of the output objects that still exist at the end
31    // of the checkpoint
32    pub fn latest_live_output_objects(&self) -> Vec<&Object> {
33        let mut latest_live_objects = BTreeMap::new();
34        for tx in self.transactions.iter() {
35            for obj in tx.output_objects.iter() {
36                latest_live_objects.insert(obj.id(), obj);
37            }
38            for obj_ref in tx.removed_object_refs_post_version() {
39                latest_live_objects.remove(&obj_ref.object_id);
40            }
41        }
42        latest_live_objects.into_values().collect()
43    }
44
45    // returns the object refs that are eventually deleted or wrapped in the current
46    // checkpoint
47    pub fn eventually_removed_object_refs_post_version(&self) -> Vec<ObjectReference> {
48        let mut eventually_removed_object_refs = BTreeMap::new();
49        for tx in self.transactions.iter() {
50            for obj_ref in tx.removed_object_refs_post_version() {
51                eventually_removed_object_refs.insert(obj_ref.object_id, obj_ref);
52            }
53            for obj in tx.output_objects.iter() {
54                eventually_removed_object_refs.remove(&(obj.id()));
55            }
56        }
57        eventually_removed_object_refs.into_values().collect()
58    }
59
60    pub fn all_objects(&self) -> Vec<&Object> {
61        self.transactions
62            .iter()
63            .flat_map(|tx| &tx.input_objects)
64            .chain(self.transactions.iter().flat_map(|tx| &tx.output_objects))
65            .collect()
66    }
67
68    /// The transaction that closes this checkpoint's epoch — the `AdvanceEpoch`
69    /// / `advance_epoch_safe_mode` transaction. `None` if this isn't an
70    /// epoch-boundary checkpoint (genesis included), or its last transaction
71    /// unexpectedly isn't an end-of-epoch transaction.
72    pub fn end_of_epoch_transaction(&self) -> Option<&CheckpointTransaction> {
73        // Guard: only epoch-boundary checkpoints carry a closing tx — bail otherwise.
74        self.checkpoint_summary.end_of_epoch_data.as_ref()?;
75        // The epoch-change tx is always ordered last, after every user tx;
76        // verify rather than assume, since callers treat `None` as a hard error.
77        let transaction = self.transactions.last()?;
78        transaction
79            .transaction
80            .intent_message()
81            .value
82            .is_end_of_epoch_tx()
83            .then_some(transaction)
84    }
85
86    /// Returns the epoch boundary information for this checkpoint, paired
87    /// with the events of the transaction that produced this epoch's start
88    /// system state (`EndOfEpoch` for non-genesis checkpoints, `Genesis`
89    /// for checkpoint 0).
90    /// Returns `None` for non-epoch-boundary checkpoints.
91    pub fn epoch_info(
92        &self,
93    ) -> Result<Option<(EpochInfo, Option<TransactionEvents>)>, StorageError> {
94        // If there is no end of epoch data, return None, except for checkpoint 0
95        if self.checkpoint_summary.end_of_epoch_data.is_none()
96            && self.checkpoint_summary.sequence_number != 0
97        {
98            return Ok(None);
99        }
100
101        let (start_checkpoint, transaction) = if self.checkpoint_summary.sequence_number != 0 {
102            let Some(transaction) = self.end_of_epoch_transaction() else {
103                return Err(StorageError::custom(format!(
104                    "Failed to get end of epoch transaction in checkpoint {} with EndOfEpochData",
105                    self.checkpoint_summary.sequence_number,
106                )));
107            };
108            (self.checkpoint_summary.sequence_number + 1, transaction)
109        } else {
110            // For checkpoint 0, we look for the genesis transaction
111            let Some(transaction) = self.transactions.iter().find(|tx| {
112                matches!(
113                    tx.transaction.intent_message().value.kind(),
114                    TransactionKind::Genesis(_)
115                )
116            }) else {
117                return Err(StorageError::custom(format!(
118                    "Failed to get genesis transaction in checkpoint {}",
119                    self.checkpoint_summary.sequence_number,
120                )));
121            };
122            (0, transaction)
123        };
124
125        let system_state =
126            get_iota_system_state(&transaction.output_objects.as_slice()).map_err(|e| {
127                StorageError::custom(format!(
128                    "Failed to find system state object output from end of epoch or genesis transaction: {e}"
129                ))
130            })?;
131
132        Ok(Some((
133            EpochInfo {
134                epoch: system_state.epoch(),
135                protocol_version: system_state.protocol_version(),
136                start_timestamp_ms: system_state.epoch_start_timestamp_ms(),
137                end_timestamp_ms: None,
138                start_checkpoint,
139                end_checkpoint: None,
140                reference_gas_price: system_state.reference_gas_price(),
141                system_state,
142            },
143            transaction.events.clone(),
144        )))
145    }
146}
147
148#[derive(Clone, Debug, Serialize, Deserialize)]
149pub struct CheckpointTransaction {
150    /// The input Transaction
151    pub transaction: Transaction,
152    /// The effects produced by executing this transaction
153    pub effects: TransactionEffects,
154    /// The events, if any, emitted by this transaction during execution
155    pub events: Option<TransactionEvents>,
156    /// The state of all inputs to this transaction as they were prior to
157    /// execution.
158    pub input_objects: Vec<Object>,
159    /// The state of all output objects created or mutated or unwrapped by this
160    /// transaction.
161    pub output_objects: Vec<Object>,
162}
163
164impl CheckpointTransaction {
165    // provide an iterator over all deleted or wrapped objects in this transaction
166    pub fn removed_objects_pre_version(&self) -> impl Iterator<Item = &Object> {
167        // Since each object ID can only show up once in the input_objects, we can just
168        // use the ids of deleted and wrapped objects to lookup the object in
169        // the input_objects.
170        self.effects
171            .all_removed_objects()
172            .into_iter() // Use id and version to lookup in input Objects
173            .map(|(object_ref, _)| {
174                self.input_objects
175                    .iter()
176                    .find(|o| o.id() == object_ref.object_id)
177                    .expect("all removed objects should show up in input objects")
178            })
179    }
180
181    pub fn removed_object_refs_post_version(&self) -> impl Iterator<Item = ObjectReference> {
182        let deleted = self.effects.deleted().into_iter();
183        let wrapped = self.effects.wrapped().into_iter();
184        let unwrapped_then_deleted = self.effects.unwrapped_then_deleted().into_iter();
185        deleted.chain(wrapped).chain(unwrapped_then_deleted)
186    }
187
188    pub fn changed_objects(&self) -> impl Iterator<Item = (&Object, Option<&Object>)> {
189        self.effects
190            .all_changed_objects()
191            .into_iter()
192            .map(|(object_ref, ..)| {
193                let object = self
194                    .output_objects
195                    .iter()
196                    .find(|o| o.id() == object_ref.object_id)
197                    .expect("changed objects should show up in output objects");
198
199                let old_object = self
200                    .input_objects
201                    .iter()
202                    .find(|o| o.id() == object_ref.object_id);
203
204                (object, old_object)
205            })
206    }
207
208    pub fn created_objects(&self) -> impl Iterator<Item = &Object> {
209        // Iterator over (ObjectId, version) for created objects
210        self.effects
211            .created()
212            .into_iter()
213            // Lookup Objects in output Objects as well as old versions for mutated objects
214            .map(|(object_ref, _)| {
215                self.output_objects
216                    .iter()
217                    .find(|o| o.id() == object_ref.object_id && o.version() == object_ref.version)
218                    .expect("created objects should show up in output objects")
219            })
220    }
221}
222
223impl BackingPackageStore for CheckpointData {
224    fn get_package_object(
225        &self,
226        package_id: &ObjectId,
227    ) -> crate::error::IotaResult<Option<crate::storage::PackageObject>> {
228        self.transactions
229            .iter()
230            .flat_map(|transaction| transaction.output_objects.iter())
231            .find(|object| object.is_package() && &object.id() == package_id)
232            .cloned()
233            .map(crate::storage::PackageObject::new)
234            .pipe(Ok)
235    }
236}