From c9c3119a95c4309c9372a56ac854764d9beca912 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 22 May 2024 10:30:16 -0700 Subject: [PATCH] Add `aptos move run-repeadetly` command to submit same transaction multiple times --- Cargo.lock | 1 + crates/aptos/Cargo.toml | 1 + crates/aptos/src/common/types.rs | 93 ++++++++++++------ crates/aptos/src/move_tool/mod.rs | 41 ++++++++ .../aptos/src/move_tool/submit_repeatedly.rs | 96 +++++++++++++++++++ 5 files changed, 202 insertions(+), 30 deletions(-) create mode 100644 crates/aptos/src/move_tool/submit_repeatedly.rs diff --git a/Cargo.lock b/Cargo.lock index d7090f84d1d6e..17e69e1215099 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -251,6 +251,7 @@ dependencies = [ "aptos-cli-common", "aptos-config", "aptos-crypto", + "aptos-experimental-bulk-txn-submit", "aptos-faucet-core", "aptos-framework", "aptos-gas-profiling", diff --git a/crates/aptos/Cargo.toml b/crates/aptos/Cargo.toml index 69d8112f01d8f..68b390ae536f9 100644 --- a/crates/aptos/Cargo.toml +++ b/crates/aptos/Cargo.toml @@ -22,6 +22,7 @@ aptos-cached-packages = { workspace = true } aptos-cli-common = { workspace = true } aptos-config = { workspace = true } aptos-crypto = { workspace = true } +aptos-experimental-bulk-txn-submit = { workspace = true } aptos-faucet-core = { workspace = true } aptos-framework = { workspace = true } aptos-gas-profiling = { workspace = true } diff --git a/crates/aptos/src/common/types.rs b/crates/aptos/src/common/types.rs index 354a49ac0023a..189132e454018 100644 --- a/crates/aptos/src/common/types.rs +++ b/crates/aptos/src/common/types.rs @@ -1526,11 +1526,11 @@ pub struct TransactionOptions { impl TransactionOptions { /// Builds a rest client - fn rest_client(&self) -> CliTypedResult { + pub(crate) fn rest_client(&self) -> CliTypedResult { self.rest_options.client(&self.profile_options) } - pub fn get_transaction_account_type(&self) -> CliTypedResult { + pub(crate) fn get_transaction_account_type(&self) -> CliTypedResult { if self.private_key_options.private_key.is_some() || self.private_key_options.private_key_file.is_some() { @@ -1602,14 +1602,35 @@ impl TransactionOptions { .into_inner()) } - /// Submit a transaction - pub async fn submit_transaction( + pub(crate) fn get_now_timestamp_checked( &self, - payload: TransactionPayload, - ) -> CliTypedResult { - let client = self.rest_client()?; - let (sender_public_key, sender_address) = self.get_public_key_and_address()?; + onchain_timestamp_usecs: u64, + ) -> CliTypedResult { + // Retrieve local time, and ensure it's within an expected skew of the blockchain + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|err| CliError::UnexpectedError(err.to_string()))? + .as_secs(); + let now_usecs = now * US_IN_SECS; + // Warn local user that clock is skewed behind the blockchain. + // There will always be a little lag from real time to blockchain time + if now_usecs < onchain_timestamp_usecs - ACCEPTED_CLOCK_SKEW_US { + eprintln!("Local clock is is skewed from blockchain clock. Clock is more than {} seconds behind the blockchain {}", ACCEPTED_CLOCK_SKEW_US, onchain_timestamp_usecs / US_IN_SECS ); + } + Ok(now) + } + + pub(crate) async fn compute_gas_price_and_max_gas( + &self, + payload: &TransactionPayload, + client: &Client, + sender_address: &AccountAddress, + sender_public_key: &Ed25519PublicKey, + sequence_number: u64, + chain_id: ChainId, + expiration_time_secs: u64, + ) -> CliTypedResult<(u64, u64)> { // Ask to confirm price if the gas unit price is estimated above the lowest value when // it is automatically estimated let ask_to_confirm_price; @@ -1623,27 +1644,6 @@ impl TransactionOptions { gas_unit_price }; - // Get sequence number for account - let (account, state) = get_account_with_state(&client, sender_address).await?; - let sequence_number = account.sequence_number; - - // Retrieve local time, and ensure it's within an expected skew of the blockchain - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map_err(|err| CliError::UnexpectedError(err.to_string()))? - .as_secs(); - let now_usecs = now * US_IN_SECS; - - // Warn local user that clock is skewed behind the blockchain. - // There will always be a little lag from real time to blockchain time - if now_usecs < state.timestamp_usecs - ACCEPTED_CLOCK_SKEW_US { - eprintln!("Local clock is is skewed from blockchain clock. Clock is more than {} seconds behind the blockchain {}", ACCEPTED_CLOCK_SKEW_US, state.timestamp_usecs / US_IN_SECS ); - } - let expiration_time_secs = now + self.gas_options.expiration_secs; - - let chain_id = ChainId::new(state.chain_id); - // TODO: Check auth key against current private key and provide a better message - let max_gas = if let Some(max_gas) = self.gas_options.max_gas { // If the gas unit price was estimated ask, but otherwise you've chosen hwo much you want to spend if ask_to_confirm_price { @@ -1657,7 +1657,7 @@ impl TransactionOptions { let unsigned_transaction = transaction_factory .payload(payload.clone()) - .sender(sender_address) + .sender(*sender_address) .sequence_number(sequence_number) .expiration_timestamp_secs(expiration_time_secs) .build(); @@ -1698,6 +1698,39 @@ impl TransactionOptions { adjusted_max_gas }; + Ok((gas_unit_price, max_gas)) + } + + /// Submit a transaction + pub async fn submit_transaction( + &self, + payload: TransactionPayload, + ) -> CliTypedResult { + let client = self.rest_client()?; + let (sender_public_key, sender_address) = self.get_public_key_and_address()?; + + // Get sequence number for account + let (account, state) = get_account_with_state(&client, sender_address).await?; + let sequence_number = account.sequence_number; + + let now = self.get_now_timestamp_checked(state.timestamp_usecs)?; + let expiration_time_secs = now + self.gas_options.expiration_secs; + + let chain_id = ChainId::new(state.chain_id); + // TODO: Check auth key against current private key and provide a better message + + let (gas_unit_price, max_gas) = self + .compute_gas_price_and_max_gas( + &payload, + &client, + &sender_address, + &sender_public_key, + sequence_number, + chain_id, + expiration_time_secs, + ) + .await?; + // Sign and submit transaction let transaction_factory = TransactionFactory::new(chain_id) .with_gas_unit_price(gas_unit_price) diff --git a/crates/aptos/src/move_tool/mod.rs b/crates/aptos/src/move_tool/mod.rs index 5d31321d96c05..31b44b4114004 100644 --- a/crates/aptos/src/move_tool/mod.rs +++ b/crates/aptos/src/move_tool/mod.rs @@ -1,6 +1,7 @@ // Copyright © Aptos Foundation // SPDX-License-Identifier: Apache-2.0 +use self::submit_repeatedly::submit_repeatedly; use crate::{ account::derive_resource_account::ResourceAccountSeed, common::{ @@ -72,6 +73,7 @@ mod manifest; pub mod package_hooks; mod show; pub mod stored_package; +mod submit_repeatedly; /// Tool for Move related operations /// @@ -99,6 +101,7 @@ pub enum MoveTool { Publish(PublishPackage), Run(RunFunction), RunScript(RunScript), + RunRepeatedly(RunFunctionRepeatedly), #[clap(subcommand, hide = true)] Show(show::ShowTool), Test(TestPackage), @@ -131,6 +134,7 @@ impl MoveTool { MoveTool::Prove(tool) => tool.execute_serialized().await, MoveTool::Publish(tool) => tool.execute_serialized().await, MoveTool::Run(tool) => tool.execute_serialized().await, + MoveTool::RunRepeatedly(tool) => tool.execute_serialized().await, MoveTool::RunScript(tool) => tool.execute_serialized().await, MoveTool::Show(tool) => tool.execute_serialized().await, MoveTool::Test(tool) => tool.execute_serialized().await, @@ -1446,6 +1450,43 @@ impl CliCommand for RunFunction { } } +/// Run a Move function +#[derive(Parser)] +pub struct RunFunctionRepeatedly { + #[clap(flatten)] + pub(crate) entry_function_args: EntryFunctionArguments, + #[clap(flatten)] + pub(crate) txn_options: TransactionOptions, + + #[clap(long)] + num_times: usize, + + #[clap(long, default_value = "10")] + single_request_api_batch_size: usize, + + #[clap(long, default_value = "10")] + parallel_requests_outstanding: usize, +} + +#[async_trait] +impl CliCommand for RunFunctionRepeatedly { + fn command_name(&self) -> &'static str { + "RunFunctionRepeatedly" + } + + async fn execute(self) -> CliTypedResult { + submit_repeatedly( + &self.txn_options, + TransactionPayload::EntryFunction(self.entry_function_args.try_into()?), + self.num_times, + self.single_request_api_batch_size, + self.parallel_requests_outstanding, + ) + .await + .map(|v| format!("Committed {} txns", v)) + } +} + /// Run a view function #[derive(Parser)] pub struct ViewFunction { diff --git a/crates/aptos/src/move_tool/submit_repeatedly.rs b/crates/aptos/src/move_tool/submit_repeatedly.rs new file mode 100644 index 0000000000000..e6fd254fd86b9 --- /dev/null +++ b/crates/aptos/src/move_tool/submit_repeatedly.rs @@ -0,0 +1,96 @@ +// Copyright (c) Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +use crate::common::{ + types::{AccountType, CliError, CliTypedResult, TransactionOptions}, + utils::{get_account_with_state, prompt_yes_with_override}, +}; +use aptos_experimental_bulk_txn_submit::{ + coordinator::execute_txn_list, workloads::FixedPayloadSignedTransactionBuilder, +}; +use aptos_sdk::{transaction_builder::TransactionFactory, types::LocalAccount}; +use aptos_types::{chain_id::ChainId, transaction::TransactionPayload}; +use std::time::Duration; + +/// For transaction payload and options, either get gas profile or submit for execution. +pub async fn submit_repeatedly( + txn_options_ref: &TransactionOptions, + payload: TransactionPayload, + num_times: usize, + single_request_api_batch_size: usize, + parallel_requests_outstanding: usize, +) -> CliTypedResult { + if txn_options_ref.profile_gas || txn_options_ref.benchmark || txn_options_ref.local { + return Err(CliError::UnexpectedError( + "Cannot perform profiling, benchmarking or local execution for submit repeatedly." + .to_string(), + )); + } + + let client = txn_options_ref.rest_client()?; + let (sender_public_key, sender_address) = txn_options_ref.get_public_key_and_address()?; + + // Get sequence number for account + let (account, state) = get_account_with_state(&client, sender_address).await?; + let sequence_number = account.sequence_number; + + let sender_account = match txn_options_ref.get_transaction_account_type()? { + AccountType::Local => { + let (private_key, _) = txn_options_ref.get_key_and_address()?; + LocalAccount::new(sender_address, private_key, sequence_number) + }, + AccountType::HardwareWallet => { + return Err(CliError::UnexpectedError( + "Cannot use hardware wallet to submit repeatedly.".to_string(), + )); + }, + }; + + let now = txn_options_ref.get_now_timestamp_checked(state.timestamp_usecs)?; + let expiration_time_secs = now + txn_options_ref.gas_options.expiration_secs; + + let chain_id = ChainId::new(state.chain_id); + // TODO: Check auth key against current private key and provide a better message + + let (gas_unit_price, max_gas) = txn_options_ref + .compute_gas_price_and_max_gas( + &payload, + &client, + &sender_address, + &sender_public_key, + sequence_number, + chain_id, + expiration_time_secs, + ) + .await?; + + // Sign and submit transaction + let transaction_factory = TransactionFactory::new(chain_id) + .with_gas_unit_price(gas_unit_price) + .with_max_gas_amount(max_gas) + .with_transaction_expiration_time(txn_options_ref.gas_options.expiration_secs); + + prompt_yes_with_override( + &format!( + "About to submit {} transactions and spend up to {} APT. Continue?", + num_times, + num_times as f32 * gas_unit_price as f32 * max_gas as f32 / 1e8 + ), + txn_options_ref.prompt_options, + )?; + + let results = execute_txn_list( + vec![sender_account], + vec![client], + (0..num_times).map(|_| ()).collect::>(), + single_request_api_batch_size, + parallel_requests_outstanding, + Duration::from_secs_f32(0.05), + transaction_factory, + FixedPayloadSignedTransactionBuilder::new(payload), + true, + ) + .await?; + + Ok(results.into_iter().filter(|v| *v == "success").count()) +}