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