Skip to main content

iota_config/
migration_tx_data.rs

1// Copyright (c) 2024 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{
5    collections::{BTreeMap, HashSet},
6    fs::File,
7    io::{BufReader, BufWriter},
8    path::Path,
9};
10
11use anyhow::{Context, Result};
12use iota_genesis_common::prepare_and_execute_genesis_transaction;
13use iota_sdk_types::{
14    ObjectData, TransactionDigest,
15    checkpoint::{CheckpointContents, CheckpointSummary},
16};
17use iota_types::{
18    balance::Balance,
19    effects::{TransactionEffects, TransactionEffectsAPI, TransactionEvents},
20    gas_coin::GasCoin,
21    message_envelope::Message,
22    messages_checkpoint::CheckpointContentsExt,
23    object::Object,
24    stardust::output::{AliasOutput, BasicOutput, NftOutput},
25    timelock::timelock::{TimeLock, is_timelocked_gas_balance},
26    transaction::Transaction,
27};
28use serde::{Deserialize, Serialize};
29use tracing::trace;
30
31use crate::genesis::{Genesis, GenesisCeremonyParameters, UnsignedGenesis};
32
33pub type TransactionsData =
34    BTreeMap<TransactionDigest, (Transaction, TransactionEffects, TransactionEvents)>;
35
36// Migration data from the Stardust network is loaded separately after genesis
37// to reduce the size of the genesis transaction.
38#[derive(Eq, PartialEq, Debug, Clone, Deserialize, Serialize, Default)]
39pub struct MigrationTxData {
40    inner: TransactionsData,
41}
42
43impl MigrationTxData {
44    pub fn new(txs_data: TransactionsData) -> Self {
45        Self { inner: txs_data }
46    }
47
48    pub fn txs_data(&self) -> &TransactionsData {
49        &self.inner
50    }
51
52    pub fn is_empty(&self) -> bool {
53        self.inner.is_empty()
54    }
55
56    /// Executes all the migration transactions for this migration data and
57    /// returns the vector of objects created by these executions.
58    pub fn get_objects(&self) -> impl Iterator<Item = Object> + '_ {
59        self.inner.values().flat_map(|(tx, _, _)| {
60            self.objects_by_tx_digest(*tx.digest())
61                .expect("the migration data is corrupted")
62                .into_iter()
63        })
64    }
65
66    /// Executes the migration transaction identified by `digest` and returns
67    /// the vector of objects created by the execution.
68    pub fn objects_by_tx_digest(&self, digest: TransactionDigest) -> Option<Vec<Object>> {
69        let (tx, effects, _) = self.inner.get(&digest)?;
70
71        // We use default ceremony parameters, not the real ones. This should not affect
72        // the execution of a genesis transaction.
73        let default_ceremony_parameters = GenesisCeremonyParameters::default();
74
75        // Execute the transaction
76        let (execution_effects, _, execution_objects) = prepare_and_execute_genesis_transaction(
77            default_ceremony_parameters.chain_start_timestamp_ms,
78            default_ceremony_parameters.protocol_version,
79            tx,
80        );
81
82        // Validate the results
83        assert_eq!(
84            effects.digest(),
85            execution_effects.digest(),
86            "invalid execution"
87        );
88
89        // Return
90        Some(execution_objects)
91    }
92
93    fn validate_from_genesis_components(
94        &self,
95        checkpoint: &CheckpointSummary,
96        contents: &CheckpointContents,
97        genesis_tx_digest: TransactionDigest,
98    ) -> anyhow::Result<()> {
99        anyhow::ensure!(
100            checkpoint.content_digest == contents.digest(),
101            "checkpoint's content digest is corrupted"
102        );
103        let mut validation_digests_queue: HashSet<TransactionDigest> =
104            self.inner.keys().copied().collect();
105        for exec_digest in contents.iter() {
106            // We skip the genesis transaction to process only migration transactions from
107            // the migration.blob.
108            if exec_digest.transaction == genesis_tx_digest {
109                continue;
110            }
111            let valid_tx_digest = &exec_digest.transaction;
112            let valid_effects_digest = &exec_digest.effects;
113            let (tx, effects, events) = self
114                .inner
115                .get(valid_tx_digest)
116                .ok_or(anyhow::anyhow!("missing transaction digest"))?;
117
118            if &effects.digest() != valid_effects_digest
119                || effects.transaction_digest() != valid_tx_digest
120                || &tx.data().digest() != valid_tx_digest
121            {
122                anyhow::bail!("invalid transaction or effects data");
123            }
124
125            if let Some(valid_events_digest) = effects.events_digest() {
126                if &events.digest() != valid_events_digest {
127                    anyhow::bail!("invalid events data");
128                }
129            } else if !events.is_empty() {
130                anyhow::bail!("invalid events data");
131            }
132            validation_digests_queue.remove(valid_tx_digest);
133        }
134        anyhow::ensure!(
135            validation_digests_queue.is_empty(),
136            "the migration data is corrupted"
137        );
138        Ok(())
139    }
140
141    /// Validates the content of the migration data through a `Genesis`. The
142    /// validation is based on cryptographic links (i.e., hash digests) between
143    /// transactions, transaction effects and events.
144    pub fn validate_from_genesis(&self, genesis: &Genesis) -> anyhow::Result<()> {
145        self.validate_from_genesis_components(
146            &genesis.checkpoint(),
147            genesis.checkpoint_contents(),
148            *genesis.transaction().digest(),
149        )
150    }
151
152    /// Validates the content of the migration data through an
153    /// `UnsignedGenesis`. The validation is based on cryptographic links
154    /// (i.e., hash digests) between transactions, transaction effects and
155    /// events.
156    pub fn validate_from_unsigned_genesis(
157        &self,
158        unsigned_genesis: &UnsignedGenesis,
159    ) -> anyhow::Result<()> {
160        self.validate_from_genesis_components(
161            unsigned_genesis.checkpoint(),
162            unsigned_genesis.checkpoint_contents(),
163            *unsigned_genesis.transaction().digest(),
164        )
165    }
166
167    /// Validates the total supply of the migration data adding up the amount of
168    /// gas coins found in migrated objects.
169    pub fn validate_total_supply(&self, expected_total_supply: u64) -> anyhow::Result<()> {
170        let total_supply: u64 = self
171            .get_objects()
172            .map(|object| match &object.data {
173                ObjectData::Struct(_) => GasCoin::try_from(&object)
174                    .map(|gas| gas.value())
175                    .or_else(|_| {
176                        TimeLock::<Balance>::try_from(&object).map(|t| {
177                            assert!(is_timelocked_gas_balance(
178                                &object.struct_tag().expect("should not be a package")
179                            ));
180                            t.locked().value()
181                        })
182                    })
183                    .or_else(|_| AliasOutput::try_from(&object).map(|a| a.balance.value()))
184                    .or_else(|_| BasicOutput::try_from(&object).map(|b| b.balance.value()))
185                    .or_else(|_| NftOutput::try_from(&object).map(|n| n.balance.value()))
186                    .unwrap_or(0),
187                ObjectData::Package(_) => 0,
188            })
189            .sum();
190
191        anyhow::ensure!(
192            total_supply == expected_total_supply,
193            "the migration data total supply of {total_supply} does not match the expected total supply of {expected_total_supply}"
194        );
195        Ok(())
196    }
197
198    /// Loads a `MigrationTxData` in memory from a file found in `path`.
199    pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, anyhow::Error> {
200        let path = path.as_ref();
201        trace!("reading Migration transaction data from {}", path.display());
202        let read = File::open(path).with_context(|| {
203            format!(
204                "unable to load Migration transaction data from {}",
205                path.display()
206            )
207        })?;
208        bcs::from_reader(BufReader::new(read)).with_context(|| {
209            format!(
210                "unable to parse Migration transaction data from {}",
211                path.display()
212            )
213        })
214    }
215
216    /// Saves a `MigrationTxData` from memory into a file in `path`.
217    pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<(), anyhow::Error> {
218        let path = path.as_ref();
219        trace!("writing Migration transaction data to {}", path.display());
220        let mut write = BufWriter::new(File::create(path)?);
221        bcs::serialize_into(&mut write, &self).with_context(|| {
222            format!(
223                "unable to save Migration transaction data to {}",
224                path.display()
225            )
226        })?;
227        Ok(())
228    }
229}