Skip to content
137 changes: 137 additions & 0 deletions crates/cli/src/commands/spamd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
use super::{SpamCampaignContext, SpamCommandArgs};
use crate::CliError;
use crate::{
commands::{self},
util::data_dir,
};
use contender_core::db::DbOps;
use std::{
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
time::Duration,
};
use tracing::{debug, error, info, warn};

/// Runs spam in a loop, potentially executing multiple spam runs.
///
/// If `limit_loops` is `None`, it will run indefinitely.
///
/// If `limit_loops` is `Some(n)`, it will run `n` times.
///
/// If `gen_report` is `true`, it will generate a report at the end.
pub async fn spamd(
db: &(impl DbOps + Clone + Send + Sync + 'static),
args: SpamCommandArgs,
gen_report: bool,
limit_loops: Option<u64>,
) -> Result<(), CliError> {
let is_done = Arc::new(AtomicBool::new(false));
let mut scenario = args.init_scenario(db).await?;

// collects run IDs from the spam command
let mut run_ids = vec![];

// if CTRL-C signal is received, set `is_done` to true
{
let is_done = is_done.clone();
tokio::task::spawn(async move {
tokio::signal::ctrl_c()
.await
.expect("Failed to listen for CTRL-C");
info!(
"CTRL-C received. Spam daemon will shut down as soon as current batch finishes..."
);
is_done.store(true, Ordering::SeqCst);
});
}

// runs spam command in a loop
let mut i = 0;
// this holds a Some value only when a timeout has been started.
let mut timeout_start = None;
loop {
let mut do_finish = false;
if let Some(loops) = &limit_loops {
if i >= *loops {
do_finish = true;
}
i += 1;
}
if is_done.load(Ordering::SeqCst) {
do_finish = true;
}
if do_finish {
debug!("spamd loop finished");
break;
}

let db = db.clone();
let spam_res =
commands::spam(&db, &args, &mut scenario, SpamCampaignContext::default()).await;
let wait_time = Duration::from_secs(3);

if let Err(e) = spam_res {
error!("spam run failed: {e:?}");

if timeout_start.is_none() {
let start_time = std::time::Instant::now();
timeout_start = Some(start_time);
warn!("retrying in {} seconds...", wait_time.as_secs());
tokio::time::sleep(wait_time).await;
continue;
}

if let Some(timeout_start) = timeout_start {
if std::time::Instant::now().duration_since(timeout_start)
> args.spam_args.spam_timeout
{
warn!("timeout reached, quitting spam loop...");
scenario.ctx.cancel_token.cancel();
break;
} else {
tokio::time::sleep(wait_time).await;
}
} else {
scenario.ctx.cancel_token.cancel();
break;
}
} else {
timeout_start = None;
let run_id = spam_res.expect("spam");
if let Some(run_id) = run_id {
run_ids.push(run_id);
}
}
}

// generate a report if requested; in closure for tokio::select to handle CTRL-C
let run_report = || async move {
if gen_report {
if run_ids.is_empty() {
warn!("No runs found, exiting.");
return Ok::<_, CliError>(());
}
let first_run_id = run_ids.iter().min().expect("no run IDs found");
let last_run_id = *run_ids.iter().max().expect("no run IDs found");
contender_report::command::report(
Some(last_run_id),
last_run_id - first_run_id,
db,
&data_dir()?,
)
.await?;
}
Ok(())
};

tokio::select! {
_ = run_report() => {},
_ = tokio::signal::ctrl_c() => {
info!("CTRL-C received, cancelling report...");
}
}

Ok(())
}
7 changes: 1 addition & 6 deletions crates/cli/src/default_scenarios/blobs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,6 @@ fn blob_txs(blob_data: impl AsRef<str>, recipient: Option<String>) -> Vec<SpamRe

impl ToTestConfig for BlobsCliArgs {
fn to_testconfig(&self) -> contender_testfile::TestConfig {
TestConfig {
env: None,
create: None,
setup: None,
spam: Some(blob_txs(&self.blob_data, self.recipient.to_owned())),
}
TestConfig::new().with_spam(blob_txs(&self.blob_data, self.recipient.to_owned()))
}
}
2 changes: 1 addition & 1 deletion crates/cli/src/default_scenarios/custom_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ impl NameAndArgs {
impl ToTestConfig for CustomContractArgs {
fn to_testconfig(&self) -> contender_testfile::TestConfig {
TestConfig::new()
.with_create(vec![CreateDefinition::new(&self.contract)])
.with_create(vec![CreateDefinition::new(&self.contract.clone().into())])
.with_setup(self.setup.to_owned())
.with_spam(self.spam.iter().map(SpamRequest::new_tx).collect())
}
Expand Down
75 changes: 37 additions & 38 deletions crates/cli/src/default_scenarios/erc20.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,48 +69,47 @@ impl ToTestConfig for Erc20Args {
.with_args(&[recipient.to_string(), self.fund_amount.to_string()])
})
.collect();
let spam_steps = vec![SpamRequest::new_tx(&{
let mut func_def = FunctionCallDefinition::new(token.template_name())
.with_from_pool("spammers") // Senders from limited pool
.with_signature("transfer(address guy, uint256 wad)")
.with_args(&[
// Use token_recipient if provided (via --recipient flag),
// otherwise this is a placeholder for fuzzing
self.token_recipient
.as_ref()
.map(|addr| addr.to_string())
.unwrap_or_else(|| {
"0x0000000000000000000000000000000000000000".to_string()
}),
self.send_amount.to_string(),
])
.with_gas_limit(55000);

TestConfig {
env: None,
create: Some(vec![CreateDefinition {
contract: token.to_owned(),
// Only add fuzzing if token_recipient is NOT provided
if self.token_recipient.is_none() {
func_def = func_def.with_fuzz(&[FuzzParam {
param: Some("guy".to_string()),
value: None,
min: Some(U256::from(1)),
max: Some(
U256::from_str("0x0000000000ffffffffffffffffffffffffffffffff").unwrap(),
),
}]);
}

func_def
})];

TestConfig::new()
.with_create(vec![CreateDefinition {
contract: token.to_owned().into(),
signature: None,
args: None,
from: None,
from_pool: Some("admin".to_owned()),
}]),
setup: Some(setup_steps),
spam: Some(vec![SpamRequest::new_tx(&{
let mut func_def = FunctionCallDefinition::new(token.template_name())
.with_from_pool("spammers") // Senders from limited pool
.with_signature("transfer(address guy, uint256 wad)")
.with_args(&[
// Use token_recipient if provided (via --recipient flag),
// otherwise this is a placeholder for fuzzing
self.token_recipient
.as_ref()
.map(|addr| addr.to_string())
.unwrap_or_else(|| {
"0x0000000000000000000000000000000000000000".to_string()
}),
self.send_amount.to_string(),
])
.with_gas_limit(55000);

// Only add fuzzing if token_recipient is NOT provided
if self.token_recipient.is_none() {
func_def = func_def.with_fuzz(&[FuzzParam {
param: Some("guy".to_string()),
value: None,
min: Some(U256::from(1)),
max: Some(
U256::from_str("0x0000000000ffffffffffffffffffffffffffffffff").unwrap(),
),
}]);
}

func_def
})]),
}
}])
.with_setup(setup_steps)
.with_spam(spam_steps)
}
}
11 changes: 4 additions & 7 deletions crates/cli/src/default_scenarios/eth_functions/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,17 +73,14 @@ impl ToTestConfig for EthFunctionsArgs {
let opcode_txs = opcode_txs(opcodes, *num_iterations);
let txs = [precompile_txs, opcode_txs].concat();

TestConfig {
env: None,
create: Some(vec![CreateDefinition {
TestConfig::new()
.with_create(vec![CreateDefinition {
contract: contracts::SPAM_ME.into(),
signature: None,
args: None,
from: None,
from_pool: Some("admin".to_owned()),
}]),
setup: None,
spam: Some(txs),
}
}])
.with_spam(txs)
}
}
11 changes: 4 additions & 7 deletions crates/cli/src/default_scenarios/fill_block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,17 +81,14 @@ impl ToTestConfig for FillBlockArgs {
})
.collect::<Vec<_>>();

