Skip to content
Merged
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

77 changes: 42 additions & 35 deletions crates/cli/src/commands/campaign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,6 @@ pub struct CampaignCliArgs {
)]
pub pending_timeout: u64,

/// The number of accounts to generate for each agent (`from_pool` in scenario files)
#[arg(
short,
long,
visible_aliases = ["na", "accounts"],
default_value_t = 10
)]
pub accounts_per_agent: u64,

/// Max number of txs to send in a single json-rpc batch request.
#[arg(
long = "rpc-batch-size",
Expand Down Expand Up @@ -159,37 +150,41 @@ pub async fn run_campaign(
.or_else(|| campaign.spam.seed.map(|s| s.to_string()))
.unwrap_or(load_seedfile()?);

// Setup phase. Skip builtin scenarios since they do their own setup at spam time.
let provider = args.eth_json_rpc_args.new_rpc_provider()?;
// Setup phase: run setup for each (stage, mix) with the same derived seed that spam will use.
// This ensures setup creates accounts matching what spam expects.
// Skip builtin scenarios since they do their own setup at spam time.
if !args.skip_setup {
for scenario_label in campaign.setup_scenarios() {
let scenario = match parse_builtin_reference(&scenario_label) {
Some(builtin) => SpamScenario::Builtin(
builtin
.to_builtin_scenario(
&provider,
&create_spam_cli_args(None, &args, CampaignMode::Tps, 1, 1),
/* TODO: KLUDGE:
- I don't think a `BuiltinScenarioCli` *needs* `rate` or `duration` -- that's for the spammer.
- we should use a different interface for `to_builtin_scenario` (replace `SpamCliArgs`)
*/
)
.await?,
),
None => SpamScenario::Testfile(scenario_label.to_owned()),
};
let mut setup_args = args.eth_json_rpc_args.clone();
setup_args.seed = Some(base_seed.clone());
let setup_cmd = SetupCommandArgs::new(scenario, setup_args)?;
commands::setup(db, setup_cmd).await?;
for stage in &stages {
let stage_seed = bump_seed(&base_seed, &stage.name);
for (mix_idx, mix) in stage.mix.iter().enumerate() {
if mix.rate == 0 {
continue;
}
// Skip builtins - they do their own setup during spam
if parse_builtin_reference(&mix.scenario).is_some() {
continue;
}

let scenario_seed = bump_seed(&stage_seed, &mix_idx.to_string());
let scenario = SpamScenario::Testfile(mix.scenario.clone());

let mut setup_args = args.eth_json_rpc_args.clone();
setup_args.seed = Some(scenario_seed);
// Ensure accounts_per_agent uses campaign default (10) if not explicitly set
if setup_args.accounts_per_agent.is_none() {
setup_args.accounts_per_agent = Some(10);
}
let setup_cmd = SetupCommandArgs::new(scenario, setup_args)?;
commands::setup(db, setup_cmd).await?;
}
}
}

let mut run_ids = vec![];

loop {
tokio::select! {
_ = async {
result = async {
for (stage_idx, stage) in stages.iter().enumerate() {
info!(
campaign_id = %campaign_id,
Expand Down Expand Up @@ -240,6 +235,8 @@ pub async fn run_campaign(
}
Ok::<_, CliError>(())
} => {
// Propagate any error from the campaign execution
result?;
if args.run_forever {
info!("Campaign {campaign_id} completed. Running again due to --forever flag.");
continue;
Expand Down Expand Up @@ -316,6 +313,8 @@ fn create_spam_cli_args(
spam_mode: CampaignMode,
spam_rate: u64,
spam_duration: u64,
skip_setup: bool,
redeploy: bool,
) -> SpamCliArgs {
SpamCliArgs {
eth_json_rpc_args: ScenarioSendTxsCliArgs {
Expand All @@ -337,13 +336,12 @@ fn create_spam_cli_args(
duration: spam_duration,
pending_timeout: args.pending_timeout,
run_forever: false,
accounts_per_agent: args.accounts_per_agent,
},
ignore_receipts: args.ignore_receipts,
optimistic_nonces: args.optimistic_nonces,
gen_report: false,
redeploy: args.redeploy,
skip_setup: true,
redeploy,
skip_setup,
rpc_batch_size: args.rpc_batch_size,
spam_timeout: args.spam_timeout,
}
Expand Down Expand Up @@ -383,12 +381,21 @@ async fn execute_stage(
args.eth_json_rpc_args.seed = Some(scenario_seed.clone());
debug!("mix {mix_idx} seed: {}", scenario_seed);

// Check if this is a builtin scenario to determine skip_setup/redeploy behavior:
// - Builtins: respect campaign's flags (they do their own setup during spam)
// - Toml scenarios: always skip setup (ran in Phase 1), redeploy not applicable
let is_builtin = parse_builtin_reference(&mix.scenario).is_some();
let skip_setup = if is_builtin { args.skip_setup } else { true };
let redeploy = if is_builtin { args.redeploy } else { false };

let spam_cli_args = create_spam_cli_args(
Some(mix.scenario.clone()),
&args,
campaign.spam.mode,
mix.rate,
stage.duration,
skip_setup,
redeploy,
);

let spam_scenario = if let Some(builtin_cli) = parse_builtin_reference(&mix.scenario) {
Expand Down
23 changes: 14 additions & 9 deletions crates/cli/src/commands/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,23 @@ Requires --auth-rpc-url and --jwt-secret to be set.",
value_parser = parse_value,
)]
pub gas_price: Option<U256>,

/// The number of accounts to generate for each agent (`from_pool` in scenario files).
/// Defaults to 1 for standalone setup, 10 for spam and campaign.
#[arg(
short = 'a',
long,
visible_aliases = ["na", "accounts"],
)]
pub accounts_per_agent: Option<u64>,
}

impl SendTxsCliArgsInner {
/// Returns the accounts_per_agent value, or the provided default if not set.
pub fn accounts_per_agent_or(&self, default: u64) -> u64 {
self.accounts_per_agent.unwrap_or(default)
}

pub fn new_rpc_provider(&self) -> Result<DynProvider<AnyNetwork>, ArgsError> {
info!("connecting to {}", self.rpc_url);
Ok(DynProvider::new(
Expand Down Expand Up @@ -301,15 +315,6 @@ Requires --priv-key to be set for each 'from' address in the given testfile.",
visible_aliases = ["indefinite", "indefinitely", "infinite"]
)]
pub run_forever: bool,

/// The number of accounts to generate for each agent (`from_pool` in scenario files)
#[arg(
short,
long,
visible_aliases = ["na", "accounts"],
default_value_t = 10
)]
pub accounts_per_agent: u64,
}

#[derive(Copy, Debug, Clone, clap::ValueEnum)]
Expand Down
4 changes: 3 additions & 1 deletion crates/cli/src/commands/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ pub async fn setup(
env,
bundle_type,
override_senders,
accounts_per_agent,
..
} = args.eth_json_rpc_args.clone();
let accounts_per_agent = accounts_per_agent.unwrap_or(1) as usize;
let engine_params = args.engine_params().await?;
let rpc_client = args.eth_json_rpc_args.new_rpc_provider()?;
let user_signers_with_defaults = args.eth_json_rpc_args.user_signers_with_defaults();
Expand Down Expand Up @@ -86,7 +88,7 @@ pub async fn setup(
continue;
}

let agent = SignerStore::new(1, &args.seed, from_pool);
let agent = SignerStore::new(accounts_per_agent, &args.seed, from_pool);
agents.add_agent(from_pool, agent);
}
}
Expand Down
3 changes: 2 additions & 1 deletion crates/cli/src/commands/spam.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,16 +219,17 @@ impl SpamCommandArgs {
duration,
pending_timeout,
run_forever,
accounts_per_agent,
} = self.spam_args.spam_args.clone();
let SendTxsCliArgsInner {
min_balance,
tx_type,
bundle_type,
env,
override_senders,
accounts_per_agent,
..
} = self.spam_args.eth_json_rpc_args.rpc_args.clone();
let accounts_per_agent = accounts_per_agent.unwrap_or(10);

let mut testconfig = self.testconfig().await?;
let spam_len = testconfig.spam.as_ref().map(|s| s.len()).unwrap_or(0);
Expand Down
5 changes: 4 additions & 1 deletion crates/cli/src/default_scenarios/builtin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,10 @@ impl BuiltinScenarioCli {
let mut agents = AgentStore::new();
agents.init(
&["spammers"],
spam_args.spam_args.accounts_per_agent as usize,
spam_args
.eth_json_rpc_args
.rpc_args
.accounts_per_agent_or(10) as usize,
&seed,
);
let spammers = agents
Expand Down
1 change: 1 addition & 0 deletions crates/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ thiserror = { workspace = true }

[dev-dependencies]
contender_testfile = { workspace = true }
toml = { workspace = true }

[package.metadata.cargo-udeps.ignore]
development = ["contender_testfile"]
3 changes: 3 additions & 0 deletions crates/core/src/generator/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ pub enum GeneratorError {
#[error("fuzz invalid")]
FuzzInvalid,

#[error("for_all_accounts requires from_pool to be set")]
ForAllAccountsRequiresFromPool,

#[error("must specify from or from_pool in scenario config")]
InvalidSender,

Expand Down
62 changes: 62 additions & 0 deletions crates/core/src/generator/function_def.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ pub struct FunctionCallDefinition {
pub blob_data: Option<String>,
/// Optional setCode data; tx type must be set to EIP7702 by spammer
pub authorization_address: Option<String>,
/// If true and `from_pool` is set, run this setup transaction for all accounts in the pool.
/// Defaults to false (only runs for the first account).
#[serde(default)]
pub for_all_accounts: bool,
}

/// User-facing definition of a function call to be executed.
Expand All @@ -55,6 +59,7 @@ impl FunctionCallDefinition {
gas_limit: None,
blob_data: None,
authorization_address: None,
for_all_accounts: false,
}
}

Expand Down Expand Up @@ -103,6 +108,10 @@ impl FunctionCallDefinition {
self.authorization_address = Some(auth_addr.as_ref().to_owned());
self
}
pub fn with_for_all_accounts(mut self, for_all_accounts: bool) -> Self {
self.for_all_accounts = for_all_accounts;
self
}

pub fn sidecar_data(&self) -> Result<Option<BlobTransactionSidecar>, GeneratorError> {
let sidecar_data = if let Some(data) = self.blob_data.as_ref() {
Expand Down Expand Up @@ -150,3 +159,56 @@ pub struct FuzzParam {
/// Maximum value fuzzer will use.
pub max: Option<U256>,
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn for_all_accounts_defaults_to_false() {
let toml = r#"
to = "0x1234567890123456789012345678901234567890"
from_pool = "test_pool"
signature = "test()"
"#;
let def: FunctionCallDefinition = toml::from_str(toml).unwrap();
assert!(!def.for_all_accounts);
}

#[test]
fn for_all_accounts_parses_true() {
let toml = r#"
to = "0x1234567890123456789012345678901234567890"
from_pool = "test_pool"
signature = "test()"
for_all_accounts = true
"#;
let def: FunctionCallDefinition = toml::from_str(toml).unwrap();
assert!(def.for_all_accounts);
}

#[test]
fn for_all_accounts_parses_false() {
let toml = r#"
to = "0x1234567890123456789012345678901234567890"
from_pool = "test_pool"
signature = "test()"
for_all_accounts = false
"#;
let def: FunctionCallDefinition = toml::from_str(toml).unwrap();
assert!(!def.for_all_accounts);
}

#[test]
fn with_for_all_accounts_builder() {
let def = FunctionCallDefinition::new("0x1234")
.with_from_pool("test_pool")
.with_for_all_accounts(true);
assert!(def.for_all_accounts);

let def = FunctionCallDefinition::new("0x1234")
.with_from_pool("test_pool")
.with_for_all_accounts(false);
assert!(!def.for_all_accounts);
}
}
Loading