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