iota_genesis_builder/stardust/migration/verification/
basic.rs

1// Copyright (c) 2024 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use std::collections::HashMap;
5
6use anyhow::{Result, anyhow, ensure};
7use iota_sdk::types::block::output::{BasicOutput, OutputId, TokenId};
8use iota_types::{
9    TypeTag,
10    balance::Balance,
11    coin::Coin,
12    dynamic_field::Field,
13    in_memory_storage::InMemoryStorage,
14    object::Owner,
15    timelock::{
16        stardust_upgrade_label::STARDUST_UPGRADE_LABEL_VALUE,
17        timelock::{TimeLock, is_timelocked_vested_reward},
18    },
19};
20
21use crate::stardust::{
22    migration::{
23        executor::FoundryLedgerData,
24        verification::{
25            created_objects::CreatedObjects,
26            util::{
27                TokensAmountCounter, verify_address_owner, verify_coin,
28                verify_expiration_unlock_condition, verify_metadata_feature, verify_native_tokens,
29                verify_parent, verify_sender_feature, verify_storage_deposit_unlock_condition,
30                verify_tag_feature, verify_timelock_unlock_condition,
31            },
32        },
33    },
34    types::address_swap_map::AddressSwapMap,
35};
36
37pub(super) fn verify_basic_output(
38    output_id: OutputId,
39    output: &BasicOutput,
40    created_objects: &CreatedObjects,
41    foundry_data: &HashMap<TokenId, FoundryLedgerData>,
42    target_milestone_timestamp: u32,
43    storage: &InMemoryStorage,
44    tokens_counter: &mut TokensAmountCounter,
45    address_swap_map: &AddressSwapMap,
46) -> Result<()> {
47    // If this is a timelocked vested reward, a `Timelock<Balance>` is created.
48    if is_timelocked_vested_reward(output_id, output, target_milestone_timestamp) {
49        let created_timelock = created_objects
50            .output()
51            .and_then(|id| {
52                storage
53                    .get_object(id)
54                    .ok_or_else(|| anyhow!("missing timelock object"))
55            })?
56            .to_rust::<TimeLock<Balance>>()
57            .ok_or_else(|| anyhow!("invalid timelock object"))?;
58
59        // Locked timestamp
60        let output_timelock_timestamp =
61            output.unlock_conditions().timelock().unwrap().timestamp() as u64 * 1000;
62        ensure!(
63            created_timelock.expiration_timestamp_ms() == output_timelock_timestamp,
64            "timelock timestamp mismatch: found {}, expected {}",
65            created_timelock.expiration_timestamp_ms(),
66            output_timelock_timestamp
67        );
68
69        // Amount
70        ensure!(
71            created_timelock.locked().value() == output.amount(),
72            "locked amount mismatch: found {}, expected {}",
73            created_timelock.locked().value(),
74            output.amount()
75        );
76        tokens_counter.update_total_value_for_iota(created_timelock.locked().value());
77
78        // Label
79        let label = created_timelock
80            .label()
81            .as_ref()
82            .ok_or_else(|| anyhow!("timelock label must be initialized"))?;
83        let expected_label = STARDUST_UPGRADE_LABEL_VALUE;
84
85        ensure!(
86            label == expected_label,
87            "timelock label mismatch: found {}, expected {}",
88            label,
89            expected_label
90        );
91
92        ensure!(
93            created_objects.native_token_coin().is_err(),
94            "unexpected native token coin found"
95        );
96
97        ensure!(
98            created_objects.coin_manager().is_err(),
99            "unexpected coin manager found"
100        );
101
102        ensure!(
103            created_objects.coin_manager_treasury_cap().is_err(),
104            "unexpected coin manager cap found"
105        );
106
107        ensure!(
108            created_objects.package().is_err(),
109            "unexpected package found"
110        );
111
112        return Ok(());
113    }
114
115    // If the output has multiple unlock conditions or a metadata, tag or sender
116    // feature, then a genesis object should have been created.
117    if output.unlock_conditions().expiration().is_some()
118        || output
119            .unlock_conditions()
120            .storage_deposit_return()
121            .is_some()
122        || output
123            .unlock_conditions()
124            .is_time_locked(target_milestone_timestamp)
125        || !output.features().is_empty()
126    {
127        ensure!(created_objects.coin().is_err(), "unexpected coin created");
128
129        let created_output_obj = created_objects.output().and_then(|id| {
130            storage
131                .get_object(id)
132                .ok_or_else(|| anyhow!("missing basic output object"))
133        })?;
134        let created_output = created_output_obj
135            .to_rust::<iota_types::stardust::output::BasicOutput>()
136            .ok_or_else(|| anyhow!("invalid basic output object"))?;
137
138        // Owner
139        // If there is an expiration unlock condition, the output is shared.
140        if output.unlock_conditions().expiration().is_some() {
141            ensure!(
142                matches!(created_output_obj.owner, Owner::Shared { .. }),
143                "basic output owner mismatch: found {:?}, expected Shared",
144                created_output_obj.owner,
145            );
146        } else {
147            verify_address_owner(
148                output.address(),
149                created_output_obj,
150                "basic output",
151                address_swap_map,
152            )?;
153        }
154
155        // Amount
156        ensure!(
157            created_output.balance.value() == output.amount(),
158            "amount mismatch: found {}, expected {}",
159            created_output.balance.value(),
160            output.amount()
161        );
162        tokens_counter.update_total_value_for_iota(created_output.balance.value());
163
164        // Native Tokens
165        verify_native_tokens::<Field<String, Balance>>(
166            output.native_tokens(),
167            foundry_data,
168            created_output.native_tokens,
169            created_objects.native_tokens().ok(),
170            storage,
171            tokens_counter,
172        )?;
173
174        // Storage Deposit Return Unlock Condition
175        verify_storage_deposit_unlock_condition(
176            output.unlock_conditions().storage_deposit_return(),
177            created_output.storage_deposit_return.as_ref(),
178        )?;
179
180        // Timelock Unlock Condition
181        verify_timelock_unlock_condition(
182            output.unlock_conditions().timelock(),
183            created_output.timelock.as_ref(),
184        )?;
185
186        // Expiration Unlock Condition
187        verify_expiration_unlock_condition(
188            output.unlock_conditions().expiration(),
189            created_output.expiration.as_ref(),
190            output.address(),
191        )?;
192
193        // Metadata Feature
194        verify_metadata_feature(
195            output.features().metadata(),
196            created_output.metadata.as_ref(),
197        )?;
198
199        // Tag Feature
200        verify_tag_feature(output.features().tag(), created_output.tag.as_ref())?;
201
202        // Sender Feature
203        verify_sender_feature(output.features().sender(), created_output.sender)?;
204
205    // Otherwise the output contains only an address unlock condition and
206    // only a coin and possibly native tokens should have been
207    // created.
208    } else {
209        ensure!(
210            created_objects.output().is_err(),
211            "unexpected output object created for simple deposit"
212        );
213
214        // Gas coin value and owner
215        let created_coin_obj = created_objects.coin().and_then(|id| {
216            storage
217                .get_object(id)
218                .ok_or_else(|| anyhow!("missing coin"))
219        })?;
220        let created_coin = created_coin_obj
221            .as_coin_maybe()
222            .ok_or_else(|| anyhow!("expected a coin"))?;
223
224        verify_address_owner(output.address(), created_coin_obj, "coin", address_swap_map)?;
225        verify_coin(output.amount(), &created_coin)?;
226        tokens_counter.update_total_value_for_iota(created_coin.value());
227
228        // Native Tokens
229        verify_native_tokens::<(TypeTag, Coin)>(
230            output.native_tokens(),
231            foundry_data,
232            None,
233            created_objects.native_tokens().ok(),
234            storage,
235            tokens_counter,
236        )?;
237    }
238
239    verify_parent(&output_id, output.address(), storage)?;
240
241    ensure!(
242        created_objects.native_token_coin().is_err(),
243        "unexpected native token coin found"
244    );
245
246    ensure!(
247        created_objects.coin_manager().is_err(),
248        "unexpected coin manager found"
249    );
250
251    ensure!(
252        created_objects.coin_manager_treasury_cap().is_err(),
253        "unexpected coin manager cap found"
254    );
255
256    ensure!(
257        created_objects.package().is_err(),
258        "unexpected package found"
259    );
260
261    Ok(())
262}