TestConfig {
env: None,
create: Some(vec![CreateDefinition {
TestConfig::new()
.with_create(vec![CreateDefinition {
contract: contracts::SPAM_ME.into(),
signature: None,
args: None,
from: None,
from_pool: Some("admin".to_owned()),
}]),
setup: None,
spam: Some(spam_txs),
}
}])
.with_spam(spam_txs)
}
}
13 changes: 5 additions & 8 deletions crates/cli/src/default_scenarios/revert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,19 @@ impl Default for RevertCliArgs {

impl ToTestConfig for RevertCliArgs {
fn to_testconfig(&self) -> contender_testfile::TestConfig {
TestConfig {
env: None,
create: Some(vec![CreateDefinition {
TestConfig::new()
.with_create(vec![CreateDefinition {
contract: SPAM_ME_6.into(),
signature: None,
args: None,
from: None,
from_pool: Some("admin".to_owned()),
}]),
setup: None,
spam: Some(vec![SpamRequest::new_tx(
}])
.with_spam(vec![SpamRequest::new_tx(
&FunctionCallDefinition::new(SPAM_ME_6.template_name())
.with_signature("consumeGasAndRevert(uint256 gas)")
.with_args(&[self.gas_use.to_string()])
.with_gas_limit(self.gas_use + 35000),
)]),
}
)])
}
}
11 changes: 4 additions & 7 deletions crates/cli/src/default_scenarios/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,17 +62,14 @@ impl ToTestConfig for StorageStressArgs {
.map(SpamRequest::Tx)
.collect::<Vec<_>>();

TestConfig {
env: None,
create: Some(vec![CreateDefinition {
TestConfig::new()
.with_create(vec![CreateDefinition {
contract: contracts::SPAM_ME.into(),
signature: None,
args: None,
from: None,
from_pool: Some("admin".to_owned()),
}]),
setup: None,
spam: Some(txs),
}
}])
.with_spam(txs)
}
}
11 changes: 4 additions & 7 deletions crates/cli/src/default_scenarios/stress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,17 +206,14 @@ impl ToTestConfig for StressCliArgs {
.flat_map(|config| config.spam.unwrap_or_default())
.collect::<Vec<_>>();

TestConfig {
env: None,
create: Some(vec![CreateDefinition {
TestConfig::new()
.with_create(vec![CreateDefinition {
contract: contracts::SPAM_ME.into(),
signature: None,
args: None,
from: None,
from_pool: Some("admin".to_owned()),
}]),
setup: None,
spam: Some(txs),
}
}])
.with_spam(txs)
}
}
7 changes: 1 addition & 6 deletions crates/cli/src/default_scenarios/transfers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,6 @@ impl ToTestConfig for TransferStressArgs {
.map(Box::new)
.map(SpamRequest::Tx)
.collect::<Vec<_>>();
contender_testfile::TestConfig {
env: None,
create: None,
setup: None,
spam: Some(txs),
}
contender_testfile::TestConfig::new().with_spam(txs)
}
}
Loading
Loading