iota_genesis_builder/stardust/migration/verification/
util.rs

1// Copyright (c) 2024 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use std::collections::HashMap;
5
6use anyhow::{Result, anyhow, bail, ensure};
7use iota_sdk::{
8    U256,
9    types::block::{
10        address::Address,
11        output::{self as sdk_output, NativeTokens, OutputId, TokenId},
12    },
13};
14use iota_types::{
15    TypeTag,
16    balance::Balance,
17    base_types::{IotaAddress, ObjectID},
18    coin::Coin,
19    collection_types::Bag,
20    dynamic_field::Field,
21    in_memory_storage::InMemoryStorage,
22    object::{Object, Owner},
23    stardust::{
24        output::{Alias, Nft, unlock_conditions},
25        stardust_to_iota_address,
26    },
27};
28use tracing::warn;
29
30use crate::stardust::{
31    migration::executor::FoundryLedgerData,
32    types::{address_swap_map::AddressSwapMap, token_scheme::MAX_ALLOWED_U64_SUPPLY},
33};
34
35pub const BASE_TOKEN_KEY: &str = "base_token";
36
37/// Counter used to count the generated tokens amounts.
38pub(super) struct TokensAmountCounter {
39    // A map of token type -> (real_generated_supply, expected_circulating_supply)
40    inner: HashMap<String, (u64, u64)>,
41}
42
43impl TokensAmountCounter {
44    /// Setup the tokens amount counter.
45    pub(super) fn new(initial_iota_supply: u64) -> Self {
46        let mut res = TokensAmountCounter {
47            inner: HashMap::new(),
48        };
49        res.update_total_value_max_for_iota(initial_iota_supply);
50        res
51    }
52
53    pub(super) fn into_inner(self) -> impl IntoIterator<Item = (String, (u64, u64))> {
54        self.inner
55    }
56
57    pub(super) fn update_total_value_for_iota(&mut self, value: u64) {
58        self.update_total_value(BASE_TOKEN_KEY, value);
59    }
60
61    fn update_total_value_max_for_iota(&mut self, max: u64) {
62        self.update_total_value_max(BASE_TOKEN_KEY, max);
63    }
64
65    pub(super) fn update_total_value(&mut self, key: &str, value: u64) {
66        self.inner
67            .entry(key.to_string())
68            .and_modify(|v| v.0 += value)
69            .or_insert((value, 0));
70    }
71
72    pub(super) fn update_total_value_max(&mut self, key: &str, max: u64) {
73        self.inner
74            .entry(key.to_string())
75            .and_modify(|v| v.1 = max)
76            .or_insert((0, max));
77    }
78}
79
80pub(super) fn verify_native_tokens<NtKind: NativeTokenKind>(
81    native_tokens: &NativeTokens,
82    foundry_data: &HashMap<TokenId, FoundryLedgerData>,
83    native_tokens_bag: impl Into<Option<Bag>>,
84    created_native_tokens: Option<&[ObjectID]>,
85    storage: &InMemoryStorage,
86    tokens_counter: &mut TokensAmountCounter,
87) -> Result<()> {
88    // Token types should be unique as the token ID is guaranteed unique within
89    // NativeTokens
90    let created_native_tokens = created_native_tokens
91        .map(|object_ids| {
92            object_ids
93                .iter()
94                .map(|id| {
95                    let obj = storage
96                        .get_object(id)
97                        .ok_or_else(|| anyhow!("missing native token field for {id}"))?;
98                    NtKind::from_object(obj).map(|nt| (nt.bag_key(), nt.value()))
99                })
100                .collect::<Result<HashMap<String, u64>, _>>()
101        })
102        .unwrap_or(Ok(HashMap::new()))?;
103
104    ensure!(
105        created_native_tokens.len() == native_tokens.len(),
106        "native token count mismatch: found {}, expected: {}",
107        created_native_tokens.len(),
108        native_tokens.len(),
109    );
110
111    if let Some(native_tokens_bag) = native_tokens_bag.into() {
112        ensure!(
113            native_tokens_bag.size == native_tokens.len() as u64,
114            "native tokens bag length mismatch: found {}, expected {}",
115            native_tokens_bag.size,
116            native_tokens.len()
117        );
118    }
119
120    for native_token in native_tokens.iter() {
121        let foundry_data = foundry_data
122            .get(native_token.token_id())
123            .ok_or_else(|| anyhow!("missing foundry data for token {}", native_token.token_id()))?;
124
125        let expected_bag_key = foundry_data.to_canonical_string(/* with_prefix */ false);
126        // The token amounts are scaled so that the total circulating supply does not
127        // exceed `u64::MAX`
128        let reduced_amount = foundry_data
129            .token_scheme_u64
130            .adjust_tokens(native_token.amount());
131
132        if let Some(&created_value) = created_native_tokens.get(&expected_bag_key) {
133            ensure!(
134                created_value == reduced_amount,
135                "created token amount mismatch: found {created_value}, expected {reduced_amount}"
136            );
137            tokens_counter.update_total_value(&expected_bag_key, created_value);
138        } else {
139            bail!(
140                "native token object was not created for token: {}",
141                native_token.token_id()
142            );
143        }
144    }
145
146    Ok(())
147}
148
149pub(super) fn verify_storage_deposit_unlock_condition(
150    original: Option<&sdk_output::unlock_condition::StorageDepositReturnUnlockCondition>,
151    created: Option<&unlock_conditions::StorageDepositReturnUnlockCondition>,
152) -> Result<()> {
153    // Storage Deposit Return Unlock Condition
154    if let Some(sdruc) = original {
155        let iota_return_address = stardust_to_iota_address(sdruc.return_address())?;
156        if let Some(obj_sdruc) = created {
157            ensure!(
158                obj_sdruc.return_address == iota_return_address,
159                "storage deposit return address mismatch: found {}, expected {}",
160                obj_sdruc.return_address,
161                iota_return_address
162            );
163            ensure!(
164                obj_sdruc.return_amount == sdruc.amount(),
165                "storage deposit return amount mismatch: found {}, expected {}",
166                obj_sdruc.return_amount,
167                sdruc.amount()
168            );
169        } else {
170            bail!("missing storage deposit return on object");
171        }
172    } else {
173        ensure!(
174            created.is_none(),
175            "erroneous storage deposit return on object"
176        );
177    }
178    Ok(())
179}
180
181pub(super) fn verify_timelock_unlock_condition(
182    original: Option<&sdk_output::unlock_condition::TimelockUnlockCondition>,
183    created: Option<&unlock_conditions::TimelockUnlockCondition>,
184) -> Result<()> {
185    // Timelock Unlock Condition
186    if let Some(timelock) = original {
187        if let Some(obj_timelock) = created {
188            ensure!(
189                obj_timelock.unix_time == timelock.timestamp(),
190                "timelock timestamp mismatch: found {}, expected {}",
191                obj_timelock.unix_time,
192                timelock.timestamp()
193            );
194        } else {
195            bail!("missing timelock on object");
196        }
197    } else {
198        ensure!(created.is_none(), "erroneous timelock on object");
199    }
200    Ok(())
201}
202
203pub(super) fn verify_expiration_unlock_condition(
204    original: Option<&sdk_output::unlock_condition::ExpirationUnlockCondition>,
205    created: Option<&unlock_conditions::ExpirationUnlockCondition>,
206    address: &Address,
207) -> Result<()> {
208    // Expiration Unlock Condition
209    if let Some(expiration) = original {
210        if let Some(obj_expiration) = created {
211            let iota_address = stardust_to_iota_address(address)?;
212            let iota_return_address = stardust_to_iota_address(expiration.return_address())?;
213            ensure!(
214                obj_expiration.owner == iota_address,
215                "expiration owner mismatch: found {}, expected {}",
216                obj_expiration.owner,
217                iota_address
218            );
219            ensure!(
220                obj_expiration.return_address == iota_return_address,
221                "expiration return address mismatch: found {}, expected {}",
222                obj_expiration.return_address,
223                iota_return_address
224            );
225            ensure!(
226                obj_expiration.unix_time == expiration.timestamp(),
227                "expiration timestamp mismatch: found {}, expected {}",
228                obj_expiration.unix_time,
229                expiration.timestamp()
230            );
231        } else {
232            bail!("missing expiration on object");
233        }
234    } else {
235        ensure!(created.is_none(), "erroneous expiration on object");
236    }
237    Ok(())
238}
239
240pub(super) fn verify_metadata_feature(
241    original: Option<&sdk_output::feature::MetadataFeature>,
242    created: Option<&Vec<u8>>,
243) -> Result<()> {
244    if let Some(metadata) = original {
245        if let Some(obj_metadata) = created {
246            ensure!(
247                obj_metadata.as_slice() == metadata.data(),
248                "metadata mismatch: found {:x?}, expected {:x?}",
249                obj_metadata.as_slice(),
250                metadata.data()
251            );
252        } else {
253            bail!("missing metadata on object");
254        }
255    } else {
256        ensure!(created.is_none(), "erroneous metadata on object");
257    }
258    Ok(())
259}
260
261pub(super) fn verify_tag_feature(
262    original: Option<&sdk_output::feature::TagFeature>,
263    created: Option<&Vec<u8>>,
264) -> Result<()> {
265    if let Some(tag) = original {
266        if let Some(obj_tag) = created {
267            ensure!(
268                obj_tag.as_slice() == tag.tag(),
269                "tag mismatch: found {:x?}, expected {:x?}",
270                obj_tag.as_slice(),
271                tag.tag()
272            );
273        } else {
274            bail!("missing tag on object");
275        }
276    } else {
277        ensure!(created.is_none(), "erroneous tag on object");
278    }
279    Ok(())
280}
281
282pub(super) fn verify_sender_feature(
283    original: Option<&sdk_output::feature::SenderFeature>,
284    created: Option<IotaAddress>,
285) -> Result<()> {
286    if let Some(sender) = original {
287        let iota_sender_address = stardust_to_iota_address(sender.address())?;
288        if let Some(obj_sender) = created {
289            ensure!(
290                obj_sender == iota_sender_address,
291                "sender mismatch: found {}, expected {}",
292                obj_sender,
293                iota_sender_address
294            );
295        } else {
296            bail!("missing sender on object");
297        }
298    } else {
299        ensure!(created.is_none(), "erroneous sender on object");
300    }
301    Ok(())
302}
303
304pub(super) fn verify_issuer_feature(
305    original: Option<&sdk_output::feature::IssuerFeature>,
306    created: Option<IotaAddress>,
307) -> Result<()> {
308    if let Some(issuer) = original {
309        let iota_issuer_address = stardust_to_iota_address(issuer.address())?;
310        if let Some(obj_issuer) = created {
311            ensure!(
312                obj_issuer == iota_issuer_address,
313                "issuer mismatch: found {}, expected {}",
314                obj_issuer,
315                iota_issuer_address
316            );
317        } else {
318            bail!("missing issuer on object");
319        }
320    } else {
321        ensure!(created.is_none(), "erroneous issuer on object");
322    }
323    Ok(())
324}
325
326pub(super) fn verify_address_owner(
327    owning_address: &Address,
328    obj: &Object,
329    name: &str,
330    address_swap_map: &AddressSwapMap,
331) -> Result<()> {
332    let expected_owner = address_swap_map.stardust_to_iota_address_owner(owning_address)?;
333
334    ensure!(
335        obj.owner == expected_owner,
336        "{name} owner mismatch: found {}, expected {}",
337        obj.owner,
338        expected_owner
339    );
340    Ok(())
341}
342
343pub(super) fn verify_shared_object(obj: &Object, name: &str) -> Result<()> {
344    let expected_owner = Owner::Shared {
345        initial_shared_version: Default::default(),
346    };
347    ensure!(
348        obj.owner.is_shared(),
349        "{name} shared owner mismatch: found {}, expected {}",
350        obj.owner,
351        expected_owner
352    );
353    Ok(())
354}
355
356// Checks whether an object exists for this address and whether it is the
357// expected alias or nft object. We do not expect an object for Ed25519
358// addresses.
359pub(super) fn verify_parent(
360    output_id: &OutputId,
361    address: &Address,
362    storage: &InMemoryStorage,
363) -> Result<()> {
364    let object_id = ObjectID::from(stardust_to_iota_address(address)?);
365    let parent = storage.get_object(&object_id);
366    match address {
367        Address::Alias(address) => {
368            if let Some(parent_obj) = parent {
369                if parent_obj.to_rust::<Alias>().is_none() {
370                    warn!(
371                        "verification failed for output id {output_id}: unexpected parent found for alias address {address}"
372                    );
373                }
374            }
375        }
376        Address::Nft(address) => {
377            if let Some(parent_obj) = parent {
378                if parent_obj.to_rust::<Nft>().is_none() {
379                    warn!(
380                        "verification failed for output id {output_id}: unexpected parent found for nft address {address}"
381                    );
382                }
383            }
384        }
385        Address::Ed25519(address) => {
386            if parent.is_some() {
387                warn!(
388                    "verification failed for output id {output_id}: unexpected parent found for ed25519 address {address}"
389                );
390            }
391        }
392    }
393    Ok(())
394}
395
396pub(super) fn verify_coin(output_amount: u64, created_coin: &Coin) -> Result<()> {
397    ensure!(
398        created_coin.value() == output_amount,
399        "coin amount mismatch: found {}, expected {}",
400        created_coin.value(),
401        output_amount
402    );
403    Ok(())
404}
405
406pub(super) trait NativeTokenKind {
407    fn bag_key(&self) -> String;
408
409    fn value(&self) -> u64;
410
411    fn from_object(obj: &Object) -> Result<Self>
412    where
413        Self: Sized;
414}
415
416impl NativeTokenKind for (TypeTag, Coin) {
417    fn bag_key(&self) -> String {
418        self.0.to_canonical_string(false)
419    }
420
421    fn value(&self) -> u64 {
422        self.1.value()
423    }
424
425    fn from_object(obj: &Object) -> Result<Self> {
426        obj.coin_type_maybe()
427            .zip(obj.as_coin_maybe())
428            .ok_or_else(|| anyhow!("expected a native token coin, found {:?}", obj.type_()))
429    }
430}
431
432impl NativeTokenKind for Field<String, Balance> {
433    fn bag_key(&self) -> String {
434        self.name.clone()
435    }
436
437    fn value(&self) -> u64 {
438        self.value.value()
439    }
440
441    fn from_object(obj: &Object) -> Result<Self> {
442        obj.to_rust::<Field<String, Balance>>()
443            .ok_or_else(|| anyhow!("expected a native token field, found {:?}", obj.type_()))
444    }
445}
446
447pub fn truncate_to_max_allowed_u64_supply(value: U256) -> u64 {
448    if value > U256::from(MAX_ALLOWED_U64_SUPPLY) {
449        MAX_ALLOWED_U64_SUPPLY
450    } else {
451        value.as_u64()
452    }
453}