diff --git a/Cargo.lock b/Cargo.lock index f584133e4..766d76692 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3151,6 +3151,7 @@ dependencies = [ "serial_test", "tempfile", "tendermint-rpc", + "text-tables", "tokio", "toml 0.8.19", "tracing", @@ -9554,6 +9555,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "text-tables" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dc41925991e82af3c3e21e25a9aad92e72930af57fbcc4b07867a18d1cd0459" + [[package]] name = "textwrap" version = "0.11.0" diff --git a/Cargo.toml b/Cargo.toml index 30c5cc7d1..bf6855ffe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -172,6 +172,7 @@ tracing-subscriber = { version = "0.3", features = [ "registry", ] } tracing-appender = "0.2.3" +text-tables = "0.3.1" url = { version = "2.4.1", features = ["serde"] } zeroize = "1.6" diff --git a/fendermint/testing/materializer/Cargo.toml b/fendermint/testing/materializer/Cargo.toml index fabce48d2..dabf71418 100644 --- a/fendermint/testing/materializer/Cargo.toml +++ b/fendermint/testing/materializer/Cargo.toml @@ -29,6 +29,7 @@ tendermint-rpc = { workspace = true } tokio = { workspace = true } toml = { workspace = true } tracing = { workspace = true } +text-tables = { workspace = true } url = { workspace = true } arbitrary = { workspace = true, optional = true } diff --git a/fendermint/testing/materializer/src/bencher.rs b/fendermint/testing/materializer/src/bencher.rs new file mode 100644 index 000000000..6a204f61a --- /dev/null +++ b/fendermint/testing/materializer/src/bencher.rs @@ -0,0 +1,40 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use std::collections::HashMap; +use std::time::{Duration, Instant}; + +#[derive(Debug, Clone, Default)] +pub struct Bencher { + pub start_time: Option, + pub latencies: HashMap, + pub block_inclusion: Option, +} + +impl Bencher { + pub fn new() -> Self { + Self { + start_time: None, + latencies: HashMap::new(), + block_inclusion: None, + } + } + + pub fn start(&mut self) { + self.start_time = Some(Instant::now()); + } + + pub fn mempool(&mut self) { + self.set_latency("mempool".to_string()); + } + + pub fn block_inclusion(&mut self, block_number: u64) { + self.set_latency("block_inclusion".to_string()); + self.block_inclusion = Some(block_number); + } + + fn set_latency(&mut self, label: String) { + let duration = self.start_time.unwrap().elapsed(); + self.latencies.insert(label, duration); + } +} diff --git a/fendermint/testing/materializer/src/concurrency/collect.rs b/fendermint/testing/materializer/src/concurrency/collect.rs new file mode 100644 index 000000000..7bec16769 --- /dev/null +++ b/fendermint/testing/materializer/src/concurrency/collect.rs @@ -0,0 +1,37 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use crate::concurrency::signal::Signal; +use ethers::prelude::H256; +use ethers::providers::Http; +use ethers::providers::{Middleware, Provider}; +use ethers::types::Block; +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Duration; +use tokio::time::sleep; + +pub async fn collect_blocks( + cancel: Arc, + provider: Provider, + assert: F, +) -> anyhow::Result>> +where + F: Fn(&Block), +{ + let mut blocks = HashMap::new(); + loop { + if cancel.received() { + break; + } + + // TODO: improve: use less calls, make sure blocks cannot be missed + let block_number = provider.get_block_number().await?; + let block = provider.get_block(block_number).await?.unwrap(); + assert(&block); + blocks.insert(block_number.as_u64(), block); + + sleep(Duration::from_millis(100)).await; + } + Ok(blocks) +} diff --git a/fendermint/testing/materializer/src/concurrency/config.rs b/fendermint/testing/materializer/src/concurrency/config.rs new file mode 100644 index 000000000..ceed20a21 --- /dev/null +++ b/fendermint/testing/materializer/src/concurrency/config.rs @@ -0,0 +1,29 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use std::time::Duration; + +#[derive(Debug, Clone, Default)] +pub struct Execution { + pub steps: Vec, +} + +impl Execution { + pub fn new() -> Self { + Self { steps: Vec::new() } + } + + pub fn add_step(mut self, max_concurrency: usize, secs: u64) -> Self { + self.steps.push(ExecutionStep { + max_concurrency, + duration: Duration::from_secs(secs), + }); + self + } +} + +#[derive(Debug, Clone)] +pub struct ExecutionStep { + pub max_concurrency: usize, + pub duration: Duration, +} diff --git a/fendermint/testing/materializer/src/concurrency/mod.rs b/fendermint/testing/materializer/src/concurrency/mod.rs new file mode 100644 index 000000000..5eaf8a6e2 --- /dev/null +++ b/fendermint/testing/materializer/src/concurrency/mod.rs @@ -0,0 +1,80 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +pub mod collect; +pub mod config; +pub mod nonce_manager; +pub mod reporting; +pub mod signal; + +use crate::bencher::Bencher; +use crate::concurrency::reporting::TestResult; +use ethers::types::H256; +use futures::FutureExt; +use std::future::Future; +use std::pin::Pin; +use std::sync::Arc; +use std::time::Instant; +use tokio::sync::Semaphore; + +#[derive(Debug)] +pub struct TestInput { + pub test_id: usize, + pub bencher: Bencher, +} + +#[derive(Debug)] +pub struct TestOutput { + pub bencher: Bencher, + pub tx_hash: H256, +} + +pub async fn execute(cfg: config::Execution, test_factory: F) -> Vec> +where + F: Fn(TestInput) -> Pin> + Send>>, +{ + let mut test_id = 0; + let mut results = Vec::new(); + for (step_id, step) in cfg.steps.iter().enumerate() { + let semaphore = Arc::new(Semaphore::new(step.max_concurrency)); + let mut handles = Vec::new(); + let step_results = Arc::new(tokio::sync::Mutex::new(Vec::new())); + let execution_start = Instant::now(); + loop { + if execution_start.elapsed() > step.duration { + break; + } + let permit = semaphore.clone().acquire_owned().await.unwrap(); + let bencher = Bencher::new(); + let test_input = TestInput { test_id, bencher }; + let task = test_factory(test_input).boxed(); + let step_results = step_results.clone(); + let handle = tokio::spawn(async move { + let test_output = task.await; + let (bencher, tx_hash, err) = match test_output { + Ok(test_output) => (Some(test_output.bencher), Some(test_output.tx_hash), None), + Err(err) => (None, None, Some(err)), + }; + step_results.lock().await.push(TestResult { + test_id, + step_id, + bencher, + tx_hash, + err, + }); + drop(permit); + }); + handles.push(handle); + test_id += 1; + } + + // Exhaust unfinished handles. + for handle in handles { + handle.await.unwrap(); + } + + let step_results = Arc::try_unwrap(step_results).unwrap().into_inner(); + results.push(step_results) + } + results +} diff --git a/fendermint/testing/materializer/src/concurrency/nonce_manager.rs b/fendermint/testing/materializer/src/concurrency/nonce_manager.rs new file mode 100644 index 000000000..b397e0dd6 --- /dev/null +++ b/fendermint/testing/materializer/src/concurrency/nonce_manager.rs @@ -0,0 +1,34 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use ethers::prelude::H160; +use ethers::types::U256; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::Mutex; + +#[derive(Default)] +pub struct NonceManager { + nonces: Arc>>, +} + +impl NonceManager { + pub fn new() -> Self { + NonceManager { + nonces: Arc::new(Mutex::new(HashMap::new())), + } + } + + pub async fn set(&self, addr: H160, amount: U256) { + let mut nonces = self.nonces.lock().await; + nonces.insert(addr, amount); + } + + pub async fn get_and_increment(&self, addr: H160) -> U256 { + let mut nonces = self.nonces.lock().await; + let next_nonce = nonces.entry(addr).or_insert_with(U256::zero); + let current_nonce = *next_nonce; + *next_nonce += U256::one(); + current_nonce + } +} diff --git a/fendermint/testing/materializer/src/concurrency/reporting/dataset.rs b/fendermint/testing/materializer/src/concurrency/reporting/dataset.rs new file mode 100644 index 000000000..cbeb293d8 --- /dev/null +++ b/fendermint/testing/materializer/src/concurrency/reporting/dataset.rs @@ -0,0 +1,87 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use anyhow::anyhow; +use std::fmt::{Display, Formatter}; + +#[derive(Debug)] +pub struct Metrics { + pub mean: f64, + pub median: f64, + pub max: f64, + pub min: f64, + pub percentile_90: f64, +} + +impl Display for Metrics { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "mean: {:.2}, median: {:.2}, max: {:.2}, min: {:.2}, 90th: {:.2}", + self.mean, self.median, self.max, self.min, self.percentile_90 + ) + } +} + +impl Metrics { + pub fn format_median(&self) -> String { + format!("median: {:.2}", self.median) + } +} + +pub fn calc_metrics(data: Vec) -> anyhow::Result { + if data.is_empty() { + return Err(anyhow!("empty data")); + } + + let mut sorted_data = data.clone(); + sorted_data.sort_by(|a, b| a.partial_cmp(b).unwrap()); + + let count = sorted_data.len(); + let mean: f64 = sorted_data.iter().sum::() / count as f64; + + let median = if count % 2 == 0 { + (sorted_data[count / 2 - 1] + sorted_data[count / 2]) / 2.0 + } else { + sorted_data[count / 2] + }; + + let max = *sorted_data.last().unwrap(); + let min = *sorted_data.first().unwrap(); + + let percentile_90_index = ((count as f64) * 0.9).ceil() as usize - 1; + let percentile_90 = sorted_data[percentile_90_index]; + + Ok(Metrics { + mean, + median, + max, + min, + percentile_90, + }) +} + +#[cfg(test)] +mod tests { + use super::super::FLOAT_TOLERANCE; + use super::*; + + #[test] + fn test_calc_dataset_metrics() { + let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]; + + let expected_mean = 5.5; + let expected_median = 5.5; + let expected_max = 10.0; + let expected_min = 1.0; + let expected_percentile_90 = 9.0; + + let metrics = calc_metrics(data).unwrap(); + + assert!((metrics.mean - expected_mean).abs() < FLOAT_TOLERANCE); + assert!((metrics.median - expected_median).abs() < FLOAT_TOLERANCE); + assert!((metrics.max - expected_max).abs() < FLOAT_TOLERANCE); + assert!((metrics.min - expected_min).abs() < FLOAT_TOLERANCE); + assert!((metrics.percentile_90 - expected_percentile_90).abs() < FLOAT_TOLERANCE); + } +} diff --git a/fendermint/testing/materializer/src/concurrency/reporting/mod.rs b/fendermint/testing/materializer/src/concurrency/reporting/mod.rs new file mode 100644 index 000000000..2ae3337f8 --- /dev/null +++ b/fendermint/testing/materializer/src/concurrency/reporting/mod.rs @@ -0,0 +1,21 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +pub mod dataset; +pub mod summary; +pub mod tps; + +use crate::bencher::Bencher; +use ethers::prelude::H256; + +#[cfg(test)] +const FLOAT_TOLERANCE: f64 = 1e-6; + +#[derive(Debug)] +pub struct TestResult { + pub test_id: usize, + pub step_id: usize, + pub tx_hash: Option, + pub bencher: Option, + pub err: Option, +} diff --git a/fendermint/testing/materializer/src/concurrency/reporting/summary.rs b/fendermint/testing/materializer/src/concurrency/reporting/summary.rs new file mode 100644 index 000000000..a5893523d --- /dev/null +++ b/fendermint/testing/materializer/src/concurrency/reporting/summary.rs @@ -0,0 +1,185 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use super::{ + dataset::{calc_metrics, Metrics}, + tps::calc_tps, + TestResult, +}; +use crate::concurrency::config; +use crate::concurrency::config::ExecutionStep; +use anyhow::anyhow; +use ethers::prelude::{Block, H256}; +use std::collections::{HashMap, HashSet}; +use std::io; + +#[derive(Debug)] +pub struct ExecutionSummary { + pub summaries: Vec, +} + +impl ExecutionSummary { + pub fn new( + cfg: config::Execution, + blocks: HashMap>, + results: Vec>, + ) -> Self { + let step_txs = Self::map_results_to_txs(&results); + let step_blocks = Self::map_blocks_to_steps(blocks, step_txs); + + let mut summaries = Vec::new(); + for (i, results) in results.into_iter().enumerate() { + let cfg = cfg.steps[i].clone(); + let blocks = step_blocks[i].clone(); + summaries.push(StepSummary::new(cfg, results, blocks)); + } + + Self { summaries } + } + + pub fn to_result(&self) -> anyhow::Result<()> { + let errs = self.errs(); + if errs.is_empty() { + Ok(()) + } else { + Err(anyhow!(errs.join("\n"))) + } + } + + pub fn errs(&self) -> Vec { + let mut errs = Vec::new(); + for summary in self.summaries.iter() { + let cloned_errs: Vec = + summary.errs.iter().map(|e| format!("{:?}", e)).collect(); + errs.extend(cloned_errs); + } + errs + } + + pub fn print(&self) { + let mut data = vec![]; + + let latencies: HashSet = self + .summaries + .iter() + .flat_map(|summary| summary.latencies.keys().cloned()) + .collect(); + + let mut header = vec![ + "max_concurrency".to_string(), + "duration".to_string(), + "TPS".to_string(), + ]; + header.extend(latencies.iter().map(|key| format!("latency ({}) ", key))); + data.push(header); + + for summary in self.summaries.iter() { + let mut row = vec![]; + row.push(summary.cfg.max_concurrency.to_string()); + row.push(summary.cfg.duration.as_secs().to_string()); + row.push(summary.tps.format_median()); + + for key in &latencies { + let latency = summary + .latencies + .get(key) + .map_or(String::from("-"), |metrics| metrics.format_median() + "s"); + row.push(latency); + } + + data.push(row); + } + + text_tables::render(&mut io::stdout(), data).unwrap(); + } + + fn map_results_to_txs(results: &[Vec]) -> Vec> { + results + .iter() + .map(|step_results| { + step_results + .iter() + .filter_map(|result| result.tx_hash) + .collect() + }) + .collect() + } + + pub fn map_blocks_to_steps( + blocks: HashMap>, + step_txs: Vec>, + ) -> Vec>> { + let mut sorted_blocks: Vec<_> = blocks.into_iter().collect(); + sorted_blocks.sort_by_key(|(block_number, _)| *block_number); + + let mut step_mapped_blocks: Vec>> = vec![Vec::new(); step_txs.len()]; + + for (_, block) in sorted_blocks { + // Determine the max step_id based on the transactions in the block + let latest_step_id = block + .transactions + .iter() + .filter_map(|tx_hash| { + step_txs.iter().enumerate().find_map(|(step_id, txs)| { + if txs.contains(tx_hash) { + Some(step_id) + } else { + None + } + }) + }) + .max(); + + if let Some(step_id) = latest_step_id { + // Add the block to the corresponding step_id. + step_mapped_blocks[step_id].push(block); + } + } + + step_mapped_blocks + } +} + +#[derive(Debug)] +pub struct StepSummary { + pub cfg: ExecutionStep, + pub latencies: HashMap, + pub tps: Metrics, + pub errs: Vec, +} + +impl StepSummary { + fn new(cfg: ExecutionStep, results: Vec, blocks: Vec>) -> Self { + let mut latencies: HashMap> = HashMap::new(); + let mut errs = Vec::new(); + + for res in results { + let Some(bencher) = res.bencher else { continue }; + + for (key, duration) in bencher.latencies.clone() { + latencies + .entry(key.clone()) + .or_default() + .push(duration.as_secs_f64()); + } + + if let Some(err) = res.err { + errs.push(err); + } + } + + let latencies = latencies + .into_iter() + .map(|(key, dataset)| (key, calc_metrics(dataset).unwrap())) + .collect(); + + let tps = calc_metrics(calc_tps(blocks)).unwrap(); + + Self { + cfg, + latencies, + tps, + errs, + } + } +} diff --git a/fendermint/testing/materializer/src/concurrency/reporting/tps.rs b/fendermint/testing/materializer/src/concurrency/reporting/tps.rs new file mode 100644 index 000000000..93d0b74e4 --- /dev/null +++ b/fendermint/testing/materializer/src/concurrency/reporting/tps.rs @@ -0,0 +1,85 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use ethers::prelude::{Block, H256, U256}; + +pub fn calc_tps(blocks: Vec>) -> Vec { + let mut tps = Vec::new(); + + let offset = 1; // The first block is skipped for lacking a block interval. + for i in offset..blocks.len() { + let prev_block = &blocks[i - 1]; + let curr_block = &blocks[i]; + + let interval = curr_block.timestamp.saturating_sub(prev_block.timestamp); + + if interval.le(&U256::zero()) { + continue; + } + + let interval = interval.as_u64() as f64; + let tx_count = curr_block.transactions.len() as f64; + let block_tps = tx_count / interval; + tps.push(block_tps); + } + tps +} + +#[cfg(test)] +mod tests { + use super::super::FLOAT_TOLERANCE; + use super::*; + + #[test] + fn test_calc_tps() { + let tx = H256::random(); + let blocks = vec![ + Block { + timestamp: U256::from(100), + transactions: vec![tx; 500], + ..Default::default() + }, + Block { + timestamp: U256::from(110), + transactions: vec![tx; 600], + ..Default::default() + }, + Block { + timestamp: U256::from(130), + transactions: vec![tx; 1400], + ..Default::default() + }, + Block { + timestamp: U256::from(160), + transactions: vec![tx; 2400], + ..Default::default() + }, + Block { + timestamp: U256::from(200), + transactions: vec![tx; 4000], + ..Default::default() + }, + ] + .into_iter() + .collect(); + + let tps = calc_tps(blocks); + assert_eq!(tps.len(), 4); // Block 1 is skipped. + let expected_tps = [ + 600.0 / 10.0, // Block 2: 600 transactions in 10 seconds = 60 TPS + 1400.0 / 20.0, // Block 3: 1400 transactions in 20 seconds = 70 TPS + 2400.0 / 30.0, // Block 4: 2400 transactions in 30 seconds = 80 TPS + 4000.0 / 40.0, // Block 5: 4000 transactions in 40 seconds = 100 TPS + ]; + + for (i, &expected) in expected_tps.iter().enumerate() { + assert!( + (tps[i] - expected).abs() < FLOAT_TOLERANCE, + "mismatch at index {}: got {}, expected {}", + i, + tps[i], + expected + ); + } + } +} diff --git a/fendermint/testing/materializer/src/concurrency/signal.rs b/fendermint/testing/materializer/src/concurrency/signal.rs new file mode 100644 index 000000000..0067583cf --- /dev/null +++ b/fendermint/testing/materializer/src/concurrency/signal.rs @@ -0,0 +1,24 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +pub struct Signal(tokio::sync::Semaphore); + +impl Signal { + pub fn new() -> Self { + Self(tokio::sync::Semaphore::new(0)) + } + + pub fn send(&self) { + self.0.close(); + } + + pub fn received(&self) -> bool { + self.0.is_closed() + } +} + +impl Default for Signal { + fn default() -> Self { + Self::new() + } +} diff --git a/fendermint/testing/materializer/src/docker/mod.rs b/fendermint/testing/materializer/src/docker/mod.rs index e85e220e3..7e8fdcc9a 100644 --- a/fendermint/testing/materializer/src/docker/mod.rs +++ b/fendermint/testing/materializer/src/docker/mod.rs @@ -90,6 +90,7 @@ macro_rules! env_vars { }; } +#[derive(Debug)] pub struct DockerMaterials; impl Materials for DockerMaterials { @@ -160,6 +161,7 @@ pub struct DockerMaterializerState { port_ranges: BTreeMap, } +#[derive(Debug)] pub struct DockerMaterializer { dir: PathBuf, rng: StdRng, diff --git a/fendermint/testing/materializer/src/lib.rs b/fendermint/testing/materializer/src/lib.rs index 2a0d1965e..0ea4dd490 100644 --- a/fendermint/testing/materializer/src/lib.rs +++ b/fendermint/testing/materializer/src/lib.rs @@ -20,6 +20,8 @@ pub mod validation; #[cfg(feature = "arb")] mod arb; +pub mod bencher; +pub mod concurrency; /// An ID identifying a resource within its parent. #[derive(Clone, Serialize, PartialEq, Eq, PartialOrd, Ord)] diff --git a/fendermint/testing/materializer/src/testnet.rs b/fendermint/testing/materializer/src/testnet.rs index 607da9d7f..299d62885 100644 --- a/fendermint/testing/materializer/src/testnet.rs +++ b/fendermint/testing/materializer/src/testnet.rs @@ -140,6 +140,15 @@ where .ok_or_else(|| anyhow!("account {id} does not exist")) } + pub fn account_mod_nth(&self, v: usize) -> &M::Account { + let nth = v % self.accounts.len(); + self.accounts + .iter() + .nth(nth) + .map(|(_, account)| account) + .unwrap() + } + /// Get a node by name. pub fn node(&self, name: &NodeName) -> anyhow::Result<&M::Node> { self.nodes diff --git a/fendermint/testing/materializer/tests/docker.rs b/fendermint/testing/materializer/tests/docker.rs index 04038a183..af492b4fd 100644 --- a/fendermint/testing/materializer/tests/docker.rs +++ b/fendermint/testing/materializer/tests/docker.rs @@ -6,14 +6,6 @@ //! //! `cargo test -p fendermint_materializer --test docker -- --nocapture` -use std::{ - collections::BTreeSet, - env::current_dir, - path::PathBuf, - pin::Pin, - time::{Duration, Instant}, -}; - use anyhow::{anyhow, Context}; use ethers::providers::Middleware; use fendermint_materializer::{ @@ -22,8 +14,15 @@ use fendermint_materializer::{ testnet::Testnet, HasCometBftApi, HasEthApi, TestnetName, }; -use futures::Future; +use futures::{Future, FutureExt}; use lazy_static::lazy_static; +use std::{ + collections::BTreeSet, + env::current_dir, + path::PathBuf, + pin::Pin, + time::{Duration, Instant}, +}; use tendermint_rpc::Client; pub type DockerTestnet = Testnet; @@ -58,16 +57,16 @@ fn read_manifest(file_name: &str) -> anyhow::Result { } /// Parse a manifest file in the `manifests` directory, clean up any corresponding -/// testnet resources, then materialize a testnet and run some tests. -pub async fn with_testnet(manifest_file_name: &str, alter: G, test: F) -> anyhow::Result<()> +/// testnet resources, then materialize a testnet and provide a cleanup function. +pub async fn make_testnet( + manifest_file_name: &str, + alter: F, +) -> anyhow::Result<( + DockerTestnet, + Box Pin + Send>> + Send>, +)> where - // https://users.rust-lang.org/t/function-that-takes-a-closure-with-mutable-reference-that-returns-a-future/54324 - F: for<'a> FnOnce( - &Manifest, - &mut DockerMaterializer, - &'a mut DockerTestnet, - ) -> Pin> + 'a>>, - G: FnOnce(&mut Manifest), + F: FnOnce(&mut Manifest), { let testnet_name = TestnetName::new( PathBuf::from(manifest_file_name) @@ -98,48 +97,49 @@ where .await .context("failed to remove testnet")?; - let mut testnet = Testnet::setup(&mut materializer, &testnet_name, &manifest) + let testnet = Testnet::setup(&mut materializer, &testnet_name, &manifest) .await .context("failed to set up testnet")?; let started = wait_for_startup(&testnet).await?; - - let res = if started { - test(&manifest, &mut materializer, &mut testnet).await - } else { - Err(anyhow!("the startup sequence timed out")) - }; - - // Print all logs on failure. - // Some might be available in logs in the files which are left behind, - // e.g. for `fendermint` we have logs, but maybe not for `cometbft`. - if res.is_err() && *PRINT_LOGS_ON_ERROR { - for (name, node) in testnet.nodes() { - let name = name.path_string(); - for log in node.fendermint_logs().await { - eprintln!("{name}/fendermint: {log}"); - } - for log in node.cometbft_logs().await { - eprintln!("{name}/cometbft: {log}"); - } - for log in node.ethapi_logs().await { - eprintln!("{name}/ethapi: {log}"); - } - } + if !started { + return Err(anyhow!("the startup sequence timed out")); } - // Tear down the testnet. - drop(testnet); + let cleanup = Box::new(|failure: bool, testnet: DockerTestnet| { + async move { + println!("Cleaning up..."); + + // Print all logs on failure. + // Some might be available in logs in the files which are left behind, + // e.g. for `fendermint` we have logs, but maybe not for `cometbft`. + if failure && *PRINT_LOGS_ON_ERROR { + for (name, node) in testnet.nodes() { + let name = name.path_string(); + for log in node.fendermint_logs().await { + eprintln!("{name}/fendermint: {log}"); + } + for log in node.cometbft_logs().await { + eprintln!("{name}/cometbft: {log}"); + } + for log in node.ethapi_logs().await { + eprintln!("{name}/ethapi: {log}"); + } + } + } - // Allow some time for containers to be dropped. - // This only happens if the testnet setup succeeded, - // otherwise the system shuts down too quick, but - // at least we can inspect the containers. - // If they don't all get dropped, `docker system prune` helps. - let drop_handle = materializer.take_dropper(); - let _ = tokio::time::timeout(*TEARDOWN_TIMEOUT, drop_handle).await; + // Allow some time for containers to be dropped. + // This only happens if the testnet setup succeeded, + // otherwise the system shuts down too quick, but + // at least we can inspect the containers. + // If they don't all get dropped, `docker system prune` helps. + let drop_handle = materializer.take_dropper(); + let _ = tokio::time::timeout(*TEARDOWN_TIMEOUT, drop_handle).await; + } + .boxed() + }); - res + Ok((testnet, cleanup)) } /// Allow time for things to consolidate and APIs to start. diff --git a/fendermint/testing/materializer/tests/docker_tests/benches.rs b/fendermint/testing/materializer/tests/docker_tests/benches.rs new file mode 100644 index 000000000..041746bc5 --- /dev/null +++ b/fendermint/testing/materializer/tests/docker_tests/benches.rs @@ -0,0 +1,177 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use std::sync::Arc; +use std::time::Duration; + +use crate::make_testnet; +use anyhow::{bail, Context}; +use ethers::prelude::{Block, H256}; +use ethers::types::U256; +use ethers::{ + core::k256::ecdsa::SigningKey, + middleware::SignerMiddleware, + providers::{JsonRpcClient, Middleware, PendingTransaction, Provider}, + signers::{Signer, Wallet}, + types::{Eip1559TransactionRequest, H160}, +}; +use fendermint_materializer::concurrency::collect::collect_blocks; +use fendermint_materializer::concurrency::nonce_manager::NonceManager; +use fendermint_materializer::concurrency::reporting::summary::ExecutionSummary; +use fendermint_materializer::concurrency::signal::Signal; +use fendermint_materializer::concurrency::TestOutput; +use fendermint_materializer::{ + concurrency::{self, config::Execution}, + materials::DefaultAccount, + HasEthApi, +}; +use futures::FutureExt; +use tokio::time::sleep; + +const MANIFEST: &str = "benches.yaml"; + +pub type TestMiddleware = SignerMiddleware, Wallet>; + +/// Create a middleware that will assign nonces and sign the message. +async fn make_middleware( + provider: Provider, + sender: &DefaultAccount, + chain_id: U256, +) -> anyhow::Result> +where + C: JsonRpcClient, +{ + let wallet: Wallet = Wallet::from_bytes(sender.secret_key().serialize().as_ref())? + .with_chain_id(chain_id.as_u64()); + + Ok(SignerMiddleware::new(provider, wallet)) +} + +const BLOCK_GAS_LIMIT: u64 = 10_000_000_000; +const MAX_TX_GAS_LIMIT: u64 = 3_000_000; + +#[serial_test::serial] +#[tokio::test] +async fn test_native_coin_transfer() -> Result<(), anyhow::Error> { + let (testnet, cleanup) = make_testnet(MANIFEST, |_| {}).await?; + + let block_gas_limit = U256::from(BLOCK_GAS_LIMIT); + let max_tx_gas_limit = U256::from(MAX_TX_GAS_LIMIT); + + let pangea = testnet.node(&testnet.root().node("pangea"))?; + let provider = pangea + .ethapi_http_provider()? + .expect("ethapi should be enabled"); + let chain_id = provider + .get_chainid() + .await + .context("failed to get chain ID")?; + + let cancel = Arc::new(Signal::new()); + + // Set up background blocks collector. + let blocks_collector = { + let cancel = cancel.clone(); + let provider = provider.clone(); + let assert = move |block: &Block| { + // Make sure block gas limit isn't the bottleneck. + let unused_gas_limit = block_gas_limit - block.gas_limit; + assert!(unused_gas_limit >= max_tx_gas_limit); + }; + tokio::spawn(collect_blocks(cancel, provider, assert)) + }; + + // Drive concurrency. + let cfg = Execution::new() + .add_step(1, 5) + .add_step(10, 5) + .add_step(100, 5) + .add_step(150, 5); + let testnet = Arc::new(testnet); + let testnet_clone = testnet.clone(); + let nonce_manager = Arc::new(NonceManager::new()); + + let results = concurrency::execute(cfg.clone(), move |mut input| { + let testnet = testnet_clone.clone(); + let nonce_manager = nonce_manager.clone(); + let provider = provider.clone(); + + let test = async move { + let sender = testnet.account_mod_nth(input.test_id); + let recipient = testnet.account_mod_nth(input.test_id + 1); + println!("running (test_id={})", input.test_id); + + let middleware = make_middleware(provider, sender, chain_id) + .await + .context("make_middleware")?; + + let sender: H160 = sender.eth_addr().into(); + let nonce = nonce_manager.get_and_increment(sender).await; + + // Create the simplest transaction possible: send tokens between accounts. + let to: H160 = recipient.eth_addr().into(); + let mut tx = Eip1559TransactionRequest::new() + .to(to) + .value(1) + .nonce(nonce); + + let gas_estimation = middleware + .estimate_gas(&tx.clone().into(), None) + .await + .unwrap(); + tx = tx.gas(gas_estimation); + assert!(gas_estimation <= max_tx_gas_limit); + + input.bencher.start(); + + let pending: PendingTransaction<_> = middleware + .send_transaction(tx, None) + .await + .context("failed to send txn")?; + let tx_hash = pending.tx_hash(); + println!("sent tx {:?} (test_id={})", tx_hash, input.test_id); + + // We expect that the transaction is pending, however it should not return an error. + match middleware.get_transaction(tx_hash).await { + Ok(Some(_)) => {} + Ok(None) => bail!("pending transaction not found by eth hash"), + Err(e) => { + bail!("failed to get pending transaction: {e}") + } + } + input.bencher.mempool(); + + loop { + if let Ok(Some(tx)) = middleware.get_transaction_receipt(tx_hash).await { + println!( + "tx included in block {:?} (test_id={})", + tx.block_number, input.test_id + ); + let block_number = tx.block_number.unwrap().as_u64(); + input.bencher.block_inclusion(block_number); + break; + } + sleep(Duration::from_millis(100)).await; + } + + Ok(TestOutput { + bencher: input.bencher, + tx_hash, + }) + }; + test.boxed() + }) + .await; + + cancel.send(); + let blocks = blocks_collector.await??; + let summary = ExecutionSummary::new(cfg.clone(), blocks, results); + summary.print(); + + let res = summary.to_result(); + let Ok(testnet) = Arc::try_unwrap(testnet) else { + bail!("Arc::try_unwrap(testnet)"); + }; + cleanup(res.is_err(), testnet).await; + res +} diff --git a/fendermint/testing/materializer/tests/docker_tests/layer2.rs b/fendermint/testing/materializer/tests/docker_tests/layer2.rs index acfd0f8f4..11cf03c1c 100644 --- a/fendermint/testing/materializer/tests/docker_tests/layer2.rs +++ b/fendermint/testing/materializer/tests/docker_tests/layer2.rs @@ -3,7 +3,6 @@ use anyhow::{anyhow, bail, Context}; use ethers::core::types as et; use ethers::providers::Middleware; -use futures::FutureExt; use std::sync::Arc; use std::time::Duration; @@ -14,7 +13,7 @@ use fendermint_vm_message::conv::from_fvm::to_eth_address; use ipc_actors_abis::gateway_getter_facet::{GatewayGetterFacet, ParentFinality}; use ipc_actors_abis::subnet_actor_getter_facet::SubnetActorGetterFacet; -use crate::with_testnet; +use crate::make_testnet; const MANIFEST: &str = "layer2.yaml"; const CHECKPOINT_PERIOD: u64 = 10; @@ -24,126 +23,122 @@ const MAX_RETRIES: u32 = 5; /// Test that top-down syncing and bottom-up checkpoint submission work. #[serial_test::serial] #[tokio::test] -async fn test_topdown_and_bottomup() { - with_testnet( - MANIFEST, - |manifest| { - // Try to make sure the bottom-up checkpoint period is quick enough for reasonable test runtime. - let subnet = manifest - .subnets - .get_mut(&ResourceId::from("england")) - .expect("subnet not found"); - - subnet.bottom_up_checkpoint.period = CHECKPOINT_PERIOD; - }, - |_, _, testnet| { - let test = async { - let brussels = testnet.node(&testnet.root().node("brussels"))?; - let london = testnet.node(&testnet.root().subnet("england").node("london"))?; - let england = testnet.subnet(&testnet.root().subnet("england"))?; - - let london_provider = Arc::new( - london - .ethapi_http_provider()? - .ok_or_else(|| anyhow!("ethapi should be enabled"))?, - ); - - let brussels_provider = Arc::new( - brussels - .ethapi_http_provider()? - .ok_or_else(|| anyhow!("ethapi should be enabled"))?, - ); - - // Gateway actor on the child - let england_gateway = GatewayGetterFacet::new( - builtin_actor_eth_addr(ipc::GATEWAY_ACTOR_ID), - london_provider.clone(), - ); - - // Subnet actor on the parent - let england_subnet = SubnetActorGetterFacet::new( - to_eth_address(&england.subnet_id.subnet_actor()) - .and_then(|a| a.ok_or_else(|| anyhow!("not an eth address")))?, - brussels_provider.clone(), - ); - - // Query the latest committed parent finality and compare to the parent. - { - let mut retry = 0; - loop { - let finality: ParentFinality = england_gateway - .get_latest_parent_finality() - .call() - .await - .context("failed to get parent finality")?; - - // If the latest finality is not zero it means the syncer is working, - if finality.height.is_zero() { - if retry < MAX_RETRIES { - eprintln!("waiting for syncing with the parent..."); - tokio::time::sleep(Duration::from_secs(SLEEP_SECS)).await; - retry += 1; - continue; - } - bail!("the parent finality is still zero"); - } - - // Check that the block hash of the parent is actually the same at that height. - let parent_block: Option> = brussels_provider - .get_block(finality.height.as_u64()) - .await - .context("failed to get parent block")?; - - let Some(parent_block_hash) = parent_block.and_then(|b| b.hash) else { - bail!("cannot find parent block at final height"); - }; - - if parent_block_hash.0 != finality.block_hash { - bail!("the finality block hash is different from the API"); - } - break; +async fn test_topdown_and_bottomup() -> Result<(), anyhow::Error> { + let (testnet, cleanup) = make_testnet(MANIFEST, |manifest| { + // Try to make sure the bottom-up checkpoint period is quick enough for reasonable test runtime. + let subnet = manifest + .subnets + .get_mut(&ResourceId::from("england")) + .expect("subnet not found"); + + subnet.bottom_up_checkpoint.period = CHECKPOINT_PERIOD; + }) + .await?; + + let res = { + let brussels = testnet.node(&testnet.root().node("brussels"))?; + let london = testnet.node(&testnet.root().subnet("england").node("london"))?; + let england = testnet.subnet(&testnet.root().subnet("england"))?; + + let london_provider = Arc::new( + london + .ethapi_http_provider()? + .ok_or_else(|| anyhow!("ethapi should be enabled"))?, + ); + + let brussels_provider = Arc::new( + brussels + .ethapi_http_provider()? + .ok_or_else(|| anyhow!("ethapi should be enabled"))?, + ); + + // Gateway actor on the child + let england_gateway = GatewayGetterFacet::new( + builtin_actor_eth_addr(ipc::GATEWAY_ACTOR_ID), + london_provider.clone(), + ); + + // Subnet actor on the parent + let england_subnet = SubnetActorGetterFacet::new( + to_eth_address(&england.subnet_id.subnet_actor()) + .and_then(|a| a.ok_or_else(|| anyhow!("not an eth address")))?, + brussels_provider.clone(), + ); + + // Query the latest committed parent finality and compare to the parent. + { + let mut retry = 0; + loop { + let finality: ParentFinality = england_gateway + .get_latest_parent_finality() + .call() + .await + .context("failed to get parent finality")?; + + // If the latest finality is not zero it means the syncer is working, + if finality.height.is_zero() { + if retry < MAX_RETRIES { + eprintln!("waiting for syncing with the parent..."); + tokio::time::sleep(Duration::from_secs(SLEEP_SECS)).await; + retry += 1; + continue; } + bail!("the parent finality is still zero"); } - // Check that the parent knows about a checkpoint submitted from the child. - { - let mut retry = 0; - loop { - // NOTE: The implementation of the following method seems like a nonsense; - // I don't know if there is a way to ask the gateway what the latest - // checkpoint is, so we'll just have to go to the parent directly. - // let (has_checkpoint, epoch, _): (bool, et::U256, _) = england_gateway - // .get_current_bottom_up_checkpoint() - // .call() - // .await - // .context("failed to get current bottomup checkpoint")?; - let ckpt_height: et::U256 = england_subnet - .last_bottom_up_checkpoint_height() - .call() - .await - .context("failed to query last checkpoint height")?; - - if !ckpt_height.is_zero() { - break; - } - - if retry < MAX_RETRIES { - eprintln!("waiting for a checkpoint to be submitted..."); - tokio::time::sleep(Duration::from_secs(SLEEP_SECS)).await; - retry += 1; - continue; - } - - bail!("hasn't submitted a bottom-up checkpoint"); - } + // Check that the block hash of the parent is actually the same at that height. + let parent_block: Option> = brussels_provider + .get_block(finality.height.as_u64()) + .await + .context("failed to get parent block")?; + + let Some(parent_block_hash) = parent_block.and_then(|b| b.hash) else { + bail!("cannot find parent block at final height"); + }; + + if parent_block_hash.0 != finality.block_hash { + bail!("the finality block hash is different from the API"); + } + break; + } + } + + // Check that the parent knows about a checkpoint submitted from the child. + { + let mut retry = 0; + loop { + // NOTE: The implementation of the following method seems like a nonsense; + // I don't know if there is a way to ask the gateway what the latest + // checkpoint is, so we'll just have to go to the parent directly. + // let (has_checkpoint, epoch, _): (bool, et::U256, _) = england_gateway + // .get_current_bottom_up_checkpoint() + // .call() + // .await + // .context("failed to get current bottomup checkpoint")?; + let ckpt_height: et::U256 = england_subnet + .last_bottom_up_checkpoint_height() + .call() + .await + .context("failed to query last checkpoint height")?; + + if !ckpt_height.is_zero() { + break; } - Ok(()) - }; + if retry < MAX_RETRIES { + eprintln!("waiting for a checkpoint to be submitted..."); + tokio::time::sleep(Duration::from_secs(SLEEP_SECS)).await; + retry += 1; + continue; + } + + bail!("hasn't submitted a bottom-up checkpoint"); + } + } + + Ok(()) + }; - test.boxed_local() - }, - ) - .await - .unwrap() + cleanup(res.is_err(), testnet).await; + res } diff --git a/fendermint/testing/materializer/tests/docker_tests/mod.rs b/fendermint/testing/materializer/tests/docker_tests/mod.rs index a74d346dd..b3bf9de6d 100644 --- a/fendermint/testing/materializer/tests/docker_tests/mod.rs +++ b/fendermint/testing/materializer/tests/docker_tests/mod.rs @@ -5,6 +5,7 @@ //! sharing their materializer state. // Tests using the manifest bearing their name. +mod benches; pub mod layer2; pub mod root_only; pub mod standalone; diff --git a/fendermint/testing/materializer/tests/docker_tests/root_only.rs b/fendermint/testing/materializer/tests/docker_tests/root_only.rs index 293f26c55..d1f5fa65e 100644 --- a/fendermint/testing/materializer/tests/docker_tests/root_only.rs +++ b/fendermint/testing/materializer/tests/docker_tests/root_only.rs @@ -1,46 +1,38 @@ -use std::time::Duration; - // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT +use crate::make_testnet; use anyhow::{anyhow, bail}; use ethers::{providers::Middleware, types::U64}; use fendermint_materializer::HasEthApi; -use futures::FutureExt; - -use crate::with_testnet; +use std::time::Duration; const MANIFEST: &str = "root-only.yaml"; #[serial_test::serial] #[tokio::test] -async fn test_full_node_sync() { - with_testnet( - MANIFEST, - |_| {}, - |_, _, testnet| { - let test = async { - // Allow a little bit of time for node-2 to catch up with node-1. - tokio::time::sleep(Duration::from_secs(5)).await; - // Check that node2 is following node1. - let node2 = testnet.root().node("node-2"); - let dnode2 = testnet.node(&node2)?; - - let provider = dnode2 - .ethapi_http_provider()? - .ok_or_else(|| anyhow!("node-2 has ethapi enabled"))?; - - let bn = provider.get_block_number().await?; - - if bn <= U64::one() { - bail!("expected a block beyond genesis"); - } - - Ok(()) - }; - - test.boxed_local() - }, - ) - .await - .unwrap() +async fn test_full_node_sync() -> Result<(), anyhow::Error> { + let (testnet, cleanup) = make_testnet(MANIFEST, |_| {}).await?; + + let res = { + // Allow a little bit of time for node-2 to catch up with node-1. + tokio::time::sleep(Duration::from_secs(5)).await; + // Check that node2 is following node1. + let node2 = testnet.root().node("node-2"); + let dnode2 = testnet.node(&node2)?; + + let provider = dnode2 + .ethapi_http_provider()? + .ok_or_else(|| anyhow!("node-2 has ethapi enabled"))?; + + let bn = provider.get_block_number().await?; + + if bn <= U64::one() { + bail!("expected a block beyond genesis"); + } + + Ok(()) + }; + + cleanup(res.is_err(), testnet).await; + res } diff --git a/fendermint/testing/materializer/tests/docker_tests/standalone.rs b/fendermint/testing/materializer/tests/docker_tests/standalone.rs index 805943889..346cc071c 100644 --- a/fendermint/testing/materializer/tests/docker_tests/standalone.rs +++ b/fendermint/testing/materializer/tests/docker_tests/standalone.rs @@ -1,20 +1,17 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT - -use std::time::{Duration, Instant}; - +use crate::make_testnet; use anyhow::{bail, Context}; +use ethers::prelude::transaction::eip2718::TypedTransaction; use ethers::{ core::k256::ecdsa::SigningKey, middleware::SignerMiddleware, providers::{JsonRpcClient, Middleware, PendingTransaction, Provider}, signers::{Signer, Wallet}, - types::{transaction::eip2718::TypedTransaction, Eip1559TransactionRequest, H160}, + types::{Eip1559TransactionRequest, H160}, }; use fendermint_materializer::{manifest::Rootnet, materials::DefaultAccount, HasEthApi}; -use futures::FutureExt; - -use crate::with_testnet; +use std::time::{Duration, Instant}; const MANIFEST: &str = "standalone.yaml"; @@ -43,61 +40,56 @@ where /// from the ethereum API instance it was sent to even before it is included in the block. #[serial_test::serial] #[tokio::test] -async fn test_sent_tx_found_in_mempool() { - with_testnet( - MANIFEST, - |manifest| { - // Slow down consensus to where we can see the effect of the transaction not being found by Ethereum hash. - if let Rootnet::New { ref mut env, .. } = manifest.rootnet { - env.insert("CMT_CONSENSUS_TIMEOUT_COMMIT".into(), "10s".into()); - }; - }, - |_, _, testnet| { - let test = async { - let bob = testnet.account("bob")?; - let charlie = testnet.account("charlie")?; - - let pangea = testnet.node(&testnet.root().node("pangea"))?; - let provider = pangea - .ethapi_http_provider()? - .expect("ethapi should be enabled"); - - let middleware = make_middleware(provider, bob) - .await - .context("failed to set up middleware")?; - - eprintln!("middleware ready, pending tests"); - - // Create the simplest transaction possible: send tokens between accounts. - let to: H160 = charlie.eth_addr().into(); - let transfer = Eip1559TransactionRequest::new().to(to).value(1); - - let pending: PendingTransaction<_> = middleware - .send_transaction(transfer, None) - .await - .context("failed to send txn")?; - - let tx_hash = pending.tx_hash(); - - eprintln!("sent pending txn {:?}", tx_hash); - - // We expect that the transaction is pending, however it should not return an error. - match middleware.get_transaction(tx_hash).await { - Ok(Some(_)) => {} - Ok(None) => bail!("pending transaction not found by eth hash"), - Err(e) => { - bail!("failed to get pending transaction: {e}") - } - } - - Ok(()) - }; - - test.boxed_local() - }, - ) - .await - .unwrap() +async fn test_sent_tx_found_in_mempool() -> Result<(), anyhow::Error> { + let (testnet, cleanup) = make_testnet(MANIFEST, |manifest| { + // Slow down consensus to where we can see the effect of the transaction not being found by Ethereum hash. + if let Rootnet::New { ref mut env, .. } = manifest.rootnet { + env.insert("CMT_CONSENSUS_TIMEOUT_COMMIT".into(), "10s".into()); + }; + }) + .await?; + + let res = { + let bob = testnet.account("bob")?; + let charlie = testnet.account("charlie")?; + + let pangea = testnet.node(&testnet.root().node("pangea"))?; + let provider = pangea + .ethapi_http_provider()? + .expect("ethapi should be enabled"); + + let middleware = make_middleware(provider, bob) + .await + .context("failed to set up middleware")?; + + eprintln!("middleware ready, pending tests"); + + // Create the simplest transaction possible: send tokens between accounts. + let to: H160 = charlie.eth_addr().into(); + let transfer = Eip1559TransactionRequest::new().to(to).value(1); + + let pending: PendingTransaction<_> = middleware + .send_transaction(transfer, None) + .await + .context("failed to send txn")?; + + let tx_hash = pending.tx_hash(); + + eprintln!("sent pending txn {:?}", tx_hash); + + // We expect that the transaction is pending, however it should not return an error. + match middleware.get_transaction(tx_hash).await { + Ok(Some(_)) => {} + Ok(None) => bail!("pending transaction not found by eth hash"), + Err(e) => { + bail!("failed to get pending transaction: {e}") + } + } + Ok(()) + }; + + cleanup(res.is_err(), testnet).await; + res } /// Test that transactions sent out-of-order with regards to the nonce are not rejected, @@ -109,92 +101,88 @@ async fn test_out_of_order_mempool() { const MAX_WAIT_TIME: Duration = Duration::from_secs(10); const SLEEP_TIME: Duration = Duration::from_secs(1); - with_testnet( - MANIFEST, - |_| {}, - |_, _, testnet| { - let test = async { - let bob = testnet.account("bob")?; - let charlie = testnet.account("charlie")?; - - let pangea = testnet.node(&testnet.root().node("pangea"))?; - let provider = pangea - .ethapi_http_provider()? - .expect("ethapi should be enabled"); - - let middleware = make_middleware(provider, bob) - .await - .context("failed to set up middleware")?; - - // Create the simplest transaction possible: send tokens between accounts. - let to: H160 = charlie.eth_addr().into(); - let tx = Eip1559TransactionRequest::new().to(to).value(1); - let mut tx: TypedTransaction = tx.into(); - - // Fill out the nonce, gas, etc. - middleware - .fill_transaction(&mut tx, None) + let (testnet, cleanup) = make_testnet(MANIFEST, |_| {}).await.unwrap(); + + let res = async { + let bob = testnet.account("bob")?; + let charlie = testnet.account("charlie")?; + + let pangea = testnet.node(&testnet.root().node("pangea"))?; + let provider = pangea + .ethapi_http_provider()? + .expect("ethapi should be enabled"); + + let middleware = make_middleware(provider, bob) + .await + .context("failed to set up middleware")?; + + // Create the simplest transaction possible: send tokens between accounts. + let to: H160 = charlie.eth_addr().into(); + let tx = Eip1559TransactionRequest::new().to(to).value(1); + let mut tx: TypedTransaction = tx.into(); + + // Fill out the nonce, gas, etc. + middleware + .fill_transaction(&mut tx, None) + .await + .context("failed to fill tx")?; + + // Create a few more transactions to be sent out-of-order. + let mut txs = vec![tx]; + + for i in 1..5 { + let mut tx = txs[0].clone(); + let nonce = tx.nonce().expect("fill_transaction filled the nonce"); + tx.set_nonce(nonce.saturating_add(i.into())); + txs.push(tx) + } + + let mut pending_txs = Vec::new(); + + // Submit transactions in opposite order. + for (i, tx) in txs.iter().enumerate().rev() { + let sig = middleware + .signer() + .sign_transaction(tx) + .await + .context("failed to sign tx")?; + + let rlp = tx.rlp_signed(&sig); + + let pending_tx: PendingTransaction<_> = middleware + .send_raw_transaction(rlp) + .await + .with_context(|| format!("failed to send tx {i}"))?; + + pending_txs.push(pending_tx) + } + + // Check that they eventually get included. + let start = Instant::now(); + 'pending: loop { + for tx in pending_txs.iter() { + let receipt = middleware + .get_transaction_receipt(tx.tx_hash()) .await - .context("failed to fill tx")?; - - // Create a few more transactions to be sent out-of-order. - let mut txs = vec![tx]; - - for i in 1..5 { - let mut tx = txs[0].clone(); - let nonce = tx.nonce().expect("fill_transaction filled the nonce"); - tx.set_nonce(nonce.saturating_add(i.into())); - txs.push(tx) - } - - let mut pending_txs = Vec::new(); - - // Submit transactions in opposite order. - for (i, tx) in txs.iter().enumerate().rev() { - let sig = middleware - .signer() - .sign_transaction(tx) - .await - .context("failed to sign tx")?; - - let rlp = tx.rlp_signed(&sig); - - let pending_tx: PendingTransaction<_> = middleware - .send_raw_transaction(rlp) - .await - .with_context(|| format!("failed to send tx {i}"))?; - - pending_txs.push(pending_tx) - } - - // Check that they eventually get included. - let start = Instant::now(); - 'pending: loop { - for tx in pending_txs.iter() { - let receipt = middleware - .get_transaction_receipt(tx.tx_hash()) - .await - .context("failed to get receipt")?; - - if receipt.is_none() { - if start.elapsed() > MAX_WAIT_TIME { - bail!("some transactions are still not executed") - } else { - tokio::time::sleep(SLEEP_TIME).await; - continue 'pending; - } - } + .context("failed to get receipt")?; + + if receipt.is_none() { + if start.elapsed() > MAX_WAIT_TIME { + bail!("some transactions are still not executed") + } else { + tokio::time::sleep(SLEEP_TIME).await; + continue 'pending; } - // All of them have receipt. - break 'pending; } + } + // All of them have receipt. + break 'pending; + } - Ok(()) - }; + Ok(()) + }; - test.boxed_local() - }, - ) - .await - .unwrap() + let res = res.await; + cleanup(res.is_err(), testnet).await; + res.unwrap() } diff --git a/fendermint/testing/materializer/tests/manifests/benches.yaml b/fendermint/testing/materializer/tests/manifests/benches.yaml new file mode 100644 index 000000000..39c77b60d --- /dev/null +++ b/fendermint/testing/materializer/tests/manifests/benches.yaml @@ -0,0 +1,120 @@ +accounts: + "1": {} + "2": {} + "3": {} + "4": {} + "5": {} + "6": {} + "7": {} + "8": {} + "9": {} + "10": {} + "11": {} + "12": {} + "13": {} + "14": {} + "15": {} + "16": {} + "17": {} + "18": {} + "19": {} + "20": {} + "21": {} + "22": {} + "23": {} + "24": {} + "25": {} + "26": {} + "27": {} + "28": {} + "29": {} + "30": {} + "31": {} + "32": {} + "33": {} + "34": {} + "35": {} + "36": {} + "37": {} + "38": {} + "39": {} + "40": {} + "41": {} + "42": {} + "43": {} + "44": {} + "45": {} + "46": {} + "47": {} + "48": {} + "49": {} + "50": {} + +rootnet: + type: New + # Balances and collateral are in atto + validators: + "1": '100' + balances: + "1": '100000000000000000000' + "2": '200000000000000000000' + "3": '300000000000000000000' + "4": '100000000000000000000' + "5": '200000000000000000000' + "6": '300000000000000000000' + "7": '100000000000000000000' + "8": '200000000000000000000' + "9": '300000000000000000000' + "10": '100000000000000000000' + "11": '200000000000000000000' + "12": '300000000000000000000' + "13": '100000000000000000000' + "14": '200000000000000000000' + "15": '300000000000000000000' + "16": '100000000000000000000' + "17": '200000000000000000000' + "18": '300000000000000000000' + "19": '100000000000000000000' + "20": '200000000000000000000' + "21": '300000000000000000000' + "22": '100000000000000000000' + "23": '200000000000000000000' + "24": '300000000000000000000' + "25": '100000000000000000000' + "26": '200000000000000000000' + "27": '300000000000000000000' + "28": '100000000000000000000' + "29": '200000000000000000000' + "30": '300000000000000000000' + "31": '100000000000000000000' + "32": '200000000000000000000' + "33": '300000000000000000000' + "34": '100000000000000000000' + "35": '200000000000000000000' + "36": '300000000000000000000' + "37": '100000000000000000000' + "38": '200000000000000000000' + "39": '300000000000000000000' + "40": '100000000000000000000' + "41": '200000000000000000000' + "42": '300000000000000000000' + "43": '100000000000000000000' + "44": '200000000000000000000' + "45": '300000000000000000000' + "46": '100000000000000000000' + "47": '200000000000000000000' + "48": '300000000000000000000' + "49": '100000000000000000000' + "50": '200000000000000000000' + env: + CMT_CONSENSUS_TIMEOUT_COMMIT: 1s + FM_LOG_LEVEL: info,fendermint=debug + + nodes: + # A singleton node. + pangea: + mode: + type: Validator + validator: "1" + seed_nodes: [] + ethapi: true