Skip to content

Commit

Permalink
feat(iota-genesis-builder): Add an AddressSwapSplit map for Hornet sn…
Browse files Browse the repository at this point in the history
…apshot outputs (#4792)

* feat(iota-genesis-builder): add SwapSplitIterator for Hornet Outputs

* feat(iota-genesis-builder): add support for the SwapSplit

* fix(iota-genesis-builder): change the order of filters and make SwapSplit support different destinations

* feat(iota-e2e-tests): create a test for the address swap split feature

* fix(iota-genesis-builder): review comments for SwapSplit

* timelock set and misc suggestions (#4857)

* fix(iota-genesis-builder): review comments

* fix(iota-genesis-builder): clippy for test-outputs

* add expect for timelock candidates

Co-authored-by: Konstantinos Demartinos <[email protected]>

* fix(iota-genesis-builder): format

* fix(iota-genesis-builder): add assertion about the original vested header

* fix(iota-genesis-builder): timelock cmp

* fix(iota-genesis-builder): debug illegal vesting outputs

* fix(iota-genesis-builder): fixes #4896

* fix(iota-genesis-builder): clippy

* fix(iota-genesis-builder): output index

---------

Co-authored-by: DaughterOfMars <[email protected]>
Co-authored-by: Konstantinos Demartinos <[email protected]>
  • Loading branch information
3 people authored Jan 18, 2025
1 parent 1ac0d1b commit 46ed3db
Show file tree
Hide file tree
Showing 15 changed files with 707 additions and 41 deletions.
114 changes: 108 additions & 6 deletions crates/iota-e2e-tests/tests/full_node_migration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,27 @@ use iota_genesis_builder::{
migration::{Migration, MigrationTargetNetwork},
parse::HornetSnapshotParser,
process_outputs::scale_amount_for_iota,
types::address_swap_map::AddressSwapMap,
types::{address_swap_map::AddressSwapMap, address_swap_split_map::AddressSwapSplitMap},
},
};
use iota_json_rpc_types::{
IotaObjectDataFilter, IotaObjectDataOptions, IotaObjectResponseQuery,
IotaData, IotaObjectDataFilter, IotaObjectDataOptions, IotaObjectResponseQuery,
IotaTransactionBlockResponse, IotaTransactionBlockResponseOptions,
};
use iota_keys::keystore::{AccountKeystore, FileBasedKeystore};
use iota_macros::sim_test;
use iota_sdk::IotaClient;
use iota_types::{
IOTA_FRAMEWORK_ADDRESS, STARDUST_ADDRESS, TypeTag,
base_types::{IotaAddress, ObjectID},
balance::Balance,
base_types::{IotaAddress, MoveObjectType, ObjectID},
crypto::SignatureScheme::ED25519,
dynamic_field::DynamicFieldName,
gas_coin::GAS,
programmable_transaction_builder::ProgrammableTransactionBuilder,
quorum_driver_types::ExecuteTransactionRequestType,
stardust::{coin_type::CoinType, output::NftOutput},
timelock::timelock::TimeLock,
transaction::{Argument, ObjectArg, Transaction, TransactionData},
};
use move_core_types::ident_str;
Expand All @@ -44,6 +46,7 @@ use test_cluster::TestClusterBuilder;

const HORNET_SNAPSHOT_PATH: &str = "tests/migration/test_hornet_full_snapshot.bin";
const ADDRESS_SWAP_MAP_PATH: &str = "tests/migration/address_swap.csv";
const ADDRESS_SWAP_SPLIT_MAP_PATH: &str = "tests/migration/swap_split.csv";
const TEST_TARGET_NETWORK: &str = "alphanet-test";
const MIGRATION_DATA_FILE_NAME: &str = "stardust_object_snapshot.bin";
const DELEGATOR: &str = "0x4f72f788cdf4bb478cf9809e878e6163d5b351c82c11f1ea28750430752e7892";
Expand All @@ -54,7 +57,7 @@ const MAIN_ADDRESS_MNEMONIC: &str = "few hood high omit camp keep burger give ha
const SPONSOR_ADDRESS_MNEMONIC: &str = "okay pottery arch air egg very cave cash poem gown sorry mind poem crack dawn wet car pink extra crane hen bar boring salt";

#[sim_test]
async fn test_full_node_load_migration_data() -> Result<(), anyhow::Error> {
async fn test_full_node_load_migration_data_with_address_swap() -> Result<(), anyhow::Error> {
telemetry_subscribers::init_for_testing();

// Setup the temporary dir and create the writer for the stardust object
Expand All @@ -64,8 +67,15 @@ async fn test_full_node_load_migration_data() -> Result<(), anyhow::Error> {
let object_snapshot_writer =
BufWriter::new(File::create(&stardudst_object_snapshot_file_path)?);

// Get the address swap map
let address_swap_map = AddressSwapMap::from_csv(ADDRESS_SWAP_MAP_PATH)?;

// Generate the stardust object snapshot
genesis_builder_snapshot_generation(object_snapshot_writer)?;
genesis_builder_snapshot_generation(
object_snapshot_writer,
address_swap_map,
AddressSwapSplitMap::default(),
)?;
// Then load it
let snapshot_source = SnapshotSource::Local(stardudst_object_snapshot_file_path);

Expand All @@ -91,15 +101,54 @@ async fn test_full_node_load_migration_data() -> Result<(), anyhow::Error> {
Ok(())
}

#[sim_test]
async fn test_full_node_load_migration_data_with_address_swap_split() -> Result<(), anyhow::Error> {
telemetry_subscribers::init_for_testing();

// Setup the temporary dir and create the writer for the stardust object
// snapshot
let dir = tempdir()?;
let stardudst_object_snapshot_file_path = dir.path().join(MIGRATION_DATA_FILE_NAME);
let object_snapshot_writer =
BufWriter::new(File::create(&stardudst_object_snapshot_file_path)?);

// Get the address swap split map
let address_swap_split_map = AddressSwapSplitMap::from_csv(ADDRESS_SWAP_SPLIT_MAP_PATH)?;

// Generate the stardust object snapshot
genesis_builder_snapshot_generation(
object_snapshot_writer,
AddressSwapMap::default(),
address_swap_split_map.clone(),
)?;
// Then load it
let snapshot_source = SnapshotSource::Local(stardudst_object_snapshot_file_path);

// A new test cluster can be spawn with the stardust object snapshot
let test_cluster = TestClusterBuilder::new()
.with_migration_data(vec![snapshot_source])
.with_delegator(IotaAddress::from_str(DELEGATOR).unwrap())
.build()
.await;

// Use a client to issue a test transaction
let client = test_cluster.wallet.get_client().await.unwrap();

check_address_swap_split_map_after_migration(client, address_swap_split_map).await?;

Ok(())
}

fn genesis_builder_snapshot_generation(
object_snapshot_writer: impl Write,
address_swap_map: AddressSwapMap,
address_swap_split_map: AddressSwapSplitMap,
) -> Result<(), anyhow::Error> {
let mut snapshot_parser =
HornetSnapshotParser::new::<false>(File::open(HORNET_SNAPSHOT_PATH)?)?;
let total_supply = scale_amount_for_iota(snapshot_parser.total_supply()?)?;
let target_network = MigrationTargetNetwork::from_str(TEST_TARGET_NETWORK)?;
let coin_type = CoinType::Iota;
let address_swap_map = AddressSwapMap::from_csv(ADDRESS_SWAP_MAP_PATH)?;

// Migrate using the parser output stream
Migration::new(
Expand All @@ -111,6 +160,7 @@ fn genesis_builder_snapshot_generation(
)?
.run_for_iota(
snapshot_parser.target_milestone_timestamp(),
address_swap_split_map,
snapshot_parser.outputs(),
object_snapshot_writer,
)?;
Expand Down Expand Up @@ -406,3 +456,55 @@ pub async fn fund_address(

Ok(())
}

async fn check_address_swap_split_map_after_migration(
iota_client: IotaClient,
address_swap_split_map: AddressSwapSplitMap,
) -> Result<(), anyhow::Error> {
for destinations in address_swap_split_map.map().values() {
for (destination, tokens, tokens_timelocked) in destinations {
if *tokens > 0 {
let balance = iota_client
.coin_read_api()
.get_balance(*destination, None)
.await?;
assert_eq!(balance.total_balance, (*tokens as u128));
}
if *tokens_timelocked > 0 {
let mut total = 0;
let owned_timelocks = iota_client
.read_api()
.get_owned_objects(
*destination,
Some(IotaObjectResponseQuery::new(
Some(IotaObjectDataFilter::StructType(
MoveObjectType::timelocked_iota_balance().into(),
)),
Some(IotaObjectDataOptions::new().with_bcs()),
)),
None,
None,
)
.await?
.data;
for response in owned_timelocks {
total += bcs::from_bytes::<TimeLock<Balance>>(
&response
.data
.expect("missing response data")
.bcs
.expect("missing BCS data")
.try_as_move()
.expect("failed to convert to Move object")
.bcs_bytes,
)
.expect("should be a timelock balance")
.locked()
.value();
}
assert_eq!(total, *tokens_timelocked);
}
}
}
Ok(())
}
3 changes: 3 additions & 0 deletions crates/iota-e2e-tests/tests/migration/swap_split.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Origin,Destination,Tokens,TokensTimelocked
iota1qp8h9augeh6tk3uvlxqfapuwv93atv63eqkpru029p6sgvr49eufyz7katr,0x1336d143de5eb55bcb069f55da5fc9f0c84e368022fd2bbe0125b1093b446313,107667149000,107667149000
iota1qp8h9augeh6tk3uvlxqfapuwv93atv63eqkpru029p6sgvr49eufyz7katr,0x83b5ed87bac715ecb09017a72d531ccc3c43bcb58edeb1ce383f1c46cfd79bec,388647312000,0
27 changes: 24 additions & 3 deletions crates/iota-genesis-builder/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use iota_genesis_builder::{
migration::{Migration, MigrationTargetNetwork},
parse::HornetSnapshotParser,
process_outputs::scale_amount_for_iota,
types::address_swap_map::AddressSwapMap,
types::{address_swap_map::AddressSwapMap, address_swap_split_map::AddressSwapSplitMap},
},
};
use iota_types::stardust::coin_type::CoinType;
Expand All @@ -38,9 +38,14 @@ enum Snapshot {
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."
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 (ed25519 hex) IOTA Address that will be used for the swap."
)]
address_swap_map_path: Option<String>,
#[clap(
long,
help = "Path to the address swap split map file. This must be a CSV file with four columns, where an entry contains in the first column a (bech32) Address present in the Hornet full-snapshot, in the second column an (ed25519 hex) IOTA Address that will be used for the swap, in the third column a target amount of iota tokens to be split from the origin address to the destination address and in the fourth column the amount of timelocked iota tokens used for the same scope."
)]
address_swap_split_map_path: Option<String>,
#[clap(long, value_parser = clap::value_parser!(MigrationTargetNetwork), help = "Target network for migration")]
target_network: MigrationTargetNetwork,
},
Expand All @@ -55,15 +60,23 @@ fn main() -> Result<()> {

// Parse the CLI arguments
let cli = Cli::parse();
let (snapshot_path, address_swap_map_path, target_network, coin_type) = match cli.snapshot {
let (
snapshot_path,
address_swap_map_path,
target_network,
address_swap_split_map_path,
coin_type,
) = match cli.snapshot {
Snapshot::Iota {
snapshot_path,
address_swap_map_path,
address_swap_split_map_path,
target_network,
} => (
snapshot_path,
address_swap_map_path,
target_network,
address_swap_split_map_path,
CoinType::Iota,
),
};
Expand All @@ -83,6 +96,13 @@ fn main() -> Result<()> {
} else {
AddressSwapMap::default()
};

let address_swap_split_map =
if let Some(address_swap_split_map_path) = address_swap_split_map_path {
AddressSwapSplitMap::from_csv(&address_swap_split_map_path)?
} else {
AddressSwapSplitMap::default()
};
// Prepare the migration using the parser output stream
let migration = Migration::new(
snapshot_parser.target_milestone_timestamp(),
Expand All @@ -100,6 +120,7 @@ fn main() -> Result<()> {
CoinType::Iota => {
migration.run_for_iota(
snapshot_parser.target_milestone_timestamp(),
address_swap_split_map,
snapshot_parser.outputs(),
object_snapshot_writer,
)?;
Expand Down
5 changes: 3 additions & 2 deletions crates/iota-genesis-builder/src/stake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ impl GenesisStake {
self.token_allocation.is_empty()
&& self.gas_coins_to_destroy.is_empty()
&& self.timelocks_to_destroy.is_empty()
&& self.timelocks_to_split.is_empty()
}

/// Calculate the total amount of token allocations.
Expand Down Expand Up @@ -197,9 +198,9 @@ impl GenesisStake {
target_stake_nanos,
&mut timelock_surplus,
);
if !timelock_allocation_objects.to_destroy.is_empty() {
if !timelock_allocation_objects.staked_with_timelock.is_empty() {
// Inside this block some timelock objects were picked from the pool; so we can
// save all the references to timelocks to destroy
// save all the references to timelocks to destroy, if there are any
self.timelocks_to_destroy
.append(&mut timelock_allocation_objects.to_destroy);
// Finally we create some token allocations based on timelock_allocation_objects
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ use crate::stardust::{
},
native_token::package_data::NativeTokenPackageData,
process_outputs::process_outputs_for_iota,
types::{address_swap_map::AddressSwapMap, output_header::OutputHeader},
types::{
address_swap_map::AddressSwapMap, address_swap_split_map::AddressSwapSplitMap,
output_header::OutputHeader,
},
};

/// We fix the protocol version used in the migration.
Expand Down Expand Up @@ -169,11 +172,12 @@ impl Migration {
pub fn run_for_iota<'a>(
self,
target_milestone_timestamp: u32,
swap_split_map: AddressSwapSplitMap,
outputs: impl Iterator<Item = Result<(OutputHeader, Output)>> + 'a,
writer: impl Write,
) -> Result<()> {
itertools::process_results(
process_outputs_for_iota(target_milestone_timestamp, outputs),
process_outputs_for_iota(target_milestone_timestamp, swap_split_map, outputs),
|outputs| self.run(outputs, writer),
)?
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ fn basic_simple_coin_id() {
fn basic_simple_coin_id_with_expired_timelock() {
for header in [
random_output_header(),
OutputHeader::new_testing(
OutputHeader::new(
// A potential vesting reward output transaction ID.
*TransactionId::from_str(
"0xb191c4bc825ac6983789e50545d5ef07a1d293a98ad974fc9498cb1812345678",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ mod foundry;
mod nft;

fn random_output_header() -> OutputHeader {
OutputHeader::new_testing(
OutputHeader::new(
random(),
random_output_index(),
random(),
Expand Down
Loading

0 comments on commit 46ed3db

Please sign in to comment.