Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions crates/spammer/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -565,4 +565,25 @@ mod tests {

assert_eq!(reparsed, original);
}

#[test]
fn cli_rejects_overflowing_transaction_mix_total() {
use clap::Parser;

#[derive(Parser, Debug)]
struct TestCli {
#[command(flatten)]
args: SpammerArgs,
}

let result = TestCli::try_parse_from([
"spammer",
"--mix",
"transfer=4294967295,legacy=1",
]);
assert!(
result.is_err(),
"CLI accepted transaction mix weights whose total overflows u32"
);
}
}
70 changes: 68 additions & 2 deletions crates/spammer/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,8 +251,21 @@ impl TxTypeMix {
]
}

/// Sum of mix weights, or `None` if the aggregate does not fit in `u32`.
pub fn checked_total_weight(&self) -> Option<u32> {
self.transfer
.checked_add(self.legacy)?
.checked_add(self.erc20)?
.checked_add(self.guzzler)
}

/// Sum of mix weights. Callers must validate with [`Self::checked_total_weight`]
/// (or [`Config::validate`]) first — overflowing mixes are rejected at the
/// `--mix` / config boundary rather than here.
pub fn total_weight(&self) -> u32 {
self.transfer + self.legacy + self.erc20 + self.guzzler
self.checked_total_weight().expect(
"TxTypeMix total weight overflows u32; reject overflowing --mix before use",
)
}
}

Expand Down Expand Up @@ -287,6 +300,12 @@ impl FromStr for TxTypeMix {
}
}

if out.checked_total_weight().is_none() {
return Err(
"--mix total weight overflows u32; reduce the sum of type weights".to_string(),
);
}

Ok(out)
}
}
Expand Down Expand Up @@ -371,7 +390,10 @@ impl Config {
if let Err(msg) = self.guzzler_fn_weights.validate_enabled_args() {
eyre::bail!("{msg}");
}
if self.tx_type_mix.total_weight() == 0 {
let Some(mix_total) = self.tx_type_mix.checked_total_weight() else {
eyre::bail!("--mix total weight overflows u32; reduce the sum of type weights");
};
if mix_total == 0 {
eyre::bail!("--mix total weight is 0; at least one tx type must have weight > 0");
}
if self.tx_type_mix.guzzler > 0 && self.guzzler_fn_weights.total_weight() == 0 {
Expand Down Expand Up @@ -501,6 +523,50 @@ mod tests {
assert!(err.contains("Invalid weight"));
}

#[test]
fn tx_type_mix_rejects_overflowing_total() {
let result = TxTypeMix::from_str("transfer=4294967295,legacy=1");
assert!(
result.is_err(),
"--mix parser accepted weights whose total cannot fit in u32"
);
let err = result.unwrap_err();
assert!(err.contains("--mix"));
assert!(err.contains("overflows"));
}

#[test]
fn tx_type_mix_accepts_total_equal_to_u32_max() {
let mix = TxTypeMix::from_str("transfer=4294967295").expect("u32::MAX alone is valid");
assert_eq!(mix.total_weight(), u32::MAX);
}

#[test]
fn config_validation_does_not_panic_on_overflowing_mix_total() {
let config = Config {
tx_type_mix: TxTypeMix {
transfer: u32::MAX,
legacy: 1,
..Default::default()
},
..default_config()
};

let result = std::panic::catch_unwind(|| config.validate());
assert!(
result.is_ok(),
"overflowing --mix total caused a panic"
);
let validation = result.unwrap();
assert!(
validation.is_err(),
"overflowing --mix total was accepted"
);
let msg = format!("{:#}", validation.unwrap_err());
assert!(msg.contains("--mix"));
assert!(msg.contains("overflows"));
}

#[test]
fn config_rejects_guzzler_mix_without_fn_weights() {
let config = Config {
Expand Down