diff --git a/Cargo.toml b/Cargo.toml index 227b2a5..ba3eecf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ opt-level = 3 # Starknet dependencies starknet = "0.6.0" -num_cpus = "1.0" +goose = "0.17.2" env_logger = "0.10.0" log = "0.4.17" tokio = { version = "1", features = ["full"] } @@ -24,7 +24,7 @@ futures = "0.3" clap = { version = "4.2.7", features = ["derive"] } color-eyre = "0.6.2" config = "0.13.3" -dotenv = "0.15.0" +dotenvy = "0.15.7" serde = "1.0.163" serde_derive = "1.0.163" serde_json = { version = "1.0.96", features = ["preserve_order"] } @@ -33,4 +33,4 @@ rand = { version = "0.8.5", features = ["rand_chacha"] } lazy_static = "1.4.0" colored = "2.0.4" sysinfo = "0.29.8" -statrs = "0.16.0" +crossbeam-queue = "0.3.11" diff --git a/src/actions/goose.rs b/src/actions/goose.rs new file mode 100644 index 0000000..c79bef2 --- /dev/null +++ b/src/actions/goose.rs @@ -0,0 +1,435 @@ +use std::{mem, sync::Arc}; + +use color_eyre::eyre::ensure; +use crossbeam_queue::ArrayQueue; +use goose::{config::GooseConfiguration, prelude::*}; +use serde::{de::DeserializeOwned, Serialize}; +use starknet::{ + accounts::{ + Account, Call, ConnectedAccount, ExecutionEncoder, RawExecution, SingleOwnerAccount, + }, + core::types::{ + ExecutionResult, FieldElement, InvokeTransactionResult, MaybePendingTransactionReceipt, + }, + macros::{felt, selector}, + providers::{ + jsonrpc::{ + HttpTransport, HttpTransportError, JsonRpcClientError, JsonRpcMethod, JsonRpcResponse, + }, + JsonRpcClient, ProviderError, + }, + signers::LocalWallet, +}; + +use crate::{ + actions::shoot::{GatlingShooterSetup, CHECK_INTERVAL, MAX_FEE}, + generators::get_rng, + utils::wait_for_tx, +}; + +use super::shoot::StarknetAccount; + +pub async fn erc20(shooter: &GatlingShooterSetup) -> color_eyre::Result<()> { + let environment = shooter.environment()?; + let erc20_address = environment.erc20_address; + let config = shooter.config(); + + ensure!( + config.run.num_erc20_transfers >= config.run.concurrency, + "Too few erc20 transfers for the amount of concurrency" + ); + + // div_euclid will truncate integers when not evenly divisable + let user_iterations = config + .run + .num_erc20_transfers + .div_euclid(config.run.concurrency); + // this will always be a multiple of concurrency, unlike num_erc20_transfers + let total_transactions = user_iterations * config.run.concurrency; + + // If these are not equal that means user_iterations was truncated + if total_transactions != config.run.num_erc20_transfers { + log::warn!("Number of erc20 transfers is not evenly divisble by concurrency, doing {total_transactions} transfers instead"); + } + + let goose_config = { + let mut default = GooseConfiguration::default(); + default.host = config.rpc.url.clone(); + default.iterations = user_iterations as usize; + default.users = Some(config.run.concurrency as usize); + default + }; + + let transfer_setup: TransactionFunction = + setup(environment.accounts.clone(), user_iterations as usize).await?; + + let transfer: TransactionFunction = + Arc::new(move |user| Box::pin(transfer(user, erc20_address))); + + let transfer_wait: TransactionFunction = goose_user_wait_last_tx(shooter.rpc_client().clone()); + + GooseAttack::initialize_with_config(goose_config.clone())? + .register_scenario( + scenario!("Transfer") + .register_transaction( + Transaction::new(transfer_setup) + .set_name("Transfer Setup") + .set_on_start(), + ) + .register_transaction( + Transaction::new(transfer) + .set_name("Transfer") + .set_sequence(1), + ) + .register_transaction( + Transaction::new(transfer_wait) + .set_name("Transfer Finalizing") + .set_sequence(2), + ) + .register_transaction( + transaction!(verify_transactions) + .set_name("Transfer Verification") + .set_sequence(3), + ), + ) + .execute() + .await?; + + Ok(()) +} + +pub async fn erc721(shooter: &GatlingShooterSetup) -> color_eyre::Result<()> { + let config = shooter.config(); + let environment = shooter.environment()?; + + ensure!( + config.run.num_erc20_transfers >= config.run.concurrency, + "Too few erc721 mints for the amount of concurrency" + ); + + // div_euclid will truncate integers when not evenly divisable + let user_iterations = config + .run + .num_erc721_mints + .div_euclid(config.run.concurrency); + // this will always be a multiple of concurrency, unlike num_erc721_mints + let total_transactions = user_iterations * config.run.concurrency; + + // If these are not equal that means user_iterations was truncated + if total_transactions != config.run.num_erc721_mints { + log::warn!("Number of erc721 mints is not evenly divisble by concurrency, doing {total_transactions} mints instead"); + } + + let goose_mint_config = { + let mut default = GooseConfiguration::default(); + default.host = config.rpc.url.clone(); + default.iterations = user_iterations as usize; + default.users = Some(config.run.concurrency as usize); + default + }; + + let nonces = Arc::new(ArrayQueue::new(total_transactions as usize)); + let erc721_address = environment.erc721_address; + let mut nonce = shooter.deployer_account().get_nonce().await?; + + for _ in 0..total_transactions { + nonces + .push(nonce) + .expect("ArrayQueue has capacity for all mints"); + nonce += FieldElement::ONE; + } + + let from_account = shooter.deployer_account().clone(); + + let mint_setup: TransactionFunction = + setup(environment.accounts.clone(), user_iterations as usize).await?; + + let mint: TransactionFunction = Arc::new(move |user| { + let nonce = nonces + .pop() + .expect("Nonce ArrayQueue should have enough nonces for all mints"); + let from_account = from_account.clone(); + Box::pin(async move { mint(user, erc721_address, nonce, &from_account).await }) + }); + + let mint_wait: TransactionFunction = goose_user_wait_last_tx(shooter.rpc_client().clone()); + + GooseAttack::initialize_with_config(goose_mint_config.clone())? + .register_scenario( + scenario!("Minting") + .register_transaction( + Transaction::new(mint_setup) + .set_name("Mint Setup") + .set_on_start(), + ) + .register_transaction(Transaction::new(mint).set_name("Minting").set_sequence(1)) + .register_transaction( + Transaction::new(mint_wait) + .set_name("Mint Finalizing") + .set_sequence(2), + ) + .register_transaction( + transaction!(verify_transactions) + .set_name("Mint Verification") + .set_sequence(3), + ), + ) + .execute() + .await?; + + Ok(()) +} + +#[derive(Debug, Clone)] +struct GooseUserState { + account: StarknetAccount, + nonce: FieldElement, + prev_tx: Vec, +} + +pub type RpcError = ProviderError>; + +impl GooseUserState { + pub async fn new( + account: StarknetAccount, + transactions_amount: usize, + ) -> Result { + Ok(Self { + nonce: account.get_nonce().await?, + account, + prev_tx: Vec::with_capacity(transactions_amount), + }) + } +} + +async fn setup( + accounts: Vec, + transactions_amount: usize, +) -> Result { + let queue = ArrayQueue::new(accounts.len()); + for account in accounts { + queue + .push(GooseUserState::new(account, transactions_amount).await?) + .expect("Queue should have enough space for all accounts as it's length is from the accounts vec"); + } + let queue = Arc::new(queue); + + Ok(Arc::new(move |user| { + let queue = queue.clone(); + user.set_session_data( + queue + .pop() + .expect("Not enough accounts were created for the amount of users"), + ); + + Box::pin(async { Ok(()) }) + })) +} + +fn goose_user_wait_last_tx(provider: Arc>) -> TransactionFunction { + Arc::new(move |user| { + let thing = user + .get_session_data::() + .expect("Should be in a goose user with GooseUserState session data") + .prev_tx + .last() + .expect("At least one transaction should have been done"); + + let provider = provider.clone(); + + Box::pin(async move { + wait_for_tx(&provider, *thing, CHECK_INTERVAL) + .await + .expect("Transaction should have been successful"); + + Ok(()) + }) + }) +} + +// Hex: 0xdead +// from_hex_be isn't const whereas from_mont is +const VOID_ADDRESS: FieldElement = FieldElement::from_mont([ + 18446744073707727457, + 18446744073709551615, + 18446744073709551615, + 576460752272412784, +]); + +async fn transfer(user: &mut GooseUser, erc20_address: FieldElement) -> TransactionResult { + let GooseUserState { account, nonce, .. } = user + .get_session_data::() + .expect("Should be in a goose user with GooseUserState session data"); + + let (amount_low, amount_high) = (felt!("1"), felt!("0")); + + let call = Call { + to: erc20_address, + selector: selector!("transfer"), + calldata: vec![VOID_ADDRESS, amount_low, amount_high], + }; + + let response: InvokeTransactionResult = send_execution( + user, + vec![call], + *nonce, + &account.clone(), + JsonRpcMethod::AddInvokeTransaction, + ) + .await?; + + let GooseUserState { nonce, prev_tx, .. } = + user.get_session_data_mut::().expect( + "Should be successful as we already asserted that the session data is a GooseUserState", + ); + + *nonce += FieldElement::ONE; + + prev_tx.push(response.transaction_hash); + + Ok(()) +} + +async fn mint( + user: &mut GooseUser, + erc721_address: FieldElement, + nonce: FieldElement, + from_account: &SingleOwnerAccount>, LocalWallet>, +) -> TransactionResult { + let recipient = user + .get_session_data::() + .expect("Should be in a goose user with GooseUserState session data") + .account + .clone() + .address(); + + let (token_id_low, token_id_high) = (get_rng(), felt!("0x0000")); + + let call = Call { + to: erc721_address, + selector: selector!("mint"), + calldata: vec![recipient, token_id_low, token_id_high], + }; + + let response: InvokeTransactionResult = send_execution( + user, + vec![call], + nonce, + from_account, + JsonRpcMethod::AddInvokeTransaction, + ) + .await?; + + user.get_session_data_mut::() + .expect( + "Should be successful as we already asserted that the session data is a GooseUserState", + ) + .prev_tx + .push(response.transaction_hash); + + Ok(()) +} + +async fn verify_transactions(user: &mut GooseUser) -> TransactionResult { + let transaction = user + .get_session_data_mut::() + .expect("Should be in a goose user with GooseUserState session data") + .prev_tx + .pop() + .expect("There should be enough previous transactions for every verification"); + + let receipt: MaybePendingTransactionReceipt = + send_request(user, JsonRpcMethod::GetTransactionReceipt, transaction).await?; + + match receipt { + MaybePendingTransactionReceipt::Receipt(receipt) => match receipt.execution_result() { + ExecutionResult::Succeeded => Ok(()), + ExecutionResult::Reverted { reason } => { + panic!("Transaction {transaction:#064x} has been rejected/reverted: {reason}"); + } + }, + MaybePendingTransactionReceipt::PendingReceipt(_) => { + panic!("Transaction {transaction:#064x} is pending when no transactions should be") + } + } +} + +pub async fn send_execution( + user: &mut GooseUser, + calls: Vec, + nonce: FieldElement, + from_account: &SingleOwnerAccount>, LocalWallet>, + method: JsonRpcMethod, +) -> Result> { + let calldata = from_account.encode_calls(&calls); + + #[allow(dead_code)] // Removes warning for unused fields, we need them to properly transmute + struct FakeRawExecution { + calls: Vec, + nonce: FieldElement, + max_fee: FieldElement, + } + + let raw_exec = FakeRawExecution { + calls, + nonce, + max_fee: MAX_FEE, + }; + + // TODO: We cannot right now construct RawExecution directly and need to use this hack + // see https://github.com/xJonathanLEI/starknet-rs/issues/538 + let raw_exec = unsafe { mem::transmute::(raw_exec) }; + + let param = starknet::core::types::BroadcastedInvokeTransaction { + sender_address: from_account.address(), + calldata, + max_fee: MAX_FEE, + signature: from_account + .sign_execution(&raw_exec) + .await + .expect("Raw Execution should be correctly constructed for signature"), + nonce, + is_query: false, + }; + + send_request(user, method, param).await +} + +pub async fn send_request( + user: &mut GooseUser, + method: JsonRpcMethod, + param: impl Serialize, +) -> Result> { + // Copied from https://docs.rs/starknet-providers/0.9.0/src/starknet_providers/jsonrpc/transports/http.rs.html#21-27 + #[derive(Debug, Serialize)] + struct JsonRpcRequest { + id: u64, + jsonrpc: &'static str, + method: JsonRpcMethod, + params: T, + } + + let request = JsonRpcRequest { + id: 1, + jsonrpc: "2.0", + method, + params: [param], + }; + + let body = user + .post_json("/", &request) + .await? + .response + .map_err(TransactionError::Reqwest)? + .json::>() + .await + .map_err(TransactionError::Reqwest)?; + + match body { + JsonRpcResponse::Success { result, .. } => Ok(result), + // Returning this error would probably be a good idea, + // but the goose error type doesn't allow it and we are + // required to return it as a constraint of TransactionFunction + JsonRpcResponse::Error { error, .. } => panic!("{error}"), + } +} diff --git a/src/actions/mod.rs b/src/actions/mod.rs index 35d8fb5..afcb4ef 100644 --- a/src/actions/mod.rs +++ b/src/actions/mod.rs @@ -1 +1,28 @@ -pub mod shoot; +use crate::config::GatlingConfig; + +use self::shoot::GatlingShooterSetup; + +mod goose; +mod shoot; + +pub async fn shoot(config: GatlingConfig) -> color_eyre::Result<()> { + let run_erc20 = config.run.num_erc20_transfers != 0; + let run_erc721 = config.run.num_erc721_mints != 0; + + let mut shooter = GatlingShooterSetup::from_config(config).await?; + shooter.setup().await?; + + if run_erc20 { + goose::erc20(&shooter).await?; + } else { + log::info!("Skipping erc20 transfers") + } + + if run_erc721 { + goose::erc721(&shooter).await?; + } else { + log::info!("Skipping erc721 mints") + } + + Ok(()) +} diff --git a/src/actions/shoot.rs b/src/actions/shoot.rs index 91b9c3e..01d17be 100644 --- a/src/actions/shoot.rs +++ b/src/actions/shoot.rs @@ -1,22 +1,13 @@ use crate::config::{ContractSourceConfig, GatlingConfig}; -use crate::generators::get_rng; -use crate::utils::{ - build_benchmark_report, compute_contract_address, sanitize_filename, wait_for_tx, - BenchmarkType, SYSINFO, -}; +use crate::utils::{compute_contract_address, wait_for_tx}; use color_eyre::eyre::Context; -use color_eyre::{eyre::eyre, Report as EyreReport, Result}; +use color_eyre::{eyre::eyre, Result}; use log::{debug, info, warn}; use starknet::core::types::contract::SierraClass; use std::collections::HashMap; use std::path::Path; -use tokio::task::JoinSet; - -use crate::metrics::BenchmarkReport; - -use rand::seq::SliceRandom; use starknet::accounts::{ Account, AccountFactory, Call, ConnectedAccount, ExecutionEncoding, OpenZeppelinAccountFactory, @@ -33,7 +24,7 @@ use starknet::providers::{MaybeUnknownErrorCode, StarknetErrorWithMessage}; use starknet::signers::{LocalWallet, SigningKey}; use std::str; use std::sync::Arc; -use std::time::{Duration, SystemTime}; +use std::time::Duration; use url::Url; @@ -41,29 +32,9 @@ use url::Url; pub static MAX_FEE: FieldElement = felt!("0x6efb28c75a0000"); pub static CHECK_INTERVAL: Duration = Duration::from_millis(500); -type StarknetAccount = SingleOwnerAccount>, LocalWallet>; - -/// Shoot the load test simulation. -pub async fn shoot(config: GatlingConfig) -> Result { - info!("starting simulation with config: {:#?}", config); - let mut shooter = GatlingShooter::from_config(config.clone()).await?; - let mut gatling_report = Default::default(); - // Trigger the setup phase. - shooter.setup(&mut gatling_report).await?; - - let threads = std::cmp::min(num_cpus::get(), config.run.concurrency as usize); - info!("Using {} threads", threads); - - // Run the benchmarks. - shooter.run(&mut gatling_report).await?; +pub type StarknetAccount = SingleOwnerAccount>, LocalWallet>; - // Trigger the teardown phase. - shooter.teardown(&mut gatling_report).await?; - - Ok(gatling_report) -} - -pub struct GatlingShooter { +pub struct GatlingShooterSetup { config: GatlingConfig, starknet_rpc: Arc>, signer: LocalWallet, @@ -74,12 +45,12 @@ pub struct GatlingShooter { #[derive(Clone)] pub struct GatlingEnvironment { - _erc20_address: FieldElement, - erc721_address: FieldElement, - accounts: Vec, + pub erc20_address: FieldElement, + pub erc721_address: FieldElement, + pub accounts: Vec, } -impl GatlingShooter { +impl GatlingShooterSetup { pub async fn from_config(config: GatlingConfig) -> Result { let starknet_rpc: Arc> = Arc::new(starknet_rpc_provider(Url::parse(&config.clone().rpc.url)?)); @@ -114,31 +85,26 @@ impl GatlingShooter { }) } - pub fn environment(&self) -> Result { - self.environment.clone().ok_or(eyre!( + pub fn environment(&self) -> Result<&GatlingEnvironment> { + self.environment.as_ref().ok_or(eyre!( "Environment is not yet populated, you should run the setup function first" )) } - /// Return a random account address from the ones deployed during the setup phase - /// or the deployer account address if no accounts were deployed or - /// if the environment is not yet populated - pub fn get_random_account(&self) -> StarknetAccount { - match self.environment() { - Ok(environment) => { - let mut rng = rand::thread_rng(); - environment - .accounts - .choose(&mut rng) - .unwrap_or(&self.account) - .clone() - } - Err(_) => self.account.clone(), - } + pub fn config(&self) -> &GatlingConfig { + &self.config + } + + pub fn rpc_client(&self) -> &Arc> { + &self.starknet_rpc + } + + pub fn deployer_account(&self) -> &StarknetAccount { + &self.account } /// Setup the simulation. - async fn setup<'a>(&mut self, _gatling_report: &'a mut GatlingReport) -> Result<()> { + pub async fn setup(&mut self) -> Result<()> { let chain_id = self.starknet_rpc.chain_id().await?.to_bytes_be(); let block_number = self.starknet_rpc.block_number().await?; info!( @@ -165,20 +131,17 @@ impl GatlingShooter { let erc20_address = self.deploy_erc20(erc20_class_hash).await?; let erc721_address = self.deploy_erc721(erc721_class_hash).await?; - let accounts = if setup_config.num_accounts > 0 { - self.create_accounts( + let accounts = self + .create_accounts( account_class_hash, - setup_config.num_accounts, + self.config.run.concurrency as usize, execution_encoding, erc20_address, ) - .await? - } else { - Vec::new() - }; + .await?; let environment = GatlingEnvironment { - _erc20_address: erc20_address, + erc20_address, erc721_address, accounts, }; @@ -188,313 +151,6 @@ impl GatlingShooter { Ok(()) } - /// Teardown the simulation. - async fn teardown<'a>(&mut self, gatling_report: &'a mut GatlingReport) -> Result<()> { - info!("Tearing down!"); - info!("{}", *SYSINFO); - - info!( - "Writing reports to `{}` directory", - self.config.report.reports_dir.display() - ); - for report in &gatling_report.benchmark_reports { - let report_path = self - .config - .report - .reports_dir - .join(sanitize_filename(&report.name)) - .with_extension("json"); - - std::fs::create_dir_all(&self.config.report.reports_dir)?; - let writer = std::fs::File::create(report_path)?; - serde_json::to_writer(writer, &report.to_json()?)?; - } - - Ok(()) - } - - async fn check_transactions( - &self, - transactions: Vec, - ) -> (Vec, Vec) { - info!("Checking transactions ..."); - let now = SystemTime::now(); - - let total_txs = transactions.len(); - - let mut accepted_txs = Vec::new(); - let mut errors = Vec::new(); - - let mut set = JoinSet::new(); - - let mut transactions = transactions.into_iter(); - - for _ in 0..self.config.run.concurrency { - if let Some(transaction) = transactions.next() { - let starknet_rpc = Arc::clone(&self.starknet_rpc); - set.spawn(async move { - wait_for_tx(&starknet_rpc, transaction, CHECK_INTERVAL) - .await - .map(|_| transaction) - .map_err(|err| (err, transaction)) - }); - } - } - - while let Some(res) = set.join_next().await { - if let Some(transaction) = transactions.next() { - let starknet_rpc = Arc::clone(&self.starknet_rpc); - set.spawn(async move { - wait_for_tx(&starknet_rpc, transaction, CHECK_INTERVAL) - .await - .map(|_| transaction) - .map_err(|err| (err, transaction)) - }); - } - - match res.unwrap() { - Ok(transaction) => { - accepted_txs.push(transaction); - debug!("Transaction {:#064x} accepted", transaction) - } - Err((err, transaction)) => { - errors.push(err); - debug!("Transaction {:#064x} rejected", transaction) - } - } - } - - info!( - "Took {} seconds to check transactions", - now.elapsed().unwrap().as_secs_f32() - ); - - let accepted_ratio = accepted_txs.len() as f64 / total_txs as f64 * 100.0; - let rejected_ratio = errors.len() as f64 / total_txs as f64 * 100.0; - - info!( - "{} transactions accepted ({:.2}%)", - accepted_txs.len(), - accepted_ratio, - ); - info!( - "{} transactions rejected ({:.2}%)", - errors.len(), - rejected_ratio - ); - - (accepted_txs, errors) - } - - /// Run the benchmarks. - async fn run<'a>(&mut self, gatling_report: &'a mut GatlingReport) -> Result<()> { - info!("❤️‍🔥 FIRING ! ❤️‍🔥"); - - let num_blocks = self.config.report.num_blocks; - let start_block = self.starknet_rpc.block_number().await?; - let mut transactions = Vec::new(); - - let num_erc20_transfers = self.config.run.num_erc20_transfers; - - let erc20_blocks = if num_erc20_transfers > 0 { - // Run ERC20 transfer transactions - let start_block = self.starknet_rpc.block_number().await?; - - let (mut transacs, _) = self.run_erc20(num_erc20_transfers).await; - - // Wait for the last transaction to be incorporated in a block - wait_for_tx( - &self.starknet_rpc, - *transacs.last().unwrap(), - CHECK_INTERVAL, - ) - .await?; - - let end_block = self.starknet_rpc.block_number().await?; - - transactions.append(&mut transacs); - - Ok((start_block, end_block)) - } else { - Err("0 ERC20 transfers to make") - }; - - let num_erc721_mints = self.config.run.num_erc721_mints; - - let erc721_blocks = if num_erc721_mints > 0 { - // Run ERC721 mint transactions - let start_block = self.starknet_rpc.block_number().await?; - - let (mut transacs, _) = self.run_erc721(num_erc721_mints).await; - - // Wait for the last transaction to be incorporated in a block - wait_for_tx( - &self.starknet_rpc, - *transacs.last().unwrap(), - CHECK_INTERVAL, - ) - .await?; - - let end_block = self.starknet_rpc.block_number().await?; - - transactions.append(&mut transacs); - - Ok((start_block, end_block)) - } else { - Err("0 ERC721 mints to make") - }; - - let end_block = self.starknet_rpc.block_number().await?; - - let full_blocks = if start_block < end_block { - Ok((start_block, end_block)) - } else { - Err("no executions were made") - }; - - // Build benchmark reports - for (token, blocks) in [ - ("ERC20", erc20_blocks), - ("ERC721", erc721_blocks), - ("Full", full_blocks), - ] { - match blocks { - Ok((start_block, end_block)) => { - // The transactions we sent will be incorporated in the next accepted block - build_benchmark_report( - self.starknet_rpc.clone(), - token.to_string(), - BenchmarkType::BlockRange(start_block + 1, end_block), - gatling_report, - ) - .await?; - - build_benchmark_report( - self.starknet_rpc.clone(), - format!("{token}_latest_{num_blocks}").to_string(), - BenchmarkType::LatestBlocks(num_blocks), - gatling_report, - ) - .await?; - } - Err(err) => warn!("Skip creating {token} reports because of `{err}`"), - }; - } - - // Check transactions - if !transactions.is_empty() { - self.check_transactions(transactions).await; - } else { - warn!("No load test was executed, are both mints and transfers set to 0?"); - } - - Ok(()) - } - - async fn run_erc20(&mut self, num_transfers: u64) -> (Vec, Vec) { - info!("Sending {num_transfers} ERC20 transfer transactions ..."); - - let start = SystemTime::now(); - - let mut accepted_txs = Vec::new(); - let mut errors = Vec::new(); - - for _ in 0..num_transfers { - match self - .transfer( - self.config.setup.fee_token_address, - self.get_random_account(), - FieldElement::from_hex_be("0xdead").unwrap(), - felt!("1"), - ) - .await - { - Ok(transaction_hash) => { - accepted_txs.push(transaction_hash); - } - Err(e) => { - let e = eyre!(e).wrap_err("Error while sending ERC20 transfer transaction"); - errors.push(e); - } - } - } - - let took = start.elapsed().unwrap().as_secs_f32(); - info!( - "Took {} seconds to send {} transfer transactions, on average {} sent per second", - took, - num_transfers, - num_transfers as f32 / took - ); - - let accepted_ratio = accepted_txs.len() as f64 / num_transfers as f64 * 100.0; - let rejected_ratio = errors.len() as f64 / num_transfers as f64 * 100.0; - - info!( - "{} transfer transactions sent successfully ({:.2}%)", - accepted_txs.len(), - accepted_ratio, - ); - info!( - "{} transfer transactions failed ({:.2}%)", - errors.len(), - rejected_ratio - ); - - (accepted_txs, errors) - } - - async fn run_erc721<'a>(&mut self, num_mints: u64) -> (Vec, Vec) { - let environment = self.environment().unwrap(); - - info!("Sending {num_mints} ERC721 mint transactions ..."); - - let start = SystemTime::now(); - - let mut accepted_txs = Vec::new(); - let mut errors = Vec::new(); - - for _ in 0..num_mints { - // TODO: Change the ERC721 contract such that mint is permissionless - match self - .mint(self.account.clone(), environment.erc721_address) - .await - { - Ok(transaction_hash) => { - accepted_txs.push(transaction_hash); - } - Err(e) => { - let e = eyre!(e).wrap_err("Error while sending ERC721 mint transaction"); - errors.push(e); - } - }; - } - - let took = start.elapsed().unwrap().as_secs_f32(); - info!( - "Took {} seconds to send {} mint transactions, on average {} sent per second", - took, - num_mints, - num_mints as f32 / took - ); - - let accepted_ratio = accepted_txs.len() as f64 / num_mints as f64 * 100.0; - let rejected_ratio = errors.len() as f64 / num_mints as f64 * 100.0; - - info!( - "{} mint transactions sent successfully ({:.2}%)", - accepted_txs.len(), - accepted_ratio, - ); - info!( - "{} mint transactions failed ({:.2}%)", - errors.len(), - rejected_ratio - ); - - (accepted_txs, errors) - } - async fn transfer( &mut self, contract_address: FieldElement, @@ -533,46 +189,6 @@ impl GatlingShooter { Ok(result.transaction_hash) } - async fn mint( - &mut self, - account: StarknetAccount, - contract_address: FieldElement, - ) -> Result { - let from_address = account.address(); - let nonce = match self.nonces.get(&from_address) { - Some(nonce) => *nonce, - None => account.get_nonce().await?, - }; - - debug!( - "Minting for address={:#064x} with nonce={}", - contract_address, nonce - ); - - let (token_id_low, token_id_high) = (get_rng(), felt!("0x0000")); - - let call = Call { - to: contract_address, - selector: selector!("mint"), - calldata: vec![ - self.get_random_account().address(), // recipient - token_id_low, - token_id_high, - ], - }; - - let result = account - .execute(vec![call]) - .max_fee(MAX_FEE) - .nonce(nonce) - .send() - .await?; - - self.nonces.insert(from_address, nonce + FieldElement::ONE); - - Ok(result.transaction_hash) - } - async fn deploy_erc721(&mut self, class_hash: FieldElement) -> Result { let contract_factory = ContractFactory::new(class_hash, self.account.clone()); let from_address = self.account.address(); @@ -914,12 +530,6 @@ impl GatlingShooter { } } -/// The simulation report. -#[derive(Debug, Default, Clone)] -pub struct GatlingReport { - pub benchmark_reports: Vec, -} - /// Create a StarkNet RPC provider from a URL. /// # Arguments /// * `rpc` - The URL of the StarkNet RPC provider. diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 4b549d3..04d2f5e 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -3,7 +3,7 @@ // Imports use clap::{Args, Parser, Subcommand}; -const VERSION_STRING: &str = concat!(env!("CARGO_PKG_VERSION")); +const VERSION_STRING: &str = env!("CARGO_PKG_VERSION"); /// Main CLI struct #[derive(Parser, Debug)] diff --git a/src/config.rs b/src/config.rs index 53e40d1..4192f5f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -89,7 +89,6 @@ pub struct SetupConfig { pub erc721_contract: ContractSourceConfig, pub account_contract: ContractSourceConfig, pub fee_token_address: FieldElement, - pub num_accounts: usize, #[serde(deserialize_with = "from_str_deserializer")] pub chain_id: FieldElement, } diff --git a/src/main.rs b/src/main.rs index 9ef2672..c3d3643 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,9 +2,9 @@ extern crate log; use clap::Parser; use color_eyre::eyre::Result; -use dotenv::dotenv; +use dotenvy::dotenv; use gatling::{ - actions::shoot::shoot, + actions, cli::{Cli, Command}, config::GatlingConfig, }; @@ -34,9 +34,9 @@ async fn main() -> Result<()> { // Execute the command. match cli.command { Command::Shoot { .. } => { - let gatling_report = shoot(cfg).await?; - info!("Gatling completed: {:#?}", gatling_report); + actions::shoot(cfg).await?; } } + Ok(()) } diff --git a/src/metrics.rs b/src/metrics.rs index 2a04fdf..6801c98 100644 --- a/src/metrics.rs +++ b/src/metrics.rs @@ -1,11 +1,10 @@ -use crate::utils::{get_num_tx_per_block, SYSINFO}; +use crate::utils::{get_num_tx_per_block, SysInfo, SYSINFO}; use color_eyre::Result; use serde_json::{json, Value}; use starknet::providers::{jsonrpc::HttpTransport, JsonRpcClient, Provider}; -use statrs::statistics::Statistics; -use std::{fmt, sync::Arc}; +use std::{fmt, ops::Deref, sync::Arc}; pub const BLOCK_TIME: u64 = 6; @@ -37,6 +36,12 @@ pub struct MetricResult { pub value: f64, } +/// The simulation report. +#[derive(Debug, Default, Clone)] +pub struct GatlingReport { + pub benchmark_reports: Vec, +} + /// A benchmark report contains a name (used for displaying) and a vector of metric results /// of all the metrics that were computed for the benchmark /// A benchmark report can be created from a block range or from the last x blocks @@ -51,12 +56,9 @@ impl BenchmarkReport { pub async fn from_block_range<'a>( starknet_rpc: Arc>, name: String, - start_block: u64, - end_block: u64, + mut start_block: u64, + mut end_block: u64, ) -> Result { - let mut start_block = start_block; - let mut end_block = end_block; - // Whenever possible, skip the first and last blocks from the metrics // to make sure all the blocks used for calculating metrics are full if end_block - start_block > 2 { @@ -85,53 +87,59 @@ impl BenchmarkReport { Ok(BenchmarkReport { name, metrics }) } - pub fn to_json(&self) -> Result { - let sysinfo_string = format!( - "CPU Count: {}\n\ - CPU Model: {}\n\ - CPU Speed (MHz): {}\n\ - Total Memory: {} GB\n\ - Platform: {}\n\ - Release: {}\n\ - Architecture: {}", - SYSINFO.cpu_count, - SYSINFO.cpu_frequency, - SYSINFO.cpu_brand, - SYSINFO.memory / (1024 * 1024 * 1024), - SYSINFO.os_name, - SYSINFO.kernel_version, - SYSINFO.arch - ); + pub fn to_json(&self) -> Value { + let SysInfo { + os_name, + kernel_version, + arch, + cpu_count, + cpu_frequency, + cpu_brand, + memory, + } = SYSINFO.deref(); - let mut report = vec![]; + let gigabyte_memory = memory / (1024 * 1024 * 1024); - for metric in self.metrics.iter() { - report.push(json!({ - "name": metric.name, - "unit": metric.unit, - "value": metric.value, - "extra": sysinfo_string - })); - } - - let report_json = serde_json::to_value(report)?; + let sysinfo_string = format!( + "CPU Count: {cpu_count}\n\ + CPU Model: {cpu_brand}\n\ + CPU Speed (MHz): {cpu_frequency}\n\ + Total Memory: {gigabyte_memory} GB\n\ + Platform: {os_name}\n\ + Release: {kernel_version}\n\ + Architecture: {arch}", + ); - Ok(report_json) + self.metrics + .iter() + .map(|metric| { + json!({ + "name": metric.name, + "unit": metric.unit, + "value": metric.value, + "extra": sysinfo_string + }) + }) + .collect::() } } impl fmt::Display for MetricResult { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}: {} {}", self.name, self.value, self.unit) + let Self { name, value, unit } = self; + + write!(f, "{name}: {value} {unit}") } } impl fmt::Display for BenchmarkReport { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - writeln!(f, "Benchmark Report: {}", self.name)?; + let Self { name, metrics } = self; + + writeln!(f, "Benchmark Report: {name}")?; - for metric in &self.metrics { - writeln!(f, "{}", metric)?; + for metric in metrics { + writeln!(f, "{metric}")?; } Ok(()) @@ -143,7 +151,7 @@ fn average_tps(num_tx_per_block: &[u64]) -> f64 { } fn average_tpb(num_tx_per_block: &[u64]) -> f64 { - num_tx_per_block.iter().map(|x| *x as f64).mean() + num_tx_per_block.iter().sum::() as f64 / num_tx_per_block.len() as f64 } pub fn compute_all_metrics(num_tx_per_block: Vec) -> Vec { diff --git a/src/utils.rs b/src/utils.rs index 5b30b27..d62470e 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -18,8 +18,7 @@ use starknet::{ use std::time::Duration; use sysinfo::{CpuExt, System, SystemExt}; -use crate::actions::shoot::{GatlingReport, CHECK_INTERVAL}; -use crate::metrics::BenchmarkReport; +use crate::metrics::{BenchmarkReport, GatlingReport}; lazy_static! { pub static ref SYSINFO: SysInfo = SysInfo::new(); @@ -92,16 +91,26 @@ impl Default for SysInfo { impl fmt::Display for SysInfo { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + os_name, + kernel_version, + arch, + cpu_count, + cpu_frequency, + cpu_brand, + memory, + } = self; + + let cpu_ghz_freq = *cpu_frequency as f64 / 1000.0; + let gigabyte_memory = memory / (1024 * 1024 * 1024); + writeln!( f, - "System Information:\nSystem : {} Kernel Version {}\nArch : {}\nCPU : {} {:.2}GHz {} cores\nMemory : {} GB", - self.os_name, - self.kernel_version, - self.arch, - self.cpu_brand, - format!("{:.2} GHz", self.cpu_frequency as f64 / 1000.0), - self.cpu_count, - self.memory / (1024 * 1024 * 1024) + "System Information:\n\ + System : {os_name} Kernel Version {kernel_version}\n\ + Arch : {arch}\n\ + CPU : {cpu_brand} {cpu_ghz_freq:.2} GHz {cpu_count} cores\n\ + Memory : {gigabyte_memory} GB" ) } } @@ -149,7 +158,7 @@ pub async fn wait_for_tx( .. })) => { debug!("Waiting for transaction {tx_hash:#064x} to show up"); - tokio::time::sleep(CHECK_INTERVAL).await; + tokio::time::sleep(check_interval).await; } Err(err) => { return Err(eyre!(err).wrap_err(format!(