iota_genesis_builder/stardust/migration/verification/
util.rs1use 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
37pub(super) struct TokensAmountCounter {
39 inner: HashMap<String, (u64, u64)>,
41}
42
43impl TokensAmountCounter {
44 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 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(false);
126 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 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 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 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
356pub(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}