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