Skip to content

Commit

Permalink
Merge branch 'support-non-eth-receipts' into brock/pr30-mods
Browse files Browse the repository at this point in the history
  • Loading branch information
zeroXbrock committed Oct 4, 2024
2 parents 7af343b + 0c04fc4 commit eb1c56f
Show file tree
Hide file tree
Showing 10 changed files with 188 additions and 65 deletions.
9 changes: 9 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# MIT License

Copyright 2024 Flashbots

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Contender is a high-performance Ethereum network spammer and testing tool design
To install the Contender CLI, you need to have the [Rust toolchain](https://rustup.rs/) installed on your system. Then build the project from source:

```bash
git clone https://github.com/zeroxbrock/contender.git
git clone https://github.com/flashbots/contender.git
cd contender/cli
cargo build --release
alias contender="$PWD/target/release/contender_cli"
Expand Down Expand Up @@ -48,9 +48,9 @@ To use Contender as a library in your Rust project, add the crates you need to y
```toml
[dependencies]
...
contender = { git = "https://github.com/zeroxbrock/contender" }
contender_sqlite = { git = "https://github.com/zeroxbrock/contender" }
contender_testfile = { git = "https://github.com/zeroxbrock/contender" }
contender = { git = "https://github.com/flashbots/contender" }
contender_sqlite = { git = "https://github.com/flashbots/contender" }
contender_testfile = { git = "https://github.com/flashbots/contender" }
# not necessarily required, but recommended:
tokio = { version = "1.40.0", features = ["rt-multi-thread"] }
```
Expand Down
16 changes: 16 additions & 0 deletions cli/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ May be specified multiple times."
visible_aliases = &["dr"]
)]
disable_reports: bool,

/// The minimum balance to check for each private key.
#[arg(
long,
long_help = "The minimum balance to check for each private key in decimal-ETH format (`--min-balance 1.5` means 1.5 * 1e18 wei).",
default_value = "1.0"
)]
min_balance: String,
},

#[command(
Expand All @@ -95,6 +103,14 @@ May be specified multiple times."
May be specified multiple times."
)]
private_keys: Option<Vec<String>>,

/// The minimum balance to check for each private key.
#[arg(
long,
long_help = "The minimum balance to check for each private key in decimal-ETH format (ex: `--min-balance 1.5` means 1.5 * 1e18 wei).",
default_value = "1.0"
)]
min_balance: String,
},

#[command(
Expand Down
134 changes: 113 additions & 21 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
mod commands;

use alloy::{
providers::ProviderBuilder, signers::local::PrivateKeySigner, transports::http::reqwest::Url,
network::AnyNetwork,
primitives::{
utils::{format_ether, parse_ether},
Address, U256,
},
providers::{Provider, ProviderBuilder},
signers::local::PrivateKeySigner,
transports::http::reqwest::Url,
};
use commands::{ContenderCli, ContenderSubcommand};
use contender_core::{
db::{DbOps, RunTx},
generator::{types::RpcProvider, RandSeed},
generator::{
types::{AnyProvider, FunctionCallDefinition},
RandSeed,
},
spammer::{BlockwiseSpammer, LogCallback, NilCallback, TimedSpammer},
test_scenario::TestScenario,
};
Expand All @@ -22,6 +32,26 @@ static DB: LazyLock<SqliteDb> = std::sync::LazyLock::new(|| {
SqliteDb::from_file("contender.db").expect("failed to open contender.db")
});

fn get_signers_with_defaults(private_keys: Option<Vec<String>>) -> Vec<PrivateKeySigner> {
if private_keys.is_none() {
println!("No private keys provided. Using default private keys.");
}
let private_keys = private_keys.unwrap_or_default();
let private_keys = [
private_keys,
DEFAULT_PRV_KEYS
.into_iter()
.map(|s| s.to_owned())
.collect::<Vec<_>>(),
]
.concat();

private_keys
.into_iter()
.map(|k| PrivateKeySigner::from_str(&k).expect("Invalid private key"))
.collect::<Vec<PrivateKeySigner>>()
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = ContenderCli::parse_args();
Expand All @@ -31,15 +61,22 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
testfile,
rpc_url,
private_keys,
min_balance,
} => {
let url = Url::parse(rpc_url.as_ref()).expect("Invalid RPC URL");
let rpc_client = ProviderBuilder::new()
.network::<AnyNetwork>()
.on_http(url.to_owned());
let testconfig: TestConfig = TestConfig::from_file(&testfile)?;
let min_balance = parse_ether(&min_balance)?;

let private_keys = private_keys.expect("Must provide private keys for setup");
let signers: Vec<PrivateKeySigner> = private_keys
.iter()
.map(|k| PrivateKeySigner::from_str(k).expect("Invalid private key"))
.collect();
let signers = get_signers_with_defaults(private_keys);
let setup = testconfig
.setup
.as_ref()
.expect("No setup function calls found in testfile");
check_private_keys(setup, &signers);
check_balances(&signers, min_balance, &rpc_client).await;

let scenario = TestScenario::new(
testconfig.to_owned(),
Expand All @@ -62,29 +99,37 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
seed,
private_keys,
disable_reports,
min_balance,
} => {
let testfile = TestConfig::from_file(&testfile)?;
let testconfig = TestConfig::from_file(&testfile)?;
let rand_seed = seed.map(|s| RandSeed::from_str(&s)).unwrap_or_default();
let url = Url::parse(rpc_url.as_ref()).expect("Invalid RPC URL");
let rpc_client = Arc::new(ProviderBuilder::new().on_http(url.to_owned()));
let rpc_client = ProviderBuilder::new()
.network::<AnyNetwork>()
.on_http(url.to_owned());
let duration = duration.unwrap_or_default();
let min_balance = parse_ether(&min_balance)?;

let signers = private_keys.as_ref().map(|keys| {
keys.iter()
.map(|k| PrivateKeySigner::from_str(k).expect("Invalid private key"))
.collect::<Vec<PrivateKeySigner>>()
});
let signers = get_signers_with_defaults(private_keys);
let spam = testconfig
.spam
.as_ref()
.expect("No spam function calls found in testfile");
check_private_keys(spam, &signers);
check_balances(&signers, min_balance, &rpc_client).await;

if txs_per_block.is_some() && txs_per_second.is_some() {
panic!("Cannot set both --txs-per-block and --txs-per-second");
}
if txs_per_block.is_none() && txs_per_second.is_none() {
panic!("Must set either --txs-per-block (--tpb) or --txs-per-second (--tps)");
}

if let Some(txs_per_block) = txs_per_block {
let signers = signers.expect("must provide private keys for blockwise spamming");
let scenario =
TestScenario::new(testfile, DB.clone().into(), url, rand_seed, &signers);
TestScenario::new(testconfig, DB.clone().into(), url, rand_seed, &signers);
println!("Blockwise spamming with {} txs per block", txs_per_block);
match spam_callback_default(!disable_reports, rpc_client.into()).await {
match spam_callback_default(!disable_reports, Arc::new(rpc_client).into()).await {
SpamCallbackType::Log(cback) => {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
Expand All @@ -105,8 +150,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
return Ok(());
}

// private keys are not used for timed spamming; timed spamming only works with unlocked accounts
let scenario = TestScenario::new(testfile, DB.clone().into(), url, rand_seed, &[]);
// NOTE: private keys are not currently used for timed spamming.
// Timed spamming only works with unlocked accounts, because it uses the `eth_sendTransaction` RPC method.
let scenario =
TestScenario::new(testconfig, DB.clone().into(), url, rand_seed, &signers);
let tps = txs_per_second.unwrap_or(10);
println!("Timed spamming with {} txs per second", tps);
let spammer = TimedSpammer::new(scenario, NilCallback::new());
Expand Down Expand Up @@ -141,7 +188,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut writer = WriterBuilder::new()
.has_headers(true)
.from_writer(std::io::stdout());
write_run_txs(&mut writer, &txs)?;
write_run_txs(&mut writer, &txs)?; // TODO: write a macro that lets us generalize the writer param to write_run_txs, then refactor this duplication
};
}
}
Expand All @@ -153,9 +200,35 @@ enum SpamCallbackType {
Nil(NilCallback),
}

/// Panics if any of the function calls' `from` addresses do not have a corresponding private key.
fn check_private_keys(fn_calls: &[FunctionCallDefinition], prv_keys: &[PrivateKeySigner]) {
for fn_call in fn_calls {
let address = fn_call
.from
.parse::<Address>()
.expect("invalid 'from' address");
if prv_keys.iter().all(|k| k.address() != address) {
panic!("No private key found for address: {}", address);
}
}
}

const DEFAULT_PRV_KEYS: [&str; 10] = [
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
"0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d",
"0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a",
"0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6",
"0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a",
"0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba",
"0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e",
"0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356",
"0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97",
"0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6",
];

async fn spam_callback_default(
log_txs: bool,
rpc_client: Option<Arc<RpcProvider>>,
rpc_client: Option<Arc<AnyProvider>>,
) -> SpamCallbackType {
if let Some(rpc_client) = rpc_client {
if log_txs {
Expand All @@ -165,6 +238,25 @@ async fn spam_callback_default(
SpamCallbackType::Nil(NilCallback::new())
}

async fn check_balances(
prv_keys: &[PrivateKeySigner],
min_balance: U256,
rpc_client: &AnyProvider,
) {
for prv_key in prv_keys {
let address = prv_key.address();
let balance = rpc_client.get_balance(address).await.unwrap();
if balance < min_balance {
panic!(
"Insufficient balance for address {}. Required={} Actual={}. If needed, use --min-balance to set a lower threshold.",
address,
format_ether(min_balance),
format_ether(balance)
);
}
}
}

fn write_run_txs<T: std::io::Write>(
writer: &mut Writer<T>,
txs: &[RunTx],
Expand Down
4 changes: 3 additions & 1 deletion src/generator/types.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use alloy::{
network::AnyNetwork,
primitives::U256,
providers::RootProvider,
rpc::types::TransactionRequest,
Expand All @@ -8,7 +9,8 @@ use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tokio::task::JoinHandle;

pub type RpcProvider = RootProvider<Http<Client>>;
pub type EthProvider = RootProvider<Http<Client>>;
pub type AnyProvider = RootProvider<Http<Client>, AnyNetwork>;

#[derive(Clone, Debug)]
pub struct NamedTxRequest {
Expand Down
24 changes: 15 additions & 9 deletions src/spammer/blockwise.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ use crate::db::DbOps;
use crate::error::ContenderError;
use crate::generator::seeder::Seeder;
use crate::generator::templater::Templater;
use crate::generator::types::{PlanType, RpcProvider};
use crate::generator::types::{AnyProvider, EthProvider, PlanType};
use crate::generator::{Generator, PlanConfig};
use crate::test_scenario::TestScenario;
use crate::Result;
use alloy::hex::ToHexExt;
use alloy::network::TransactionBuilder;
use alloy::network::{AnyNetwork, TransactionBuilder};
use alloy::primitives::FixedBytes;
use alloy::providers::{Provider, ProviderBuilder};
use futures::StreamExt;
Expand All @@ -28,7 +28,8 @@ where
{
scenario: TestScenario<D, S, P>,
msg_handler: Arc<TxActorHandle>,
rpc_client: Arc<RpcProvider>,
rpc_client: AnyProvider,
eth_client: Arc<EthProvider>,
callback_handler: Arc<F>,
}

Expand All @@ -40,17 +41,22 @@ where
P: PlanConfig<String> + Templater<String> + Send + Sync,
{
pub fn new(scenario: TestScenario<D, S, P>, callback_handler: F) -> Self {
let rpc_client = Arc::new(ProviderBuilder::new().on_http(scenario.rpc_url.to_owned()));
let rpc_client = ProviderBuilder::new()
.network::<AnyNetwork>()
.on_http(scenario.rpc_url.to_owned());
let eth_client = Arc::new(ProviderBuilder::new().on_http(scenario.rpc_url.to_owned()));
let msg_handler = Arc::new(TxActorHandle::new(
12,
scenario.db.clone(),
rpc_client.clone(),
Arc::new(rpc_client.to_owned()),
));
let callback_handler = Arc::new(callback_handler);

Self {
scenario,
rpc_client,
callback_handler: Arc::new(callback_handler),
eth_client,
callback_handler,
msg_handler,
}
}
Expand Down Expand Up @@ -165,15 +171,15 @@ where

if !gas_limits.contains_key(fn_sig.as_slice()) {
let gas_limit = self
.rpc_client
.eth_client
.estimate_gas(&tx.tx.to_owned())
.await
.map_err(|e| ContenderError::with_err(e, "failed to estimate gas"))?;
gas_limits.insert(fn_sig, gas_limit);
}

// clone muh Arcs
let rpc_client = self.rpc_client.clone();
let eth_client = self.eth_client.clone();
let callback_handler = self.callback_handler.clone();

// query hashmaps for gaslimit & signer of this tx
Expand All @@ -193,7 +199,7 @@ where
tasks.push(task::spawn(async move {
let provider = ProviderBuilder::new()
.wallet(signer)
.on_provider(rpc_client);
.on_provider(eth_client);

let full_tx = tx_req
.clone()
Expand Down
Loading

0 comments on commit eb1c56f

Please sign in to comment.