From 9b5f33fbf3fa0df5f2e9d2d95a98f117888d935c Mon Sep 17 00:00:00 2001 From: Pavlo Botnar Date: Wed, 11 Dec 2024 23:10:11 +0200 Subject: [PATCH] feat(iota-genesis-builder): add address swap map for swapping origin addresses to destination during the migration process (#4314) * feat(iota-genesis-builder): add address swap map for swapping origin addresses to destination during the migration process --------- Co-authored-by: DaughterOfMars --- Cargo.lock | 1 + crates/iota-genesis-builder/Cargo.toml | 1 + crates/iota-genesis-builder/src/main.rs | 19 +- .../src/stardust/migration/executor.rs | 37 ++- .../src/stardust/migration/migration.rs | 30 ++- .../src/stardust/migration/tests/basic.rs | 7 +- .../src/stardust/migration/tests/mod.rs | 14 +- .../stardust/migration/verification/alias.rs | 19 +- .../stardust/migration/verification/basic.rs | 31 ++- .../migration/verification/foundry.rs | 6 +- .../stardust/migration/verification/mod.rs | 12 +- .../stardust/migration/verification/nft.rs | 29 +- .../stardust/migration/verification/util.rs | 9 +- .../src/stardust/types/address_swap_map.rs | 255 ++++++++++++++++++ .../src/stardust/types/mod.rs | 1 + 15 files changed, 409 insertions(+), 62 deletions(-) create mode 100644 crates/iota-genesis-builder/src/stardust/types/address_swap_map.rs diff --git a/Cargo.lock b/Cargo.lock index a6985400c04..272c5b18d44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6630,6 +6630,7 @@ dependencies = [ "bigdecimal", "camino", "clap", + "csv", "fastcrypto", "flate2", "fs_extra", diff --git a/crates/iota-genesis-builder/Cargo.toml b/crates/iota-genesis-builder/Cargo.toml index 81d3c77ffe1..c62d44ebfbf 100644 --- a/crates/iota-genesis-builder/Cargo.toml +++ b/crates/iota-genesis-builder/Cargo.toml @@ -16,6 +16,7 @@ bcs.workspace = true bigdecimal = "0.4" camino.workspace = true clap.workspace = true +csv = "1.2" fastcrypto.workspace = true flate2.workspace = true fs_extra = "1.3" diff --git a/crates/iota-genesis-builder/src/main.rs b/crates/iota-genesis-builder/src/main.rs index ba782f7b042..403080b5ee7 100644 --- a/crates/iota-genesis-builder/src/main.rs +++ b/crates/iota-genesis-builder/src/main.rs @@ -13,7 +13,7 @@ use iota_genesis_builder::{ stardust::{ migration::{Migration, MigrationTargetNetwork}, parse::HornetSnapshotParser, - types::output_header::OutputHeader, + types::{address_swap_map::AddressSwapMap, output_header::OutputHeader}, }, }; use iota_sdk::types::block::{ @@ -42,6 +42,11 @@ enum Snapshot { Iota { #[clap(long, help = "Path to the Iota Hornet full-snapshot file")] snapshot_path: String, + #[clap( + long, + help = "Path to the address swap map file. This must be a CSV file with two columns, where an entry contains in the first column an IotaAddress present in the Hornet full-snapshot and in the second column an IotaAddress that will be used for the swap." + )] + address_swap_map_path: String, #[clap(long, value_parser = clap::value_parser!(MigrationTargetNetwork), help = "Target network for migration")] target_network: MigrationTargetNetwork, }, @@ -56,11 +61,17 @@ fn main() -> Result<()> { // Parse the CLI arguments let cli = Cli::parse(); - let (snapshot_path, target_network, coin_type) = match cli.snapshot { + let (snapshot_path, address_swap_map_path, target_network, coin_type) = match cli.snapshot { Snapshot::Iota { snapshot_path, + address_swap_map_path, target_network, - } => (snapshot_path, target_network, CoinType::Iota), + } => ( + snapshot_path, + address_swap_map_path, + target_network, + CoinType::Iota, + ), }; // Start the Hornet snapshot parser @@ -73,12 +84,14 @@ fn main() -> Result<()> { CoinType::Iota => scale_amount_for_iota(snapshot_parser.total_supply()?)?, }; + let address_swap_map = AddressSwapMap::from_csv(&address_swap_map_path)?; // Prepare the migration using the parser output stream let migration = Migration::new( snapshot_parser.target_milestone_timestamp(), total_supply, target_network, coin_type, + address_swap_map, )?; // Prepare the writer for the objects snapshot diff --git a/crates/iota-genesis-builder/src/stardust/migration/executor.rs b/crates/iota-genesis-builder/src/stardust/migration/executor.rs index 816123dba63..d11f85c8c00 100644 --- a/crates/iota-genesis-builder/src/stardust/migration/executor.rs +++ b/crates/iota-genesis-builder/src/stardust/migration/executor.rs @@ -35,7 +35,6 @@ use iota_types::{ stardust::{ coin_type::CoinType, output::{Nft, foundry::create_foundry_amount_coin}, - stardust_to_iota_address, stardust_to_iota_address_owner, }, timelock::timelock, transaction::{ @@ -53,7 +52,10 @@ use crate::{ MigrationTargetNetwork, PACKAGE_DEPS, create_migration_context, package_module_bytes, verification::created_objects::CreatedObjects, }, - types::{output_header::OutputHeader, token_scheme::SimpleTokenSchemeU64}, + types::{ + address_swap_map::AddressSwapMap, output_header::OutputHeader, + token_scheme::SimpleTokenSchemeU64, + }, }, }; @@ -307,6 +309,7 @@ impl Executor { header: &OutputHeader, alias: &AliasOutput, coin_type: CoinType, + address_swap_map: &mut AddressSwapMap, ) -> Result { let mut created_objects = CreatedObjects::default(); @@ -316,7 +319,8 @@ impl Executor { let move_alias = iota_types::stardust::output::Alias::try_from_stardust(alias_id, alias)?; // TODO: We should ensure that no circular ownership exists. - let alias_output_owner = stardust_to_iota_address_owner(alias.governor_address())?; + let alias_output_owner = + address_swap_map.swap_stardust_to_iota_address_owner(alias.governor_address())?; let package_deps = InputObjects::new(self.load_packages(PACKAGE_DEPS).collect()); let version = package_deps.lamport_timestamp(&[]); @@ -558,10 +562,14 @@ impl Executor { basic_output: &BasicOutput, target_milestone_timestamp_sec: u32, coin_type: &CoinType, + address_swap_map: &mut AddressSwapMap, ) -> Result { let mut basic = iota_types::stardust::output::BasicOutput::new(header.new_object_id(), basic_output)?; - let owner: IotaAddress = basic_output.address().to_string().parse()?; + + let basic_objects_owner = + address_swap_map.swap_stardust_to_iota_address(basic_output.address())?; + let mut created_objects = CreatedObjects::default(); // The minimum version of the manually created objects @@ -570,11 +578,12 @@ impl Executor { let object = if basic.is_simple_coin(target_milestone_timestamp_sec) { if !basic_output.native_tokens().is_empty() { - let coins = self.create_native_token_coins(basic_output.native_tokens(), owner)?; + let coins = self + .create_native_token_coins(basic_output.native_tokens(), basic_objects_owner)?; created_objects.set_native_tokens(coins)?; } let amount_coin = basic.into_genesis_coin_object( - owner, + basic_objects_owner, &self.protocol_config, &self.tx_context, version, @@ -596,7 +605,7 @@ impl Executor { basic.native_tokens.id = UID::new(self.tx_context.fresh_id()); } let object = basic.to_genesis_object( - owner, + basic_objects_owner, &self.protocol_config, &self.tx_context, version, @@ -618,10 +627,12 @@ impl Executor { output_id: OutputId, basic_output: &BasicOutput, target_milestone_timestamp: u32, + address_swap_map: &mut AddressSwapMap, ) -> Result { let mut created_objects = CreatedObjects::default(); - let owner: IotaAddress = basic_output.address().to_string().parse()?; + let basic_output_owner = + address_swap_map.swap_stardust_to_iota_address(basic_output.address())?; let package_deps = InputObjects::new(self.load_packages(PACKAGE_DEPS).collect()); let version = package_deps.lamport_timestamp(&[]); @@ -631,7 +642,7 @@ impl Executor { let object = timelock::to_genesis_object( timelock, - owner, + basic_output_owner, &self.protocol_config, &self.tx_context, version, @@ -648,6 +659,7 @@ impl Executor { header: &OutputHeader, nft: &NftOutput, coin_type: CoinType, + address_swap_map: &mut AddressSwapMap, ) -> Result { let mut created_objects = CreatedObjects::default(); @@ -657,8 +669,11 @@ impl Executor { let move_nft = Nft::try_from_stardust(nft_id, nft)?; // TODO: We should ensure that no circular ownership exists. - let nft_output_owner_address = stardust_to_iota_address(nft.address())?; - let nft_output_owner = stardust_to_iota_address_owner(nft.address())?; + let nft_output_owner_address = + address_swap_map.swap_stardust_to_iota_address(nft.address())?; + + let nft_output_owner = + address_swap_map.swap_stardust_to_iota_address_owner(nft.address())?; let package_deps = InputObjects::new(self.load_packages(PACKAGE_DEPS).collect()); let version = package_deps.lamport_timestamp(&[]); diff --git a/crates/iota-genesis-builder/src/stardust/migration/migration.rs b/crates/iota-genesis-builder/src/stardust/migration/migration.rs index c07efda78ec..96ef8c7fd2a 100644 --- a/crates/iota-genesis-builder/src/stardust/migration/migration.rs +++ b/crates/iota-genesis-builder/src/stardust/migration/migration.rs @@ -32,7 +32,7 @@ use crate::stardust::{ verification::{created_objects::CreatedObjects, verify_outputs}, }, native_token::package_data::NativeTokenPackageData, - types::output_header::OutputHeader, + types::{address_swap_map::AddressSwapMap, output_header::OutputHeader}, }; /// We fix the protocol version used in the migration. @@ -74,6 +74,7 @@ pub struct Migration { /// The coin type to use in order to migrate outputs. Can only be equal to /// `Iota` at the moment. Is fixed for the entire migration process. coin_type: CoinType, + address_swap_map: AddressSwapMap, } impl Migration { @@ -84,6 +85,7 @@ impl Migration { total_supply: u64, target_network: MigrationTargetNetwork, coin_type: CoinType, + address_swap_map: AddressSwapMap, ) -> Result { let executor = Executor::new( ProtocolVersion::new(MIGRATION_PROTOCOL_VERSION), @@ -96,6 +98,7 @@ impl Migration { executor, output_objects_map: Default::default(), coin_type, + address_swap_map, }) } @@ -135,7 +138,7 @@ impl Migration { .collect::>(); info!("Verifying ledger state..."); self.verify_ledger_state(&outputs)?; - + self.address_swap_map.verify_all_addresses_swapped()?; Ok(()) } @@ -194,14 +197,18 @@ impl Migration { ) -> Result<()> { for (header, output) in outputs { let created = match output { - Output::Alias(alias) => { - self.executor - .create_alias_objects(header, alias, self.coin_type)? - } - Output::Nft(nft) => { - self.executor - .create_nft_objects(header, nft, self.coin_type)? - } + Output::Alias(alias) => self.executor.create_alias_objects( + header, + alias, + self.coin_type, + &mut self.address_swap_map, + )?, + Output::Nft(nft) => self.executor.create_nft_objects( + header, + nft, + self.coin_type, + &mut self.address_swap_map, + )?, Output::Basic(basic) => { // All timelocked vested rewards(basic outputs with the specific ID format) // should be migrated as TimeLock> objects. @@ -214,6 +221,7 @@ impl Migration { header.output_id(), basic, self.target_milestone_timestamp_sec, + &mut self.address_swap_map, )? } else { self.executor.create_basic_objects( @@ -221,6 +229,7 @@ impl Migration { basic, self.target_milestone_timestamp_sec, &self.coin_type, + &mut self.address_swap_map, )? } } @@ -244,6 +253,7 @@ impl Migration { self.target_milestone_timestamp_sec, self.total_supply, self.executor.store(), + &self.address_swap_map, )?; Ok(()) } diff --git a/crates/iota-genesis-builder/src/stardust/migration/tests/basic.rs b/crates/iota-genesis-builder/src/stardust/migration/tests/basic.rs index 3888ef58d1e..b503be9862c 100644 --- a/crates/iota-genesis-builder/src/stardust/migration/tests/basic.rs +++ b/crates/iota-genesis-builder/src/stardust/migration/tests/basic.rs @@ -32,7 +32,7 @@ use crate::stardust::{ random_output_header, unlock_object, }, }, - types::output_header::OutputHeader, + types::{address_swap_map::AddressSwapMap, output_header::OutputHeader}, }; /// Test the id of a `BasicOutput` that is transformed to a simple coin. @@ -53,6 +53,7 @@ fn basic_simple_coin_id() { 1_000_000, MigrationTargetNetwork::Mainnet, CoinType::Iota, + AddressSwapMap::default(), ) .unwrap(); migration @@ -103,6 +104,7 @@ fn basic_simple_coin_id_with_expired_timelock() { 1_000_000, MigrationTargetNetwork::Mainnet, CoinType::Iota, + AddressSwapMap::default(), ) .unwrap(); migration @@ -139,6 +141,7 @@ fn basic_id() { 1_000_000, MigrationTargetNetwork::Mainnet, CoinType::Iota, + AddressSwapMap::default(), ) .unwrap(); migration @@ -183,6 +186,7 @@ fn basic_simple_coin_migration_with_native_token() { 1_000_000, MigrationTargetNetwork::Mainnet, CoinType::Iota, + AddressSwapMap::default(), ) .unwrap(); migration.run_migration(outputs).unwrap(); @@ -225,6 +229,7 @@ fn basic_simple_coin_migration_with_native_tokens() { 1_000_000, MigrationTargetNetwork::Mainnet, CoinType::Iota, + AddressSwapMap::default(), ) .unwrap(); migration.run_migration(outputs.clone()).unwrap(); diff --git a/crates/iota-genesis-builder/src/stardust/migration/tests/mod.rs b/crates/iota-genesis-builder/src/stardust/migration/tests/mod.rs index b55599cdb5a..f694f1b8758 100644 --- a/crates/iota-genesis-builder/src/stardust/migration/tests/mod.rs +++ b/crates/iota-genesis-builder/src/stardust/migration/tests/mod.rs @@ -39,7 +39,10 @@ use crate::stardust::{ }, verification::created_objects::CreatedObjects, }, - types::{output_header::OutputHeader, output_index::random_output_index}, + types::{ + address_swap_map::AddressSwapMap, output_header::OutputHeader, + output_index::random_output_index, + }, }; mod alias; @@ -63,8 +66,13 @@ fn run_migration( outputs: impl IntoIterator, coin_type: CoinType, ) -> anyhow::Result<(Executor, HashMap)> { - let mut migration = - Migration::new(1, total_supply, MigrationTargetNetwork::Mainnet, coin_type)?; + let mut migration = Migration::new( + 1, + total_supply, + MigrationTargetNetwork::Mainnet, + coin_type, + AddressSwapMap::default(), + )?; migration.run_migration(outputs)?; Ok(migration.into_parts()) } diff --git a/crates/iota-genesis-builder/src/stardust/migration/verification/alias.rs b/crates/iota-genesis-builder/src/stardust/migration/verification/alias.rs index f8cf0e1b23a..bc75037c894 100644 --- a/crates/iota-genesis-builder/src/stardust/migration/verification/alias.rs +++ b/crates/iota-genesis-builder/src/stardust/migration/verification/alias.rs @@ -17,15 +17,18 @@ use iota_types::{ }, }; -use crate::stardust::migration::{ - executor::FoundryLedgerData, - verification::{ - created_objects::CreatedObjects, - util::{ - verify_address_owner, verify_issuer_feature, verify_metadata_feature, - verify_native_tokens, verify_parent, verify_sender_feature, +use crate::stardust::{ + migration::{ + executor::FoundryLedgerData, + verification::{ + created_objects::CreatedObjects, + util::{ + verify_address_owner, verify_issuer_feature, verify_metadata_feature, + verify_native_tokens, verify_parent, verify_sender_feature, + }, }, }, + types::address_swap_map::AddressSwapMap, }; pub(super) fn verify_alias_output( @@ -35,6 +38,7 @@ pub(super) fn verify_alias_output( foundry_data: &HashMap, storage: &InMemoryStorage, total_value: &mut u64, + address_swap_map: &AddressSwapMap, ) -> anyhow::Result<()> { let alias_id = ObjectID::new(*output.alias_id_non_null(&output_id)); @@ -53,6 +57,7 @@ pub(super) fn verify_alias_output( output.governor_address(), created_output_obj, "alias output", + address_swap_map, )?; // Alias Owner diff --git a/crates/iota-genesis-builder/src/stardust/migration/verification/basic.rs b/crates/iota-genesis-builder/src/stardust/migration/verification/basic.rs index 8748efba4a3..ddd9efb6889 100644 --- a/crates/iota-genesis-builder/src/stardust/migration/verification/basic.rs +++ b/crates/iota-genesis-builder/src/stardust/migration/verification/basic.rs @@ -18,17 +18,20 @@ use iota_types::{ }, }; -use crate::stardust::migration::{ - executor::FoundryLedgerData, - verification::{ - created_objects::CreatedObjects, - util::{ - verify_address_owner, verify_coin, verify_expiration_unlock_condition, - verify_metadata_feature, verify_native_tokens, verify_parent, verify_sender_feature, - verify_storage_deposit_unlock_condition, verify_tag_feature, - verify_timelock_unlock_condition, +use crate::stardust::{ + migration::{ + executor::FoundryLedgerData, + verification::{ + created_objects::CreatedObjects, + util::{ + verify_address_owner, verify_coin, verify_expiration_unlock_condition, + verify_metadata_feature, verify_native_tokens, verify_parent, + verify_sender_feature, verify_storage_deposit_unlock_condition, verify_tag_feature, + verify_timelock_unlock_condition, + }, }, }, + types::address_swap_map::AddressSwapMap, }; pub(super) fn verify_basic_output( @@ -39,6 +42,7 @@ pub(super) fn verify_basic_output( target_milestone_timestamp: u32, storage: &InMemoryStorage, total_value: &mut u64, + address_swap_map: &AddressSwapMap, ) -> Result<()> { // If this is a timelocked vested reward, a `Timelock` is created. if is_timelocked_vested_reward(output_id, output, target_milestone_timestamp) { @@ -120,7 +124,12 @@ pub(super) fn verify_basic_output( created_output_obj.owner, ); } else { - verify_address_owner(output.address(), created_output_obj, "basic output")?; + verify_address_owner( + output.address(), + created_output_obj, + "basic output", + address_swap_map, + )?; } // Amount @@ -191,7 +200,7 @@ pub(super) fn verify_basic_output( .as_coin_maybe() .ok_or_else(|| anyhow!("expected a coin"))?; - verify_address_owner(output.address(), created_coin_obj, "coin")?; + verify_address_owner(output.address(), created_coin_obj, "coin", address_swap_map)?; verify_coin(output.amount(), &created_coin)?; *total_value += created_coin.value(); diff --git a/crates/iota-genesis-builder/src/stardust/migration/verification/foundry.rs b/crates/iota-genesis-builder/src/stardust/migration/verification/foundry.rs index da43737ee98..68aed45cfe3 100644 --- a/crates/iota-genesis-builder/src/stardust/migration/verification/foundry.rs +++ b/crates/iota-genesis-builder/src/stardust/migration/verification/foundry.rs @@ -23,7 +23,7 @@ use crate::stardust::{ }, }, native_token::package_data::NativeTokenPackageData, - types::token_scheme::SimpleTokenSchemeU64, + types::{address_swap_map::AddressSwapMap, token_scheme::SimpleTokenSchemeU64}, }; pub(super) fn verify_foundry_output( @@ -33,6 +33,7 @@ pub(super) fn verify_foundry_output( foundry_data: &HashMap, storage: &InMemoryStorage, total_value: &mut u64, + address_swap_map: &AddressSwapMap, ) -> Result<()> { let foundry_data = foundry_data .get(&output.token_id()) @@ -54,7 +55,7 @@ pub(super) fn verify_foundry_output( .as_coin_maybe() .ok_or_else(|| anyhow!("expected a coin"))?; - verify_address_owner(alias_address, created_coin_obj, "coin")?; + verify_address_owner(alias_address, created_coin_obj, "coin", address_swap_map)?; verify_coin(output.amount(), &created_coin)?; *total_value += created_coin.value(); @@ -239,6 +240,7 @@ pub(super) fn verify_foundry_output( alias_address, coin_manager_treasury_cap_obj, "coin manager treasury cap", + address_swap_map, )?; verify_parent(&output_id, alias_address, storage)?; diff --git a/crates/iota-genesis-builder/src/stardust/migration/verification/mod.rs b/crates/iota-genesis-builder/src/stardust/migration/verification/mod.rs index b2d506680f6..983b19edc96 100644 --- a/crates/iota-genesis-builder/src/stardust/migration/verification/mod.rs +++ b/crates/iota-genesis-builder/src/stardust/migration/verification/mod.rs @@ -11,7 +11,10 @@ use iota_sdk::types::block::output::{Output, OutputId, TokenId}; use iota_types::in_memory_storage::InMemoryStorage; use self::created_objects::CreatedObjects; -use crate::stardust::{migration::executor::FoundryLedgerData, types::output_header::OutputHeader}; +use crate::stardust::{ + migration::executor::FoundryLedgerData, + types::{address_swap_map::AddressSwapMap, output_header::OutputHeader}, +}; pub mod alias; pub mod basic; @@ -27,6 +30,7 @@ pub(crate) fn verify_outputs<'a>( target_milestone_timestamp: u32, total_supply: u64, storage: &InMemoryStorage, + address_swap_map: &AddressSwapMap, ) -> anyhow::Result<()> { let mut total_value = 0; for (header, output) in outputs { @@ -41,6 +45,7 @@ pub(crate) fn verify_outputs<'a>( target_milestone_timestamp, storage, &mut total_value, + address_swap_map, )?; } ensure!( @@ -58,6 +63,7 @@ fn verify_output( target_milestone_timestamp: u32, storage: &InMemoryStorage, total_value: &mut u64, + address_swap_map: &AddressSwapMap, ) -> anyhow::Result<()> { match output { Output::Alias(output) => alias::verify_alias_output( @@ -67,6 +73,7 @@ fn verify_output( foundry_data, storage, total_value, + address_swap_map, ), Output::Basic(output) => basic::verify_basic_output( header.output_id(), @@ -76,6 +83,7 @@ fn verify_output( target_milestone_timestamp, storage, total_value, + address_swap_map, ), Output::Foundry(output) => foundry::verify_foundry_output( header.output_id(), @@ -84,6 +92,7 @@ fn verify_output( foundry_data, storage, total_value, + address_swap_map, ), Output::Nft(output) => nft::verify_nft_output( header.output_id(), @@ -92,6 +101,7 @@ fn verify_output( foundry_data, storage, total_value, + address_swap_map, ), // Treasury outputs aren't used since Stardust, so no need to verify anything here. Output::Treasury(_) => return Ok(()), diff --git a/crates/iota-genesis-builder/src/stardust/migration/verification/nft.rs b/crates/iota-genesis-builder/src/stardust/migration/verification/nft.rs index 04ac251af6d..2d2bdc6fccb 100644 --- a/crates/iota-genesis-builder/src/stardust/migration/verification/nft.rs +++ b/crates/iota-genesis-builder/src/stardust/migration/verification/nft.rs @@ -15,17 +15,20 @@ use iota_types::{ stardust::output::{NFT_DYNAMIC_OBJECT_FIELD_KEY, NFT_DYNAMIC_OBJECT_FIELD_KEY_TYPE}, }; -use crate::stardust::migration::{ - executor::FoundryLedgerData, - verification::{ - created_objects::CreatedObjects, - util::{ - verify_address_owner, verify_expiration_unlock_condition, verify_issuer_feature, - verify_metadata_feature, verify_native_tokens, verify_parent, verify_sender_feature, - verify_storage_deposit_unlock_condition, verify_tag_feature, - verify_timelock_unlock_condition, +use crate::stardust::{ + migration::{ + executor::FoundryLedgerData, + verification::{ + created_objects::CreatedObjects, + util::{ + verify_address_owner, verify_expiration_unlock_condition, verify_issuer_feature, + verify_metadata_feature, verify_native_tokens, verify_parent, + verify_sender_feature, verify_storage_deposit_unlock_condition, verify_tag_feature, + verify_timelock_unlock_condition, + }, }, }, + types::address_swap_map::AddressSwapMap, }; pub(super) fn verify_nft_output( @@ -35,6 +38,7 @@ pub(super) fn verify_nft_output( foundry_data: &HashMap, storage: &InMemoryStorage, total_value: &mut u64, + address_swap_map: &AddressSwapMap, ) -> anyhow::Result<()> { let created_output_obj = created_objects.output().and_then(|id| { storage @@ -61,7 +65,12 @@ pub(super) fn verify_nft_output( created_output_obj.owner, ); } else { - verify_address_owner(output.address(), created_output_obj, "nft output")?; + verify_address_owner( + output.address(), + created_output_obj, + "nft output", + address_swap_map, + )?; } // NFT Owner diff --git a/crates/iota-genesis-builder/src/stardust/migration/verification/util.rs b/crates/iota-genesis-builder/src/stardust/migration/verification/util.rs index ea1672f6942..5c62e3fc8e7 100644 --- a/crates/iota-genesis-builder/src/stardust/migration/verification/util.rs +++ b/crates/iota-genesis-builder/src/stardust/migration/verification/util.rs @@ -22,13 +22,14 @@ use iota_types::{ object::{Object, Owner}, stardust::{ output::{Alias, Nft, unlock_conditions}, - stardust_to_iota_address, stardust_to_iota_address_owner, + stardust_to_iota_address, }, }; use tracing::warn; use crate::stardust::{ - migration::executor::FoundryLedgerData, types::token_scheme::MAX_ALLOWED_U64_SUPPLY, + migration::executor::FoundryLedgerData, + types::{address_swap_map::AddressSwapMap, token_scheme::MAX_ALLOWED_U64_SUPPLY}, }; pub(super) fn verify_native_tokens( @@ -279,8 +280,10 @@ pub(super) fn verify_address_owner( owning_address: &Address, obj: &Object, name: &str, + address_swap_map: &AddressSwapMap, ) -> Result<()> { - let expected_owner = stardust_to_iota_address_owner(owning_address)?; + let expected_owner = address_swap_map.stardust_to_iota_address_owner(owning_address)?; + ensure!( obj.owner == expected_owner, "{name} owner mismatch: found {}, expected {}", diff --git a/crates/iota-genesis-builder/src/stardust/types/address_swap_map.rs b/crates/iota-genesis-builder/src/stardust/types/address_swap_map.rs new file mode 100644 index 00000000000..7dae6fb6e00 --- /dev/null +++ b/crates/iota-genesis-builder/src/stardust/types/address_swap_map.rs @@ -0,0 +1,255 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::HashMap; + +use iota_sdk::types::block::address::Address as StardustAddress; +use iota_types::{base_types::IotaAddress, object::Owner, stardust::stardust_to_iota_address}; + +type OriginAddress = IotaAddress; + +#[derive(Debug)] +pub struct DestinationAddress { + address: IotaAddress, + swapped: bool, +} + +impl DestinationAddress { + fn new(address: IotaAddress) -> Self { + Self { + address, + swapped: false, + } + } + + fn address(&self) -> IotaAddress { + self.address + } + + fn is_swapped(&self) -> bool { + self.swapped + } + + fn set_swapped(&mut self) { + self.swapped = true; + } +} + +#[derive(Debug, Default)] +pub struct AddressSwapMap { + addresses: HashMap, +} + +impl AddressSwapMap { + /// Retrieves the destination address for a given origin address. + pub fn destination_address(&self, origin_address: &OriginAddress) -> Option { + self.addresses + .get(origin_address) + .map(DestinationAddress::address) + } + + /// Retrieves the destination address for a given origin address. + /// Marks the origin address as swapped if found. + fn swap_destination_address(&mut self, origin_address: &OriginAddress) -> Option { + self.addresses.get_mut(origin_address).map(|destination| { + // Mark the origin address as swapped + destination.set_swapped(); + destination.address() + }) + } + + /// Verifies that all addresses have been swapped at least once. + /// Returns an error if any address is not swapped. + pub fn verify_all_addresses_swapped(&self) -> anyhow::Result<()> { + let unswapped_addresses = self + .addresses + .values() + .filter_map(|a| (!a.is_swapped()).then_some(a.address())) + .collect::>(); + if !unswapped_addresses.is_empty() { + anyhow::bail!("unswapped addresses: {:?}", unswapped_addresses); + } + + Ok(()) + } + + /// Converts a [`StardustAddress`] to an [`Owner`] by first + /// converting it to an [`IotaAddress`] and then checking against the + /// swap map for potential address substitutions. + /// + /// If the address exists in the swap map, it is swapped with the + /// mapped destination address before being wrapped into an [`Owner`]. + pub fn stardust_to_iota_address_owner( + &self, + stardust_address: impl Into, + ) -> anyhow::Result { + let mut address = stardust_to_iota_address(stardust_address)?; + if let Some(addr) = self.destination_address(&address) { + address = addr; + } + Ok(Owner::AddressOwner(address)) + } + + /// Converts a [`StardustAddress`] to an [`Owner`] by first + /// converting it to an [`IotaAddress`] and then checking against the + /// swap map for potential address substitutions. + /// + /// If the address exists in the swap map, it is swapped with the + /// mapped destination address before being wrapped into an [`Owner`]. + pub fn swap_stardust_to_iota_address_owner( + &mut self, + stardust_address: impl Into, + ) -> anyhow::Result { + let mut address = stardust_to_iota_address(stardust_address)?; + if let Some(addr) = self.swap_destination_address(&address) { + address = addr; + } + Ok(Owner::AddressOwner(address)) + } + + /// Converts a [`StardustAddress`] to an [`IotaAddress`] and + /// checks against the swap map for potential address + /// substitutions. + /// + /// If the address exists in the swap map, it is swapped with the + /// mapped destination address before being returned as an + /// [`IotaAddress`]. + pub fn swap_stardust_to_iota_address( + &mut self, + stardust_address: impl Into, + ) -> anyhow::Result { + let mut address: IotaAddress = stardust_to_iota_address(stardust_address)?; + if let Some(addr) = self.swap_destination_address(&address) { + address = addr; + } + Ok(address) + } + + /// Initializes an [`AddressSwapMap`] by reading address pairs from a CSV + /// file. + /// + /// The function expects the file to contain two columns: the origin address + /// (first column) and the destination address (second column). These are + /// parsed into a [`HashMap`] that maps origin addresses to tuples + /// containing the destination address and a flag initialized to + /// `false`. + /// + /// # Example CSV File + /// ```csv + /// Origin,Destination + /// iota1qp8h9augeh6tk3uvlxqfapuwv93atv63eqkpru029p6sgvr49eufyz7katr,0xa12b4d6ec3f9a28437d5c8f3e96ba72d3c4e8f5ac98d17b1a3b8e9f2c71d4a3c + /// iota1qp7h2lkjhs6tk3uvlxqfjhlfw34atv63eqkpru356p6sgvr76eufyz1opkh,0x42d8c182eb1f3b2366d353eed4eb02a31d1d7982c0fd44683811d7036be3a85e + /// ``` + /// + /// # Parameters + /// - `file_path`: The relative path to the CSV file containing the address + /// mappings. + /// + /// # Returns + /// - An [`AddressSwapMap`] containing the parsed mappings. + /// + /// # Errors + /// - Returns an error if the file cannot be found, read, or parsed + /// correctly. + /// - Returns an error if the origin or destination addresses cannot be + /// parsed into an [`IotaAddress`]. + pub fn from_csv(file_path: &str) -> Result { + let current_dir = std::env::current_dir()?; + let file_path = current_dir.join(file_path); + let mut reader = csv::ReaderBuilder::new().from_path(file_path)?; + let mut addresses = HashMap::new(); + + verify_headers(reader.headers()?)?; + + for result in reader.records() { + let record = result?; + let origin: OriginAddress = + stardust_to_iota_address(StardustAddress::try_from_bech32(&record[0])?)?; + let destination: DestinationAddress = DestinationAddress::new(record[1].parse()?); + addresses.insert(origin, destination); + } + + Ok(AddressSwapMap { addresses }) + } + + #[cfg(test)] + pub fn addresses(&self) -> &HashMap { + &self.addresses + } +} + +fn verify_headers(headers: &csv::StringRecord) -> Result<(), anyhow::Error> { + const LEFT_HEADER: &str = "Origin"; + const RIGHT_HEADER: &str = "Destination"; + + if &headers[0] != LEFT_HEADER && &headers[1] != RIGHT_HEADER { + anyhow::bail!("Invalid CSV headers"); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::io::Write; + + use tempfile::NamedTempFile; + + use crate::stardust::types::address_swap_map::AddressSwapMap; + + fn write_temp_file(content: &str) -> NamedTempFile { + let mut file = NamedTempFile::new().unwrap(); + writeln!(file, "{}", content).unwrap(); + file + } + + #[test] + fn test_from_csv_valid_file() { + let content = "Origin,Destination\n\ + iota1qp8h9augeh6tk3uvlxqfapuwv93atv63eqkpru029p6sgvr49eufyz7katr,0xa12b4d6ec3f9a28437d5c8f3e96ba72d3c4e8f5ac98d17b1a3b8e9f2c71d4a3c"; + let file = write_temp_file(content); + let file_path = file.path().to_str().unwrap(); + let result = AddressSwapMap::from_csv(file_path); + assert!(result.is_ok()); + let map = result.unwrap(); + assert_eq!(map.addresses().len(), 1); + } + + #[test] + fn test_from_csv_missing_file() { + let result = AddressSwapMap::from_csv("nonexistent_file.csv"); + assert!(result.is_err()); + } + + #[test] + fn test_from_csv_invalid_headers() { + let content = "wrong_header1,wrong_header2\n\ + iota1qp8h9augeh6tk3uvlxqfapuwv93atv63eqkpru029p6sgvr49eufyz7katr,0xa12b4d6ec3f9a28437d5c8f3e96ba72d3c4e8f5ac98d17b1a3b8e9f2c71d4a3c"; + let file = write_temp_file(content); + let file_path = file.path().to_str().unwrap(); + let result = AddressSwapMap::from_csv(file_path); + assert!(result.is_err()); + } + + #[test] + fn test_from_csv_invalid_record() { + let content = "Origin,Destination\n\ + iota1qp8h9augeh6tk3uvlxqfapuwv93atv63eqkpru029p6sgvr49eufyz7katr,0xa12b4d6ec3f9a28437d5c8f3e96ba72d3c4e8f5ac98d17b1a3b8e9f2c71d4a3c,invalid_number"; + let file = write_temp_file(content); + let file_path = file.path().to_str().unwrap(); + let result = AddressSwapMap::from_csv(file_path); + + assert!(result.is_err()); + } + + #[test] + fn test_from_csv_empty_file() { + let content = "Origin,Destination"; + let file = write_temp_file(content); + let file_path = file.path().to_str().unwrap(); + let result = AddressSwapMap::from_csv(file_path); + assert!(result.is_ok()); + let map = result.unwrap(); + + assert_eq!(map.addresses().len(), 0); + } +} diff --git a/crates/iota-genesis-builder/src/stardust/types/mod.rs b/crates/iota-genesis-builder/src/stardust/types/mod.rs index cf2a0681889..49ea9368cf5 100644 --- a/crates/iota-genesis-builder/src/stardust/types/mod.rs +++ b/crates/iota-genesis-builder/src/stardust/types/mod.rs @@ -1,6 +1,7 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +pub mod address_swap_map; pub mod output_header; pub mod output_index; pub mod snapshot;