iota_config/
migration_tx_data.rs1use 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#[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 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 pub fn objects_by_tx_digest(&self, digest: TransactionDigest) -> Option<Vec<Object>> {
69 let (tx, effects, _) = self.inner.get(&digest)?;
70
71 let default_ceremony_parameters = GenesisCeremonyParameters::default();
74
75 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 assert_eq!(
84 effects.digest(),
85 execution_effects.digest(),
86 "invalid execution"
87 );
88
89 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 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 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 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 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 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 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}