diff --git a/Cargo.lock b/Cargo.lock index ac1c16e3e310a..049550fc11e00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18678,12 +18678,14 @@ dependencies = [ "aptos-config", "aptos-db", "aptos-rest-client", + "aptos-sdk", "aptos-storage-interface", "aptos-types", "bcs 0.1.4", "bytes", "clap 4.5.21", "either", + "hex", "move-core-types", "serde_json", "tar", diff --git a/movement-migration/validation-tool/Cargo.toml b/movement-migration/validation-tool/Cargo.toml index 9f6a829c1ef62..7da1bfbab1d3d 100644 --- a/movement-migration/validation-tool/Cargo.toml +++ b/movement-migration/validation-tool/Cargo.toml @@ -14,12 +14,14 @@ anyhow = { workspace = true } aptos-config = { workspace = true } aptos-db = { workspace = true } aptos-rest-client = { workspace = true } +aptos-sdk = { workspace = true } aptos-storage-interface = { workspace = true } aptos-types = { workspace = true } bcs = { workspace = true } bytes = { workspace = true } clap = { workspace = true } either = { workspace = true } +hex = { workspace = true } serde_json = { workspace = true } move-core-types = { workspace = true } thiserror = { workspace = true } diff --git a/movement-migration/validation-tool/script/clean_and_sync.sh b/movement-migration/validation-tool/script/clean_and_sync.sh new file mode 100755 index 0000000000000..9885c7c8fddf5 --- /dev/null +++ b/movement-migration/validation-tool/script/clean_and_sync.sh @@ -0,0 +1,101 @@ +#!/bin/bash + +# Check if exactly 2 arguments are provided +if [ $# -ne 2 ]; then + echo "Usage: $0 " + echo "Recursively deletes files from folder_to_clean that don't exist in reference_folder," + echo "then syncs reference_folder to folder_to_clean using rsync" + exit 1 +fi + +folder_to_clean="$1" +reference_folder="$2" + +# Check if both directories exist +if [ ! -d "$folder_to_clean" ]; then + echo "Error: Directory '$folder_to_clean' does not exist" + exit 1 +fi + +if [ ! -d "$reference_folder" ]; then + echo "Error: Directory '$reference_folder' does not exist" + exit 1 +fi + +# Convert to absolute paths to avoid issues +folder_to_clean=$(realpath "$folder_to_clean") +reference_folder=$(realpath "$reference_folder") + +echo "Cleaning folder: $folder_to_clean" +echo "Reference folder: $reference_folder" +echo + +# Ask for confirmation before proceeding +echo "This will:" +echo "1. Delete all files in '$folder_to_clean' that don't exist in '$reference_folder'" +echo "2. Remove empty directories" +echo "3. Sync '$reference_folder' to '$folder_to_clean' using rsync" +echo +read -p "Do you want to continue? (y/N): " -n 1 -r +echo + +if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Operation cancelled." + exit 0 +fi + +echo + +# Phase 1: Delete files that don't exist in reference folder +echo "=== Phase 1: Cleaning up files not in reference folder ===" +deleted_count=0 + +# Use find to get all files in folder_to_clean, preserving the relative path structure +while IFS= read -r -d '' file; do + # Get the relative path from folder_to_clean + relative_path="${file#"$folder_to_clean"/}" + + # Check if this file exists in the same relative location in reference folder + reference_file="$reference_folder/$relative_path" + + if [ ! -f "$reference_file" ]; then + echo "Deleting: $file" + rm "$file" + ((deleted_count++)) + fi +done < <(find "$folder_to_clean" -type f -print0) + +echo "Files deleted: $deleted_count" +echo + +# Phase 2: Remove empty directories +echo "=== Phase 2: Removing empty directories ===" +# Find and remove empty directories (bottom-up) +find "$folder_to_clean" -type d -empty -delete 2>/dev/null +echo "Empty directories removed" +echo + +# Phase 3: Sync with rsync +echo "=== Phase 3: Syncing reference folder to cleaned folder ===" +echo "Running: rsync -av --progress \"$reference_folder/\" \"$folder_to_clean/\"" +echo + +# Use rsync to sync the reference folder to the cleaned folder +# -a: archive mode (preserves permissions, timestamps, etc.) +# -v: verbose +# --progress: show progress +# Note the trailing slashes are important for rsync behavior +rsync -av --progress "$reference_folder/" "$folder_to_clean/" + +sync_exit_code=$? + +if [ $sync_exit_code -eq 0 ]; then + echo + echo "=== Cleanup and sync completed successfully ===" + echo "The folder '$folder_to_clean' now matches '$reference_folder'" +else + echo + echo "=== WARNING: rsync failed with exit code $sync_exit_code ===" + echo "Please check the rsync output above for errors" + exit $sync_exit_code +fi diff --git a/movement-migration/validation-tool/src/checks/api.rs b/movement-migration/validation-tool/src/checks/api.rs index d53dba820264a..cb7301d82712f 100644 --- a/movement-migration/validation-tool/src/checks/api.rs +++ b/movement-migration/validation-tool/src/checks/api.rs @@ -2,41 +2,35 @@ // SPDX-License-Identifier: Apache-2.0 use crate::checks::api::active_feature_flags::GlobalFeatureCheck; -use crate::types::api::MovementAptosRestClient; -use clap::Parser; +use crate::checks::api::cmp_transactions::CompareTransactions; +use crate::checks::api::submit_transaction::SubmitTransaction; +use crate::checks::api::transactions::GetTransactions; +use clap::Subcommand; mod active_feature_flags; +mod cmp_transactions; +mod submit_transaction; +mod transactions; -#[derive(Parser)] +#[derive(Subcommand)] #[clap( - name = "migration-api-validation", - about = "Validates api conformity after movement migration." + name = "migration-api-tool", + about = "Validates api conformity after movement migration" )] -pub struct Command { - // #[clap(long = "movement", help = "The url of the Movement REST endpoint.")] - // pub movement_rest_api_url: String, - #[clap(value_parser)] - #[clap( - long = "movement-aptos", - help = "The url of the Movement Aptos REST endpoint." - )] - pub movement_aptos_rest_api_url: String, +pub enum ApiTool { + ActiveFeatures(GlobalFeatureCheck), + Transactions(GetTransactions), + CompareTransactions(CompareTransactions), + SubmitTransaction(SubmitTransaction), } -impl Command { +impl ApiTool { pub async fn run(self) -> anyhow::Result<()> { - // let _movement_rest_client = MovementRestClient::new(&self.movement_rest_api_url)?; - let movement_aptos_rest_client = - MovementAptosRestClient::new(&self.movement_aptos_rest_api_url)?; - - GlobalFeatureCheck::satisfies(&movement_aptos_rest_client).await?; - - Ok(()) + match self { + ApiTool::ActiveFeatures(tool) => tool.run().await, + ApiTool::Transactions(tool) => tool.run().await, + ApiTool::CompareTransactions(tool) => tool.run().await, + ApiTool::SubmitTransaction(tool) => tool.run().await, + } } } - -#[test] -fn verify_tool() { - use clap::CommandFactory; - Command::command().debug_assert() -} diff --git a/movement-migration/validation-tool/src/checks/api/active_feature_flags.rs b/movement-migration/validation-tool/src/checks/api/active_feature_flags.rs index bd444f8ef26a1..65622ad20c826 100644 --- a/movement-migration/validation-tool/src/checks/api/active_feature_flags.rs +++ b/movement-migration/validation-tool/src/checks/api/active_feature_flags.rs @@ -4,150 +4,177 @@ use crate::checks::error::ValidationError; use crate::types::api::MovementAptosRestClient; use aptos_rest_client::aptos_api_types::ViewFunction; +use clap::Parser; use move_core_types::identifier::Identifier; use move_core_types::language_storage::ModuleId; use std::str::FromStr; use tracing::debug; -pub struct GlobalFeatureCheck; +#[derive(Parser)] +#[clap( + name = "migration-api-validation", + about = "Validates api conformity after movement migration." +)] +pub struct GlobalFeatureCheck { + #[clap(value_parser)] + #[clap( + long = "movement-aptos", + help = "The url of the Movement Aptos REST endpoint." + )] + pub movement_aptos_rest_api_url: String, +} impl GlobalFeatureCheck { - pub async fn satisfies( - movement_aptos_rest_client: &MovementAptosRestClient, - ) -> Result<(), ValidationError> { - let mut errors = vec![]; - let expected_active: Vec = vec![ - 1, // FeatureFlag::CODE_DEPENDENCY_CHECK - 2, // FeatureFlag::TREAT_FRIEND_AS_PRIVATE - 3, // FeatureFlag::SHA_512_AND_RIPEMD_160_NATIVES - 4, // FeatureFlag::APTOS_STD_CHAIN_ID_NATIVES - 5, // FeatureFlag::VM_BINARY_FORMAT_V6 - 7, // FeatureFlag::MULTI_ED25519_PK_VALIDATE_V2_NATIVES - 8, // FeatureFlag::BLAKE2B_256_NATIVE - 9, // FeatureFlag::RESOURCE_GROUPS - 10, // FeatureFlag::MULTISIG_ACCOUNTS - 11, // FeatureFlag::DELEGATION_POOLS - 12, // FeatureFlag::CRYPTOGRAPHY_ALGEBRA_NATIVES - 13, // FeatureFlag::BLS12_381_STRUCTURES - 14, // FeatureFlag::ED25519_PUBKEY_VALIDATE_RETURN_FALSE_WRONG_LENGTH - 15, // FeatureFlag::STRUCT_CONSTRUCTORS - 18, // FeatureFlag::SIGNATURE_CHECKER_V2 - 19, // FeatureFlag::STORAGE_SLOT_METADATA - 20, // FeatureFlag::CHARGE_INVARIANT_VIOLATION - 22, // FeatureFlag::GAS_PAYER_ENABLED - 23, // FeatureFlag::APTOS_UNIQUE_IDENTIFIERS - 24, // FeatureFlag::BULLETPROOFS_NATIVES - 25, // FeatureFlag::SIGNER_NATIVE_FORMAT_FIX - 26, // FeatureFlag::MODULE_EVENT - 27, // FeatureFlag::EMIT_FEE_STATEMENT - 28, // FeatureFlag::STORAGE_DELETION_REFUND - 29, // FeatureFlag::SIGNATURE_CHECKER_V2_SCRIPT_FIX - 30, // FeatureFlag::AGGREGATOR_V2_API - 31, // FeatureFlag::SAFER_RESOURCE_GROUPS - 32, // FeatureFlag::SAFER_METADATA - 33, // FeatureFlag::SINGLE_SENDER_AUTHENTICATOR - 34, // FeatureFlag::SPONSORED_AUTOMATIC_ACCOUNT_V1_CREATION - 35, // FeatureFlag::FEE_PAYER_ACCOUNT_OPTIONAL - 36, // FeatureFlag::AGGREGATOR_V2_DELAYED_FIELDS - 37, // FeatureFlag::CONCURRENT_TOKEN_V2 - 38, // FeatureFlag::LIMIT_MAX_IDENTIFIER_LENGTH - 39, // FeatureFlag::OPERATOR_BENEFICIARY_CHANGE - 41, // FeatureFlag::RESOURCE_GROUPS_SPLIT_IN_VM_CHANGE_SET - 42, // FeatureFlag::COMMISSION_CHANGE_DELEGATION_POOL - 43, // FeatureFlag::BN254_STRUCTURES - 44, // FeatureFlag::WEBAUTHN_SIGNATURE - 46, // FeatureFlag::KEYLESS_ACCOUNTS - 47, // FeatureFlag::KEYLESS_BUT_ZKLESS_ACCOUNTS - 48, // FeatureFlag::REMOVE_DETAILED_ERROR_FROM_HASH - 49, // FeatureFlag::JWK_CONSENSUS - 50, // FeatureFlag::CONCURRENT_FUNGIBLE_ASSETS - 51, // FeatureFlag::REFUNDABLE_BYTES - 52, // FeatureFlag::OBJECT_CODE_DEPLOYMENT - 53, // FeatureFlag::MAX_OBJECT_NESTING_CHECK - 54, // FeatureFlag::KEYLESS_ACCOUNTS_WITH_PASSKEYS - 55, // FeatureFlag::MULTISIG_V2_ENHANCEMENT - 56, // FeatureFlag::DELEGATION_POOL_ALLOWLISTING - 57, // FeatureFlag::MODULE_EVENT_MIGRATION - 58, // FeatureFlag::REJECT_UNSTABLE_BYTECODE - 59, // FeatureFlag::TRANSACTION_CONTEXT_EXTENSION - 60, // FeatureFlag::COIN_TO_FUNGIBLE_ASSET_MIGRATION - 62, // FeatureFlag::OBJECT_NATIVE_DERIVED_ADDRESS - 63, // FeatureFlag::DISPATCHABLE_FUNGIBLE_ASSET - 66, // FeatureFlag::AGGREGATOR_V2_IS_AT_LEAST_API - 67, // FeatureFlag::CONCURRENT_FUNGIBLE_BALANCE - 69, // FeatureFlag::LIMIT_VM_TYPE_SIZE - 70, // FeatureFlag::ABORT_IF_MULTISIG_PAYLOAD_MISMATCH - 73, // FeatureFlag::GOVERNED_GAS_POOL - ]; - - let module = - ModuleId::from_str("0x1::features").map_err(|e| ValidationError::Internal(e.into()))?; - let function = - Identifier::from_str("is_enabled").map_err(|e| ValidationError::Internal(e.into()))?; - - let mut view_function = ViewFunction { - module, - function, - ty_args: vec![], - args: vec![], - }; - - for feature_id in expected_active { - debug!("checking feature flag {}", feature_id); - let bytes = - bcs::to_bytes(&feature_id).map_err(|e| ValidationError::Internal(e.into()))?; - view_function.args = vec![bytes]; - - // Check feature for Maptos executor - let maptos_active = movement_aptos_rest_client - .view_bcs_with_json_response(&view_function, None) - .await - .map_err(|e| { - ValidationError::Internal( - format!( - "failed to get Movement feature flag {}: {:?}", - feature_id, e - ) - .into(), - ) - })? - .into_inner(); + pub async fn run(self) -> anyhow::Result<()> { + let movement_aptos_rest_client = + MovementAptosRestClient::new(&self.movement_aptos_rest_api_url)?; + + satisfies(&movement_aptos_rest_client).await?; + + Ok(()) + } +} + +#[test] +fn verify_tool() { + use clap::CommandFactory; + GlobalFeatureCheck::command().debug_assert() +} + +pub async fn satisfies( + movement_aptos_rest_client: &MovementAptosRestClient, +) -> Result<(), ValidationError> { + let mut errors = vec![]; + let expected_active: Vec = vec![ + 1, // FeatureFlag::CODE_DEPENDENCY_CHECK + 2, // FeatureFlag::TREAT_FRIEND_AS_PRIVATE + 3, // FeatureFlag::SHA_512_AND_RIPEMD_160_NATIVES + 4, // FeatureFlag::APTOS_STD_CHAIN_ID_NATIVES + 5, // FeatureFlag::VM_BINARY_FORMAT_V6 + 7, // FeatureFlag::MULTI_ED25519_PK_VALIDATE_V2_NATIVES + 8, // FeatureFlag::BLAKE2B_256_NATIVE + 9, // FeatureFlag::RESOURCE_GROUPS + 10, // FeatureFlag::MULTISIG_ACCOUNTS + 11, // FeatureFlag::DELEGATION_POOLS + 12, // FeatureFlag::CRYPTOGRAPHY_ALGEBRA_NATIVES + 13, // FeatureFlag::BLS12_381_STRUCTURES + 14, // FeatureFlag::ED25519_PUBKEY_VALIDATE_RETURN_FALSE_WRONG_LENGTH + 15, // FeatureFlag::STRUCT_CONSTRUCTORS + 18, // FeatureFlag::SIGNATURE_CHECKER_V2 + 19, // FeatureFlag::STORAGE_SLOT_METADATA + 20, // FeatureFlag::CHARGE_INVARIANT_VIOLATION + 22, // FeatureFlag::GAS_PAYER_ENABLED + 23, // FeatureFlag::APTOS_UNIQUE_IDENTIFIERS + 24, // FeatureFlag::BULLETPROOFS_NATIVES + 25, // FeatureFlag::SIGNER_NATIVE_FORMAT_FIX + 26, // FeatureFlag::MODULE_EVENT + 27, // FeatureFlag::EMIT_FEE_STATEMENT + 28, // FeatureFlag::STORAGE_DELETION_REFUND + 29, // FeatureFlag::SIGNATURE_CHECKER_V2_SCRIPT_FIX + 30, // FeatureFlag::AGGREGATOR_V2_API + 31, // FeatureFlag::SAFER_RESOURCE_GROUPS + 32, // FeatureFlag::SAFER_METADATA + 33, // FeatureFlag::SINGLE_SENDER_AUTHENTICATOR + 34, // FeatureFlag::SPONSORED_AUTOMATIC_ACCOUNT_V1_CREATION + 35, // FeatureFlag::FEE_PAYER_ACCOUNT_OPTIONAL + 36, // FeatureFlag::AGGREGATOR_V2_DELAYED_FIELDS + 37, // FeatureFlag::CONCURRENT_TOKEN_V2 + 38, // FeatureFlag::LIMIT_MAX_IDENTIFIER_LENGTH + 39, // FeatureFlag::OPERATOR_BENEFICIARY_CHANGE + 41, // FeatureFlag::RESOURCE_GROUPS_SPLIT_IN_VM_CHANGE_SET + 42, // FeatureFlag::COMMISSION_CHANGE_DELEGATION_POOL + 43, // FeatureFlag::BN254_STRUCTURES + 44, // FeatureFlag::WEBAUTHN_SIGNATURE + 46, // FeatureFlag::KEYLESS_ACCOUNTS + 47, // FeatureFlag::KEYLESS_BUT_ZKLESS_ACCOUNTS + 48, // FeatureFlag::REMOVE_DETAILED_ERROR_FROM_HASH + 49, // FeatureFlag::JWK_CONSENSUS + 50, // FeatureFlag::CONCURRENT_FUNGIBLE_ASSETS + 51, // FeatureFlag::REFUNDABLE_BYTES + 52, // FeatureFlag::OBJECT_CODE_DEPLOYMENT + 53, // FeatureFlag::MAX_OBJECT_NESTING_CHECK + 54, // FeatureFlag::KEYLESS_ACCOUNTS_WITH_PASSKEYS + 55, // FeatureFlag::MULTISIG_V2_ENHANCEMENT + 56, // FeatureFlag::DELEGATION_POOL_ALLOWLISTING + 57, // FeatureFlag::MODULE_EVENT_MIGRATION + 58, // FeatureFlag::REJECT_UNSTABLE_BYTECODE + 59, // FeatureFlag::TRANSACTION_CONTEXT_EXTENSION + 60, // FeatureFlag::COIN_TO_FUNGIBLE_ASSET_MIGRATION + 62, // FeatureFlag::OBJECT_NATIVE_DERIVED_ADDRESS + 63, // FeatureFlag::DISPATCHABLE_FUNGIBLE_ASSET + 66, // FeatureFlag::AGGREGATOR_V2_IS_AT_LEAST_API + 67, // FeatureFlag::CONCURRENT_FUNGIBLE_BALANCE + 69, // FeatureFlag::LIMIT_VM_TYPE_SIZE + 70, // FeatureFlag::ABORT_IF_MULTISIG_PAYLOAD_MISMATCH + 73, // FeatureFlag::GOVERNED_GAS_POOL + ]; + + let module = + ModuleId::from_str("0x1::features").map_err(|e| ValidationError::Internal(e.into()))?; + let function = + Identifier::from_str("is_enabled").map_err(|e| ValidationError::Internal(e.into()))?; - let maptos_active = maptos_active.get(0).ok_or_else(|| { + let mut view_function = ViewFunction { + module, + function, + ty_args: vec![], + args: vec![], + }; + + for feature_id in expected_active { + debug!("checking feature flag {}", feature_id); + let bytes = bcs::to_bytes(&feature_id).map_err(|e| ValidationError::Internal(e.into()))?; + view_function.args = vec![bytes]; + + // Check feature for Maptos executor + let maptos_active = movement_aptos_rest_client + .view_bcs_with_json_response(&view_function, None) + .await + .map_err(|e| { ValidationError::Internal( format!( - "failed to get Movement feature flag {}: response is empty", - feature_id + "failed to get Movement feature flag {}: {:?}", + feature_id, e ) .into(), ) - })?; + })? + .into_inner(); - let maptos_active = maptos_active.as_bool().ok_or_else(|| { - ValidationError::Internal( - format!( - "failed to get Movement feature flag {}: can't convert {:?} into a bool", - feature_id, maptos_active - ) - .into(), + let maptos_active = maptos_active.get(0).ok_or_else(|| { + ValidationError::Internal( + format!( + "failed to get Movement feature flag {}: response is empty", + feature_id ) - })?; + .into(), + ) + })?; - if !maptos_active { - errors.push(format!( - "Feature {}: Aptos={} — expected to be active", - feature_id, maptos_active, - )); - } + let maptos_active = maptos_active.as_bool().ok_or_else(|| { + ValidationError::Internal( + format!( + "failed to get Movement feature flag {}: can't convert {:?} into a bool", + feature_id, maptos_active + ) + .into(), + ) + })?; - // Slow down to avoid Cloudflare rate limiting - tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + if !maptos_active { + errors.push(format!( + "Feature {}: Aptos={} — expected to be active", + feature_id, maptos_active, + )); } - if !errors.is_empty() { - return Err(ValidationError::Unsatisfied(errors.join("\n").into())); - } + // Slow down to avoid Cloudflare rate limiting + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + } - Ok(()) + if !errors.is_empty() { + return Err(ValidationError::Unsatisfied(errors.join("\n").into())); } + + Ok(()) } diff --git a/movement-migration/validation-tool/src/checks/api/cmp_transactions.rs b/movement-migration/validation-tool/src/checks/api/cmp_transactions.rs new file mode 100644 index 0000000000000..585f20633fc15 --- /dev/null +++ b/movement-migration/validation-tool/src/checks/api/cmp_transactions.rs @@ -0,0 +1,78 @@ +// Copyright (c) Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +use crate::types::api::MovementAptosRestClient; +use aptos_rest_client::aptos_api_types::{TransactionData, TransactionOnChainData}; +use clap::Parser; +use std::path::PathBuf; +use tokio::fs::File; +use tokio::io::{AsyncBufReadExt, BufReader}; +use tracing::{error, info}; + +#[derive(Parser)] +#[clap( + name = "compare-transactions", + about = "Compares transactions with the same transactions from a remote validator node" +)] +pub struct CompareTransactions { + #[clap(value_parser)] + #[clap(long = "url", help = "The url of the Movement Aptos REST endpoint.")] + pub rest_api_url: String, + #[clap(long = "in", help = "Input path file name")] + pub path: PathBuf, +} + +impl CompareTransactions { + pub async fn run(self) -> anyhow::Result<()> { + let rest_client = MovementAptosRestClient::new(&self.rest_api_url)?; + compare_transactions(&rest_client, self.path).await?; + Ok(()) + } +} + +#[test] +fn verify_tool() { + use clap::CommandFactory; + CompareTransactions::command().debug_assert() +} + +async fn compare_transactions( + rest_client: &MovementAptosRestClient, + path: PathBuf, +) -> anyhow::Result<()> { + let file = File::open(path).await?; + let reader = BufReader::new(file); + let mut lines = reader.lines(); + let mut error = false; + + while let Some(line) = lines.next_line().await? { + let bytes = hex::decode(line.trim_end())?; + let tx_data_local = bcs::from_bytes::<'_, TransactionOnChainData>(&bytes)?; + let hash = tx_data_local.info.transaction_hash(); + if let Ok(response) = rest_client.get_transaction_by_hash_bcs(hash).await { + if let TransactionData::OnChain(tx_data_remote) = response.into_inner() { + if tx_data_local == tx_data_remote { + info!("Checked transaction with hash {}", hash); + } else { + error!("Remote transaction with hash {} mismatch", hash); + error!("Local transaction:\n{:?}", tx_data_local); + error!("Remote transaction:\n{:?}", tx_data_remote); + error = true; + } + } else { + // should be never the case + error!("Remote transaction with hash {} is pending", hash); + error = true; + }; + } else { + error!("Remote transaction with hash {:?} not found", hash); + error = true; + } + } + + if error { + Err(anyhow::Error::msg("Validation failed")) + } else { + Ok(()) + } +} diff --git a/movement-migration/validation-tool/src/checks/api/submit_transaction.rs b/movement-migration/validation-tool/src/checks/api/submit_transaction.rs new file mode 100644 index 0000000000000..956aecf5f6ea5 --- /dev/null +++ b/movement-migration/validation-tool/src/checks/api/submit_transaction.rs @@ -0,0 +1,259 @@ +// Copyright (c) Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +use crate::types::api::MovementAptosRestClient; +use anyhow::Context; +use aptos_rest_client::aptos_api_types::TransactionOnChainData; +use aptos_sdk::transaction_builder::TransactionBuilder; +use aptos_sdk::types::{ + account_address::AccountAddress, + chain_id::ChainId, + transaction::{EntryFunction, SignedTransaction, TransactionPayload}, + LocalAccount, +}; +use clap::Parser; +use move_core_types::identifier::Identifier; +use move_core_types::language_storage::{ModuleId, TypeTag}; +use std::str::FromStr; +use std::time::{SystemTime, UNIX_EPOCH}; +use tracing::{info, warn}; + +#[derive(Parser)] +#[clap( + name = "submit-transaction", + about = "Transfers funds from one account to another account" +)] +pub struct SubmitTransaction { + #[clap(value_parser)] + #[clap(long = "movement-url", help = "The url of the Movement REST endpoint.")] + pub movement_rest_api_url: String, + #[clap(value_parser)] + #[clap(long = "aptos-url", help = "The url of the Aptos REST endpoint.")] + pub aptos_rest_api_url: String, + #[clap(value_parser)] + #[clap(long = "receiver", help = "The receiver account address.")] + pub receiver_address: String, + #[clap(value_parser)] + #[clap(long = "amount", help = "The amount of tokens to be sent.")] + pub send_amount: u64, +} + +impl SubmitTransaction { + pub async fn run(self) -> anyhow::Result<()> { + let private_key = std::env::var("MOVE_ACCOUNT_PRIVATE_KEY") + .context("MOVE_ACCOUNT_PRIVATE_KEY variable is not set")?; + let local_account = LocalAccount::from_private_key(&private_key, 0)?; + let remote_account = + AccountAddress::from_hex(self.receiver_address.trim_start_matches("0x"))?; + + let rest_client_movement = MovementAptosRestClient::new(&self.movement_rest_api_url)?; + let rest_client_aptos = MovementAptosRestClient::new(&self.aptos_rest_api_url)?; + let chain_id = check_chain_id(&rest_client_movement, &rest_client_aptos).await?; + check_sequence_number(&rest_client_movement, &rest_client_aptos, &local_account).await?; + check_balance( + &rest_client_movement, + &rest_client_aptos, + local_account.address(), + self.send_amount, + ) + .await?; + let transaction = + create_transaction(&local_account, remote_account, self.send_amount, chain_id)?; + // let json = serde_json::to_string_pretty(&transaction)?; + // info!("Transaction created:\n{}", json); + submit_transaction(&rest_client_movement, &rest_client_aptos, &transaction).await?; + Ok(()) + } +} + +#[test] +fn verify_tool() { + use clap::CommandFactory; + SubmitTransaction::command().debug_assert() +} + +async fn check_chain_id( + rest_client_movement: &MovementAptosRestClient, + rest_client_aptos: &MovementAptosRestClient, +) -> anyhow::Result { + let chain_id_movement = rest_client_movement + .get_index_bcs() + .await? + .into_inner() + .chain_id; + let chain_id_aptos = rest_client_aptos + .get_index_bcs() + .await? + .into_inner() + .chain_id; + + if chain_id_aptos == chain_id_movement { + info!("Chain-Id: {}", chain_id_movement); + Ok(ChainId::new(chain_id_movement)) + } else { + Err(anyhow::anyhow!( + "Chain-Id mismatch. Movement chain-id: {}. Aptos chain-id: {}.", + chain_id_movement, + chain_id_aptos + )) + } +} + +async fn check_sequence_number( + rest_client_movement: &MovementAptosRestClient, + rest_client_aptos: &MovementAptosRestClient, + local_account: &LocalAccount, +) -> anyhow::Result<()> { + let sequence_number_movement = rest_client_movement + .get_account_bcs(local_account.address()) + .await + .context(format!( + "Can't get the Movement account for the address {}", + local_account.address() + ))? + .into_inner() + .sequence_number(); + let sequence_number_aptos = rest_client_aptos + .get_account_bcs(local_account.address()) + .await + .context(format!( + "Can't get the Aptos account for the address {}", + local_account.address() + ))? + .into_inner() + .sequence_number(); + + if sequence_number_movement == sequence_number_aptos { + info!("Account sequence number: {}", sequence_number_movement); + local_account.set_sequence_number(sequence_number_movement); + Ok(()) + } else { + Err(anyhow::anyhow!( + "Sequence number mismatch. Movement sequence number: {}. Aptos sequence number: {}.", + sequence_number_movement, + sequence_number_aptos + )) + } +} + +async fn check_balance( + rest_client_movement: &MovementAptosRestClient, + rest_client_aptos: &MovementAptosRestClient, + account: AccountAddress, + amount: u64, +) -> anyhow::Result<()> { + let balance_movement = rest_client_movement + .view_apt_account_balance(account) + .await + .context(format!( + "Can't get the Movement balance for address {}", + account + ))? + .into_inner(); + let balance_aptos = rest_client_aptos + .view_apt_account_balance(account) + .await + .context(format!( + "Can't get the Aptos balance for address {}", + account + ))? + .into_inner(); + + if balance_movement == balance_aptos { + info!("Account address: {}", account); + info!("Account balance: {}", balance_movement); + + if amount <= balance_movement { + Ok(()) + } else { + Err(anyhow::anyhow!( + "The account balance is less than the amount to transfer" + )) + } + } else { + Err(anyhow::anyhow!( + "Balance mismatch. Movement account balance: {}. Aptos account balance: {}.", + balance_movement, + balance_aptos + )) + } +} + +fn create_transaction( + from_account: &LocalAccount, + to_account: AccountAddress, + amount: u64, + chain_id: ChainId, +) -> anyhow::Result { + info!( + "Sending {} Octas from {} to {}", + amount, + from_account.address(), + to_account + ); + let coin_type = "0x1::aptos_coin::AptosCoin"; + let max_gas_amount = 5_000; + let gas_unit_price = 100; + let transaction_builder = TransactionBuilder::new( + TransactionPayload::EntryFunction(EntryFunction::new( + ModuleId::new(AccountAddress::ONE, Identifier::new("coin")?), + Identifier::new("transfer")?, + vec![TypeTag::from_str(coin_type)?], + vec![bcs::to_bytes(&to_account)?, bcs::to_bytes(&amount)?], + )), + SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() + 60, + chain_id, + ) + .sender(from_account.address()) + .sequence_number(from_account.sequence_number()) + .max_gas_amount(max_gas_amount) + .gas_unit_price(gas_unit_price); + + Ok(from_account.sign_with_transaction_builder(transaction_builder)) +} + +async fn submit_transaction( + rest_client_movement: &MovementAptosRestClient, + rest_client_aptos: &MovementAptosRestClient, + transaction: &SignedTransaction, +) -> anyhow::Result<()> { + // Send the transaction to the Aptos node first. + // If the submission fails, we can fix the problem and reset the DB. + let tx_on_chain_data_aptos = rest_client_aptos + .submit_and_wait_bcs(transaction) + .await + .context("Failed to submit the transaction to Aptos")? + .into_inner(); + + log_tx_on_chain_data(&tx_on_chain_data_aptos, "Aptos")?; + + let tx_on_chain_data_movement = rest_client_movement + .submit_and_wait_bcs(transaction) + .await + .context("Failed to submit the transaction to Movement")? + .into_inner(); + + log_tx_on_chain_data(&tx_on_chain_data_movement, "Movement")?; + + if tx_on_chain_data_movement == tx_on_chain_data_aptos { + info!("Transaction on-chain data on Aptos equals the data on Movement"); + } else { + warn!("Transaction on-chain data on Aptos is different than the data on Movement"); + } + + Ok(()) +} + +fn log_tx_on_chain_data(data: &TransactionOnChainData, name: &str) -> anyhow::Result<()> { + let bytes = bcs::to_bytes(data)?; + let str = hex::encode(&bytes); + + info!("Transaction on-chain data ({}):\n{}", name, str); + info!( + "Transaction info ({}):\n{}", + name, + serde_json::to_string_pretty(&data.info)? + ); + + Ok(()) +} diff --git a/movement-migration/validation-tool/src/checks/api/transactions.rs b/movement-migration/validation-tool/src/checks/api/transactions.rs new file mode 100644 index 0000000000000..3a4d51b7c2055 --- /dev/null +++ b/movement-migration/validation-tool/src/checks/api/transactions.rs @@ -0,0 +1,110 @@ +// Copyright (c) Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +use crate::types::api::MovementAptosRestClient; +use clap::Parser; +use std::cmp::min; +use std::path::{Path, PathBuf}; +use tokio::fs::File; +use tokio::io::{AsyncWriteExt, BufWriter}; +use tracing::info; + +#[derive(Parser)] +#[clap( + name = "transactions", + about = "Gets a list of transactions from a validator node and stores them into a file" +)] +pub struct GetTransactions { + #[clap(value_parser)] + #[clap(long = "url", help = "The url of the Movement Aptos REST endpoint.")] + pub rest_api_url: String, + #[clap(long = "start", help = "The start ledger version")] + pub start: u64, + #[clap( + long = "limit", + help = "Limit how many ledger versions should be queried" + )] + pub limit: Option, + #[clap(long = "out", help = "Output path file name")] + pub output_path: PathBuf, +} + +impl GetTransactions { + pub async fn run(self) -> anyhow::Result<()> { + let rest_client = MovementAptosRestClient::new(&self.rest_api_url)?; + get_transactions(&rest_client, self.start, self.limit, self.output_path).await?; + Ok(()) + } +} + +#[test] +fn verify_tool() { + use clap::CommandFactory; + GetTransactions::command().debug_assert() +} + +async fn get_transactions( + rest_client: &MovementAptosRestClient, + start: u64, + limit: Option, + output_path: impl AsRef, +) -> Result<(), anyhow::Error> { + let response = rest_client.get_index_bcs().await?; + let latest_ledger_version: u64 = response.into_inner().ledger_version.into(); + let max_ledger_version = if let Some(limit) = limit { + let ledger_version = start + limit as u64; + min(ledger_version, latest_ledger_version) + } else { + latest_ledger_version + }; + let mut current_ledger_version = start; + let mut user_tx_count = 0usize; + let mut total_tx_count = 0usize; + let file = File::create(output_path).await?; + let mut writer = BufWriter::new(file); + + info!("Latest ledger version is {}", latest_ledger_version); + + while current_ledger_version < max_ledger_version { + info!( + "Getting transactions from version {}", + current_ledger_version + ); + let txs = rest_client + .get_transactions_bcs(Some(current_ledger_version), Some(100)) + .await? + .into_inner(); + + if txs.is_empty() { + info!("Transactions not found"); + break; + } + + let mut user_transactions = 0usize; + + for tx in txs.iter() { + if tx.transaction.try_as_signed_user_txn().is_some() { + let bytes = bcs::to_bytes(tx)?; + let str = format!("{}\n", hex::encode(&bytes)); + writer.write_all(str.as_bytes()).await?; + user_transactions += 1; + } + current_ledger_version = tx.version; + } + + current_ledger_version += 1; + user_tx_count += user_transactions; + total_tx_count += txs.len(); + info!( + "Node returned {} transactions ({} signed user transactions)", + txs.len(), + user_transactions + ); + } + + info!("Total transaction count is {}", total_tx_count); + info!("Total signed user transaction count is {}", user_tx_count); + + writer.flush().await?; + Ok(()) +} diff --git a/movement-migration/validation-tool/src/lib.rs b/movement-migration/validation-tool/src/lib.rs index eb17ac2610e36..b43c5d6d56361 100644 --- a/movement-migration/validation-tool/src/lib.rs +++ b/movement-migration/validation-tool/src/lib.rs @@ -13,14 +13,15 @@ mod types; disable_version_flag = true )] pub enum ValidationTool { - Api(checks::api::Command), + #[clap(subcommand)] + Api(checks::api::ApiTool), Node(checks::node::Command), } impl ValidationTool { pub async fn run(self) -> anyhow::Result<()> { match self { - ValidationTool::Api(cmd) => cmd.run().await, + ValidationTool::Api(tool) => tool.run().await, ValidationTool::Node(cmd) => cmd.run().await, } }