Skip to content

Commit 3e8d63a

Browse files
scharissisbitwiseguyzeroXbrock
authored
fix: propagate campaign execution errors + fix-campaign-seeds (#425)
* campaign: ensure same seed is used for setup and spam phases * scenario: add from_all_accounts field option to send txs from all pool accounts * campaign: mirror behavior of spam command for --redeploy and --skip-setup flags * chore: cargo fmt * cli: move accounts_per_agent to SendTxsCliArgsInner to support TOML-based setups * fix: propagate campaign execution errors instead of silently ignoring them --------- Co-authored-by: Samuel Stokes <sam.adam.stokes@gmail.com> Co-authored-by: zeroXbrock <2791467+zeroXbrock@users.noreply.github.com>
1 parent 028dd14 commit 3e8d63a

File tree

10 files changed

+184
-72
lines changed

10 files changed

+184
-72
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cli/src/commands/campaign.rs

Lines changed: 42 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,6 @@ pub struct CampaignCliArgs {
5151
)]
5252
pub pending_timeout: u64,
5353

54-
/// The number of accounts to generate for each agent (`from_pool` in scenario files)
55-
#[arg(
56-
short,
57-
long,
58-
visible_aliases = ["na", "accounts"],
59-
default_value_t = 10
60-
)]
61-
pub accounts_per_agent: u64,
62-
6354
/// Max number of txs to send in a single json-rpc batch request.
6455
#[arg(
6556
long = "rpc-batch-size",
@@ -159,37 +150,41 @@ pub async fn run_campaign(
159150
.or_else(|| campaign.spam.seed.map(|s| s.to_string()))
160151
.unwrap_or(load_seedfile()?);
161152

162-
// Setup phase. Skip builtin scenarios since they do their own setup at spam time.
163-
let provider = args.eth_json_rpc_args.new_rpc_provider()?;
153+
// Setup phase: run setup for each (stage, mix) with the same derived seed that spam will use.
154+
// This ensures setup creates accounts matching what spam expects.
155+
// Skip builtin scenarios since they do their own setup at spam time.
164156
if !args.skip_setup {
165-
for scenario_label in campaign.setup_scenarios() {
166-
let scenario = match parse_builtin_reference(&scenario_label) {
167-
Some(builtin) => SpamScenario::Builtin(
168-
builtin
169-
.to_builtin_scenario(
170-
&provider,
171-
&create_spam_cli_args(None, &args, CampaignMode::Tps, 1, 1),
172-
/* TODO: KLUDGE:
173-
- I don't think a `BuiltinScenarioCli` *needs* `rate` or `duration` -- that's for the spammer.
174-
- we should use a different interface for `to_builtin_scenario` (replace `SpamCliArgs`)
175-
*/
176-
)
177-
.await?,
178-
),
179-
None => SpamScenario::Testfile(scenario_label.to_owned()),
180-
};
181-
let mut setup_args = args.eth_json_rpc_args.clone();
182-
setup_args.seed = Some(base_seed.clone());
183-
let setup_cmd = SetupCommandArgs::new(scenario, setup_args)?;
184-
commands::setup(db, setup_cmd).await?;
157+
for stage in &stages {
158+
let stage_seed = bump_seed(&base_seed, &stage.name);
159+
for (mix_idx, mix) in stage.mix.iter().enumerate() {
160+
if mix.rate == 0 {
161+
continue;
162+
}
163+
// Skip builtins - they do their own setup during spam
164+
if parse_builtin_reference(&mix.scenario).is_some() {
165+
continue;
166+
}
167+
168+
let scenario_seed = bump_seed(&stage_seed, &mix_idx.to_string());
169+
let scenario = SpamScenario::Testfile(mix.scenario.clone());
170+
171+
let mut setup_args = args.eth_json_rpc_args.clone();
172+
setup_args.seed = Some(scenario_seed);
173+
// Ensure accounts_per_agent uses campaign default (10) if not explicitly set
174+
if setup_args.accounts_per_agent.is_none() {
175+
setup_args.accounts_per_agent = Some(10);
176+
}
177+
let setup_cmd = SetupCommandArgs::new(scenario, setup_args)?;
178+
commands::setup(db, setup_cmd).await?;
179+
}
185180
}
186181
}
187182

188183
let mut run_ids = vec![];
189184

190185
loop {
191186
tokio::select! {
192-
_ = async {
187+
result = async {
193188
for (stage_idx, stage) in stages.iter().enumerate() {
194189
info!(
195190
campaign_id = %campaign_id,
@@ -240,6 +235,8 @@ pub async fn run_campaign(
240235
}
241236
Ok::<_, CliError>(())
242237
} => {
238+
// Propagate any error from the campaign execution
239+
result?;
243240
if args.run_forever {
244241
info!("Campaign {campaign_id} completed. Running again due to --forever flag.");
245242
continue;
@@ -316,6 +313,8 @@ fn create_spam_cli_args(
316313
spam_mode: CampaignMode,
317314
spam_rate: u64,
318315
spam_duration: u64,
316+
skip_setup: bool,
317+
redeploy: bool,
319318
) -> SpamCliArgs {
320319
SpamCliArgs {
321320
eth_json_rpc_args: ScenarioSendTxsCliArgs {
@@ -337,13 +336,12 @@ fn create_spam_cli_args(
337336
duration: spam_duration,
338337
pending_timeout: args.pending_timeout,
339338
run_forever: false,
340-
accounts_per_agent: args.accounts_per_agent,
341339
},
342340
ignore_receipts: args.ignore_receipts,
343341
optimistic_nonces: args.optimistic_nonces,
344342
gen_report: false,
345-
redeploy: args.redeploy,
346-
skip_setup: true,
343+
redeploy,
344+
skip_setup,
347345
rpc_batch_size: args.rpc_batch_size,
348346
spam_timeout: args.spam_timeout,
349347
}
@@ -383,12 +381,21 @@ async fn execute_stage(
383381
args.eth_json_rpc_args.seed = Some(scenario_seed.clone());
384382
debug!("mix {mix_idx} seed: {}", scenario_seed);
385383

384+
// Check if this is a builtin scenario to determine skip_setup/redeploy behavior:
385+
// - Builtins: respect campaign's flags (they do their own setup during spam)
386+
// - Toml scenarios: always skip setup (ran in Phase 1), redeploy not applicable
387+
let is_builtin = parse_builtin_reference(&mix.scenario).is_some();
388+
let skip_setup = if is_builtin { args.skip_setup } else { true };
389+
let redeploy = if is_builtin { args.redeploy } else { false };
390+
386391
let spam_cli_args = create_spam_cli_args(
387392
Some(mix.scenario.clone()),
388393
&args,
389394
campaign.spam.mode,
390395
mix.rate,
391396
stage.duration,
397+
skip_setup,
398+
redeploy,
392399
);
393400

394401
let spam_scenario = if let Some(builtin_cli) = parse_builtin_reference(&mix.scenario) {

crates/cli/src/commands/common.rs

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,23 @@ Requires --auth-rpc-url and --jwt-secret to be set.",
128128
value_parser = parse_value,
129129
)]
130130
pub gas_price: Option<U256>,
131+
132+
/// The number of accounts to generate for each agent (`from_pool` in scenario files).
133+
/// Defaults to 1 for standalone setup, 10 for spam and campaign.
134+
#[arg(
135+
short = 'a',
136+
long,
137+
visible_aliases = ["na", "accounts"],
138+
)]
139+
pub accounts_per_agent: Option<u64>,
131140
}
132141

133142
impl SendTxsCliArgsInner {
143+
/// Returns the accounts_per_agent value, or the provided default if not set.
144+
pub fn accounts_per_agent_or(&self, default: u64) -> u64 {
145+
self.accounts_per_agent.unwrap_or(default)
146+
}
147+
134148
pub fn new_rpc_provider(&self) -> Result<DynProvider<AnyNetwork>, ArgsError> {
135149
info!("connecting to {}", self.rpc_url);
136150
Ok(DynProvider::new(
@@ -301,15 +315,6 @@ Requires --priv-key to be set for each 'from' address in the given testfile.",
301315
visible_aliases = ["indefinite", "indefinitely", "infinite"]
302316
)]
303317
pub run_forever: bool,
304-
305-
/// The number of accounts to generate for each agent (`from_pool` in scenario files)
306-
#[arg(
307-
short,
308-
long,
309-
visible_aliases = ["na", "accounts"],
310-
default_value_t = 10
311-
)]
312-
pub accounts_per_agent: u64,
313318
}
314319

315320
#[derive(Copy, Debug, Clone, clap::ValueEnum)]

crates/cli/src/commands/setup.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,10 @@ pub async fn setup(
3636
env,
3737
bundle_type,
3838
override_senders,
39+
accounts_per_agent,
3940
..
4041
} = args.eth_json_rpc_args.clone();
42+
let accounts_per_agent = accounts_per_agent.unwrap_or(1) as usize;
4143
let engine_params = args.engine_params().await?;
4244
let rpc_client = args.eth_json_rpc_args.new_rpc_provider()?;
4345
let user_signers_with_defaults = args.eth_json_rpc_args.user_signers_with_defaults();
@@ -86,7 +88,7 @@ pub async fn setup(
8688
continue;
8789
}
8890

89-
let agent = SignerStore::new(1, &args.seed, from_pool);
91+
let agent = SignerStore::new(accounts_per_agent, &args.seed, from_pool);
9092
agents.add_agent(from_pool, agent);
9193
}
9294
}

crates/cli/src/commands/spam.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,16 +219,17 @@ impl SpamCommandArgs {
219219
duration,
220220
pending_timeout,
221221
run_forever,
222-
accounts_per_agent,
223222
} = self.spam_args.spam_args.clone();
224223
let SendTxsCliArgsInner {
225224
min_balance,
226225
tx_type,
227226
bundle_type,
228227
env,
229228
override_senders,
229+
accounts_per_agent,
230230
..
231231
} = self.spam_args.eth_json_rpc_args.rpc_args.clone();
232+
let accounts_per_agent = accounts_per_agent.unwrap_or(10);
232233

233234
let mut testconfig = self.testconfig().await?;
234235
let spam_len = testconfig.spam.as_ref().map(|s| s.len()).unwrap_or(0);

crates/cli/src/default_scenarios/builtin.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,10 @@ impl BuiltinScenarioCli {
9898
let mut agents = AgentStore::new();
9999
agents.init(
100100
&["spammers"],
101-
spam_args.spam_args.accounts_per_agent as usize,
101+
spam_args
102+
.eth_json_rpc_args
103+
.rpc_args
104+
.accounts_per_agent_or(10) as usize,
102105
&seed,
103106
);
104107
let spammers = agents

crates/core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ thiserror = { workspace = true }
3232

3333
[dev-dependencies]
3434
contender_testfile = { workspace = true }
35+
toml = { workspace = true }
3536

3637
[package.metadata.cargo-udeps.ignore]
3738
development = ["contender_testfile"]

crates/core/src/generator/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ pub enum GeneratorError {
3737
#[error("fuzz invalid")]
3838
FuzzInvalid,
3939

40+
#[error("for_all_accounts requires from_pool to be set")]
41+
ForAllAccountsRequiresFromPool,
42+
4043
#[error("must specify from or from_pool in scenario config")]
4144
InvalidSender,
4245

crates/core/src/generator/function_def.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ pub struct FunctionCallDefinition {
3232
pub blob_data: Option<String>,
3333
/// Optional setCode data; tx type must be set to EIP7702 by spammer
3434
pub authorization_address: Option<String>,
35+
/// If true and `from_pool` is set, run this setup transaction for all accounts in the pool.
36+
/// Defaults to false (only runs for the first account).
37+
#[serde(default)]
38+
pub for_all_accounts: bool,
3539
}
3640

3741
/// User-facing definition of a function call to be executed.
@@ -55,6 +59,7 @@ impl FunctionCallDefinition {
5559
gas_limit: None,
5660
blob_data: None,
5761
authorization_address: None,
62+
for_all_accounts: false,
5863
}
5964
}
6065

@@ -103,6 +108,10 @@ impl FunctionCallDefinition {
103108
self.authorization_address = Some(auth_addr.as_ref().to_owned());
104109
self
105110
}
111+
pub fn with_for_all_accounts(mut self, for_all_accounts: bool) -> Self {
112+
self.for_all_accounts = for_all_accounts;
113+
self
114+
}
106115

107116
pub fn sidecar_data(&self) -> Result<Option<BlobTransactionSidecar>, GeneratorError> {
108117
let sidecar_data = if let Some(data) = self.blob_data.as_ref() {
@@ -150,3 +159,56 @@ pub struct FuzzParam {
150159
/// Maximum value fuzzer will use.
151160
pub max: Option<U256>,
152161
}
162+
163+
#[cfg(test)]
164+
mod tests {
165+
use super::*;
166+
167+
#[test]
168+
fn for_all_accounts_defaults_to_false() {
169+
let toml = r#"
170+
to = "0x1234567890123456789012345678901234567890"
171+
from_pool = "test_pool"
172+
signature = "test()"
173+
"#;
174+
let def: FunctionCallDefinition = toml::from_str(toml).unwrap();
175+
assert!(!def.for_all_accounts);
176+
}
177+
178+
#[test]
179+
fn for_all_accounts_parses_true() {
180+
let toml = r#"
181+
to = "0x1234567890123456789012345678901234567890"
182+
from_pool = "test_pool"
183+
signature = "test()"
184+
for_all_accounts = true
185+
"#;
186+
let def: FunctionCallDefinition = toml::from_str(toml).unwrap();
187+
assert!(def.for_all_accounts);
188+
}
189+
190+
#[test]
191+
fn for_all_accounts_parses_false() {
192+
let toml = r#"
193+
to = "0x1234567890123456789012345678901234567890"
194+
from_pool = "test_pool"
195+
signature = "test()"
196+
for_all_accounts = false
197+
"#;
198+
let def: FunctionCallDefinition = toml::from_str(toml).unwrap();
199+
assert!(!def.for_all_accounts);
200+
}
201+
202+
#[test]
203+
fn with_for_all_accounts_builder() {
204+
let def = FunctionCallDefinition::new("0x1234")
205+
.with_from_pool("test_pool")
206+
.with_for_all_accounts(true);
207+
assert!(def.for_all_accounts);
208+
209+
let def = FunctionCallDefinition::new("0x1234")
210+
.with_from_pool("test_pool")
211+
.with_for_all_accounts(false);
212+
assert!(!def.for_all_accounts);
213+
}
214+
}

0 commit comments

Comments
 (0)