Summary
The spammer accepts --rate values above u32::MAX, even though its governor-based rate limiter can represent rates only as NonZeroU32.
The oversized value passes CLI parsing and Config::validate(), then panics when RateLimiter::new() converts it from u64 to u32 with expect().
The unsupported value should be rejected at the configuration boundary with a normal, descriptive error.
Affected files
crates/spammer/src/cli.rs
crates/spammer/src/config.rs
crates/spammer/src/rate_limiter.rs
crates/spammer/src/spammer.rs
Observed behavior
The CLI exposes the rate as u64 and enforces only a lower bound:
#[clap(
short = 'r',
long,
default_value_t = defaults::RATE,
global = true,
value_parser = clap::value_parser!(u64).range(1..)
)]
pub rate: u64,
SpammerArgs::to_config() copies that value directly into Config::max_rate, which is also u64.
Config::validate() does not check the rate limiter's upper bound, so a value of:
passes validation.
Later, Spammer::new() constructs the shared rate limiter with config.max_rate. RateLimiter::new() performs a fallible narrowing conversion and unwraps it:
let tps_u32 = u32::try_from(tps).expect("TPS must fit in u32");
The process then panics with:
TPS must fit in u32: TryFromIntError(())
Expected behavior
--rate should accept positive values through 1_000_000_000, the highest distinct per-second rate governor can represent at nanosecond resolution.
- Values greater than
1_000_000_000 should be rejected before the spammer begins initialization.
- The error should identify
--rate and state the supported upper bound.
- Invalid user input should produce a normal nonzero CLI error, not a panic.
- Internal configuration validation should enforce the same invariant so non-CLI callers cannot reach the panic path with an unsupported rate.
Reproduction
I added three focused regression tests against current main.
CLI parsing
#[test]
fn cli_rejects_rate_above_u32_max() {
let rate = (u64::from(u32::MAX) + 1).to_string();
let result = Cli::try_parse_from(["spammer", "--rate", rate.as_str(), "ws"]);
assert!(
result.is_err(),
"--rate accepted a value the rate limiter cannot represent"
);
}
Result:
test tests::cli_rejects_rate_above_u32_max ... FAILED
--rate accepted a value the rate limiter cannot represent
This confirms that the command-line parser accepts the unsupported value.
Configuration validation
#[test]
fn config_rejects_rate_above_u32_max() {
let config = Config {
max_rate: u64::from(u32::MAX) + 1,
..default_config()
};
assert!(
config.validate().is_err(),
"rate limiter only supports rates through u32::MAX"
);
}
Result:
test config::tests::config_rejects_rate_above_u32_max ... FAILED
rate limiter only supports rates through u32::MAX
This confirms that the oversized rate passes the current validation layer.
Rate limiter panic
#[test]
fn new_does_not_panic_for_cli_accepted_rate() {
let result = std::panic::catch_unwind(|| {
RateLimiter::new(u64::from(u32::MAX) + 1, 1, 1)
});
assert!(result.is_ok(), "CLI-accepted rate panicked in RateLimiter");
}
Result:
thread 'rate_limiter::tests::new_does_not_panic_for_cli_accepted_rate' panicked at crates/spammer/src/rate_limiter.rs:36:42:
TPS must fit in u32: TryFromIntError(())
test rate_limiter::tests::new_does_not_panic_for_cli_accepted_rate ... FAILED
All three focused tests exited with code 101 on the current implementation.
Tested against commit:
97f8da0dc4faa703fe2d68ca007e40dab2c8a9ef
Root cause
The accepted configuration type and the rate limiter's supported type disagree:
- CLI
rate: u64
Config::max_rate: u64
RateLimiter quota: NonZeroU32
No validation bridges that type boundary. The first enforced upper bound is therefore the panicking u32::try_from(...).expect(...) inside RateLimiter::new().
Supported upper bound
Implementation review found that the effective upper bound is stricter than the storage type alone suggests. governor 0.8.1 computes Quota::per_second() as 1_000_000_000ns / rate. Rates above 1_000_000_000 produce a zero-nanosecond interval, which its GCRA implementation clamps to one nanosecond. Such values fit in u32 but cannot be represented as distinct requested rates.
The correct maximum for this path is therefore 1_000_000_000, not u32::MAX. The original panic reproducer above remains valid, while the fix should enforce the actual rate-limiter resolution boundary.
Why this matters
The failure occurs after configuration parsing and validation have reported success. Depending on the execution path, the spammer can perform other setup work before reaching the rate limiter construction.
A panic is also harder for scripts and orchestrators to classify than a normal invalid-argument error, and it incorrectly presents user-controlled input as an internal invariant violation.
Suggested fix
Enforce the rate limiter's supported range at the configuration boundary.
At minimum:
- reject
Config::max_rate > 1_000_000_000 in Config::validate();
- return an error that identifies
--rate and the maximum supported value;
- add the equivalent upper bound to the Clap value parser when practical, so direct CLI users receive immediate argument validation;
- keep the rate limiter conversion as a validated invariant, or make its constructor fallible if it can be called with unvalidated configuration.
The CLI and internal configuration paths should share the same effective bound.
Potential regression tests
Add boundary tests verifying that:
--rate 1 remains accepted;
- a rate equal to
1_000_000_000 is accepted;
- a rate equal to
1_000_000_001 is rejected by CLI parsing;
- the same oversized value is rejected by
Config::validate();
- an oversized value cannot reach
RateLimiter::new() and panic;
- the error message identifies
--rate and the supported maximum.
Scope
This is a configuration-boundary correctness fix. It should not change rate limiting behavior for currently supported values, transaction generation, WebSocket behavior, or consensus/protocol behavior.
Duplicate check
I searched open and closed issues and pull requests using combinations of:
spammer rate
rate limiter
u32::MAX
TPS must fit in u32
TryFromIntError
--rate panic
No direct duplicate or existing implementation was found.
Summary
The spammer accepts
--ratevalues aboveu32::MAX, even though itsgovernor-based rate limiter can represent rates only asNonZeroU32.The oversized value passes CLI parsing and
Config::validate(), then panics whenRateLimiter::new()converts it fromu64tou32withexpect().The unsupported value should be rejected at the configuration boundary with a normal, descriptive error.
Affected files
crates/spammer/src/cli.rscrates/spammer/src/config.rscrates/spammer/src/rate_limiter.rscrates/spammer/src/spammer.rsObserved behavior
The CLI exposes the rate as
u64and enforces only a lower bound:SpammerArgs::to_config()copies that value directly intoConfig::max_rate, which is alsou64.Config::validate()does not check the rate limiter's upper bound, so a value of:passes validation.
Later,
Spammer::new()constructs the shared rate limiter withconfig.max_rate.RateLimiter::new()performs a fallible narrowing conversion and unwraps it:The process then panics with:
Expected behavior
--rateshould accept positive values through1_000_000_000, the highest distinct per-second rategovernorcan represent at nanosecond resolution.1_000_000_000should be rejected before the spammer begins initialization.--rateand state the supported upper bound.Reproduction
I added three focused regression tests against current
main.CLI parsing
Result:
This confirms that the command-line parser accepts the unsupported value.
Configuration validation
Result:
This confirms that the oversized rate passes the current validation layer.
Rate limiter panic
Result:
All three focused tests exited with code 101 on the current implementation.
Tested against commit:
Root cause
The accepted configuration type and the rate limiter's supported type disagree:
rate:u64Config::max_rate:u64RateLimiterquota:NonZeroU32No validation bridges that type boundary. The first enforced upper bound is therefore the panicking
u32::try_from(...).expect(...)insideRateLimiter::new().Supported upper bound
Implementation review found that the effective upper bound is stricter than the storage type alone suggests.
governor0.8.1 computesQuota::per_second()as1_000_000_000ns / rate. Rates above1_000_000_000produce a zero-nanosecond interval, which its GCRA implementation clamps to one nanosecond. Such values fit inu32but cannot be represented as distinct requested rates.The correct maximum for this path is therefore
1_000_000_000, notu32::MAX. The original panic reproducer above remains valid, while the fix should enforce the actual rate-limiter resolution boundary.Why this matters
The failure occurs after configuration parsing and validation have reported success. Depending on the execution path, the spammer can perform other setup work before reaching the rate limiter construction.
A panic is also harder for scripts and orchestrators to classify than a normal invalid-argument error, and it incorrectly presents user-controlled input as an internal invariant violation.
Suggested fix
Enforce the rate limiter's supported range at the configuration boundary.
At minimum:
Config::max_rate > 1_000_000_000inConfig::validate();--rateand the maximum supported value;The CLI and internal configuration paths should share the same effective bound.
Potential regression tests
Add boundary tests verifying that:
--rate 1remains accepted;1_000_000_000is accepted;1_000_000_001is rejected by CLI parsing;Config::validate();RateLimiter::new()and panic;--rateand the supported maximum.Scope
This is a configuration-boundary correctness fix. It should not change rate limiting behavior for currently supported values, transaction generation, WebSocket behavior, or consensus/protocol behavior.
Duplicate check
I searched open and closed issues and pull requests using combinations of:
spammer raterate limiteru32::MAXTPS must fit in u32TryFromIntError--rate panicNo direct duplicate or existing implementation was found.