Campaigns (Composite / Meta-Scenarios)
March 27, 2026 ยท View on GitHub
Campaigns let you run multiple existing scenarios in parallel, optionally in sequential stages, to mimic real network mixes or replay traffic profiles.
File format
name = "composite-example"
description = "Simple, Stress, and Reverts traffic mix"
[setup]
scenarios = [
"scenario:simple.toml",
"scenario:stress.toml",
"scenario:reverts.toml",
]
[spam]
mode = "tps" # or "tpb"
rate = 20 # default rate if a stage omits one (TPS or TPB via mode)
duration = 600 # default duration (seconds if tps, blocks if tpb)
seed = 42 # optional; falls back to CLI --seed or seed file
[[spam.stage]]
name = "steady"
duration_secs = 600
[[spam.stage.mix]]
scenario = "scenario:simple.toml"
share_pct = 95.0
[[spam.stage.mix]]
scenario = "scenario:stress.toml"
share_pct = 4.8
[[spam.stage.mix]]
scenario = "scenario:reverts.toml"
share_pct = 0.2
mode:tps(per-second) ortpb(per-block). Stages can override rate/duration; otherwise they inherit from[spam].rate: rate per mode (TPS ifmode="tps", TPB ifmode="tpb"). Set once at[spam]or per stage.durationat[spam]is a default per-stage duration, not a total campaign time. Each stage runs for its own duration (seconds iftps, blocks iftpb), then the next stage starts.share_pct: scenario weight inside a stage; shares are normalized and rounded, and the last entry absorbs rounding drift to preserve the target rate.[setup].scenarios: run once, in order, before spamming. Uses the standardsetuplogic for each referenced scenario file.
Stage basics
- Stages run sequentially. Each stage inherits
mode/rate/durationfrom[spam]unless the stage overrides them. - Each stage performs its own setup/init (funding, deploy/config for builtins, scenario init), then starts its spammers at the resolved rate/mix.
- Within a stage, we spin up one spammer per
mixentry at the computed per-scenario rate; they share a DB handle and run id. - The next stage starts only after the previous one completes its stage duration (seconds for
tps, blocks fortpb). Campaign duration is the sum of stage durations. - Rates and shares are recomputed per stage, so you can ramp traffic up/down or change blends across time slices.
Validation
- You must provide either
[[spam.stage]]entries or a shorthand[spam]+[[spam.mix]]withspam.duration. - If
spam.stageis present,spam.mixis rejected (prefer explicit stages). - Each stage needs a duration (seconds for
tps, blocks fortpb); if omitted, the[spam].durationdefault is used. - Mix entries must be non-empty and share percentages must sum to a positive number (they are normalized automatically).
Shorthand single-stage form
If you omit [[spam.stage]] and instead set spam.duration plus [[spam.mix]], Contender builds a single implicit stage named steady:
[spam]
mode = "tps"
rate = 20
duration = 600
seed = 42
[[spam.mix]]
scenario = "scenario:simple.toml"
share_pct = 95.0
[[spam.mix]]
scenario = "scenario:stress.toml"
share_pct = 4.8
[[spam.mix]]
scenario = "scenario:reverts.toml"
share_pct = 0.2
This is equivalent to writing a single explicit [[spam.stage]] named steady with the same rate/duration and mix.
Multi-stage example
See campaigns/staged-example.toml for a two-stage campaign that warms up at a lower TPS, then ramps to a steady-state mix.
CLI usage
Preferred: new subcommand.
contender campaign ./campaigns/composite.toml \
-r $RPC_URL -p $PKEY --pending-timeout 12 --rpc-batch-size 0
Flags mirror spam where they make sense:
- Connection/auth:
--rpc-url,--priv-key/-p,--builder-url, JWT/auth flags viaScenarioSendTxsoptions. - Funding/runtime:
--pending-timeout,--accounts-per-agent,--rpc-batch-size,--ignore-receipts,--optimistic-nonces,--timeout,--report. - Setup controls:
--skip-setup
Reporting
- Per-run:
contender report [-i <last_run_id> --preceding-runs N] - Campaign summary:
contender report --campaign [<campaign_id>](alias:--campaign-id)- If
<campaign_id>is omitted, the latest campaign is used. - Generates per-run HTML for all runs in the campaign.
- Writes
campaign-<campaign_id>.htmlandcampaign-<campaign_id>.jsonunder~/.local/state/contender/reports/with links, aggregate metrics, and per-stage/per-scenario rollups. - If you pass
--reporttocontender campaign ..., contender will also generate a report for the run-id range at the end of the campaign. - If transaction logs are incomplete for any run (e.g., tracing/storage gaps), the campaign report will use stored run metadata for totals/durations and will display a notice; error counts may be under-reported in that case.
- If
- When a stage has multiple
[[spam.stage.mix]]entries, do not combine it with--override-senders; using a single sender across mixes is rejected because it would cause nonce conflicts.
Execution semantics
- Setup: load each scenario in
[setup].scenariosand run its setup once (reuse existing setup command). - Stages: for each
[[spam.stage]]- Resolve stage mode/rate/duration from stage or
[spam]defaults. - Compute per-scenario rates:
scenario_rate = round(total_rate * share_pct/100), last entry fixed to hit the total. - Spawn one spammer per scenario in the stage, sharing a common
run_idand database handle. - Stage ends after
durationseconds/blocks.
- Resolve stage mode/rate/duration from stage or
- Reporting: if
--reportis set, generate a report for all campaign runs after the final stage.
Run metadata now records campaign_name and stage_name alongside the scenario label (campaign:<name>::<stage>), so reports and DB exports can distinguish composite runs.