Is there an existing issue?
Experiencing problems? Have you tried our Stack Exchange first?
Description of bug
Summary
For a benchmark that reads many storage keys the runner does not recognise (UNKNOWN KEY in the storage summary), the generated weights.rs can contain a proof_size in the exabyte range while the same block reports a measured size of 85 bytes:
// Proof Size summary in bytes:
// Measured: `85`
// Estimated: `8126544059662763008 + b * (7 ±0)`
Weight::from_parts(17_274_795, 8126544059662763008)
The value is not deterministic. Two runs of the same command on the same machine, same binary, same flags produced:
| Run |
proof_size |
hex |
| 1 |
2585700789447993344 |
0x23e2412b1a7ea800 |
| 2 |
8126544059662763008 |
0x70c748c417c28400 |
The plausible value for this benchmark is ~123,750 bytes
(log16(1_000_000) = 5 trie layers × 15 * 33 bytes × ~50 keys), so the emitted number is off by roughly 14 orders of magnitude.
A proof_size this large silently poisons block-weight accounting: a single extrinsic declares more PoV than any block can hold.
Versions
frame-benchmarking-cli 58.0.1
frame-benchmarking 49.0.0
polkadot-sdk 2606.0.0 (crates.io release line)
- Host: Ubuntu, 16 vCPU / 32 GB (also reproduced on 8 vCPU / 16 GB)
Analysis
The proof_size that lands in the file comes from writer.rs::add_storage_comments → analysis_function(results, BenchmarkSelector::ProofSize), evaluated per storage prefix:
// frame-benchmarking-cli/src/pallet/writer.rs:308
let proof_size = analysis_function(results, BenchmarkSelector::ProofSize)
and --output-pov-analysis defaults to median-slopes, while the value written for the weight itself goes through min_squares_iqr.
The two analysis functions differ in exactly the way that matters here:
// analysis.rs:338 — median_slopes CLAMPS
let base = selector.scale_and_cast_weight(models[0].0.max(0f64), false);
// analysis.rs:419 — min_squares_iqr does NOT
base: selector.scale_and_cast_weight(intercept, true),
scale_and_cast_weight for a non-ExtrinsicTime selector is a bare cast:
// analysis.rs:59
(value + 0.5) as u128
What was ruled out
Two obvious explanations were tested and do not hold:
-
Negative intercept cast to unsigned. Rust saturates float→integer casts, so a negative or NaN intercept becomes 0, not a huge number. Verified:
value=-1.0 → (v+0.5) as u128 = 0
value=-1e18 → (v+0.5) as u128 = 0
value=NaN → (v+0.5) as u128 = 0
value=+inf → (v+0.5) as u128 = 340282366920938463463374607431768211455
Only +inf yields a large value, and it yields u128::MAX, not the observed numbers.
-
The regression itself. Feeding linregress::fit_low_level_regression_model the same shape of data through the same wrapper produces correct results:
y constant (85): intercept=85.0 slope≈0
y = 123500 + 7b: intercept=123500.0 slope=7.0
So the corruption is upstream of the cast and of the regression: the BenchmarkResult values reaching the analysis, or the per-prefix aggregation in process_storage_results, are already wrong. The non-determinism across identical runs points that way too, a deterministic mis-parameterisation would produce the same wrong number every time.
--default-pov-mode measured does not help. It selects a different branch in process_storage_results, but the accumulation at writer.rs:643 (prefix_result.proof_size += pov_overhead * *reads) runs in that branch as well.
Suggested direction
Two things look worth doing independently of the root cause:
-
Clamp in min_squares_iqr as median_slopes already does — a one-line change at analysis.rs:419. It would not fix the bad input, but a weight file is a consensus artefact: emitting u128-range garbage is worse than emitting a clamped value.
-
Reject implausible proof_size output. A calculated proof size that exceeds, say, the maximum PoV a block can carry is never a legitimate result and could fail the run instead of being written to a file that compiles.
Happy to test a patch, the reproduction is stable in the sense that 50 steps reliably produces garbage on our benchmark, even though the specific value varies.
Steps to reproduce
The affected benchmark declares one linear component and calls a handler that reads a StorageMap belonging to another pallet which does not implement MaxEncodedLen (pallet-ismp's RequestCommitments, reported as ~50 UNKNOWN KEY entries):
#[benchmark]
fn dispatch_post(b: Linear<0, 8192>) {
// ... dispatches through the other pallet's `IsmpDispatcher`
}
./node benchmark pallet \
--chain dev --pallet my_pallet --extrinsic 'dispatch_post' \
--steps 50 --repeat 20 --wasm-execution=compiled \
--output weights.rs --template frame-weight-template.hbs
Notably, --steps 3 produces a correct value (3550) and --steps 50 does not, so the number of sampled points decides whether the output is usable.
Is there an existing issue?
Experiencing problems? Have you tried our Stack Exchange first?
Description of bug
Summary
For a benchmark that reads many storage keys the runner does not recognise (
UNKNOWN KEYin the storage summary), the generatedweights.rscan contain aproof_sizein the exabyte range while the same block reports a measured size of 85 bytes:The value is not deterministic. Two runs of the same command on the same machine, same binary, same flags produced:
proof_size25857007894479933440x23e2412b1a7ea80081265440596627630080x70c748c417c28400The plausible value for this benchmark is ~123,750 bytes
(
log16(1_000_000) = 5trie layers ×15 * 33bytes × ~50 keys), so the emitted number is off by roughly 14 orders of magnitude.A
proof_sizethis large silently poisons block-weight accounting: a single extrinsic declares more PoV than any block can hold.Versions
frame-benchmarking-cli58.0.1frame-benchmarking49.0.0polkadot-sdk2606.0.0 (crates.io release line)Analysis
The
proof_sizethat lands in the file comes fromwriter.rs::add_storage_comments→analysis_function(results, BenchmarkSelector::ProofSize), evaluated per storage prefix:and
--output-pov-analysisdefaults tomedian-slopes, while the value written for the weight itself goes throughmin_squares_iqr.The two analysis functions differ in exactly the way that matters here:
scale_and_cast_weightfor a non-ExtrinsicTimeselector is a bare cast:What was ruled out
Two obvious explanations were tested and do not hold:
Negative intercept cast to unsigned. Rust saturates float→integer casts, so a negative or
NaNintercept becomes0, not a huge number. Verified:Only
+infyields a large value, and it yieldsu128::MAX, not the observed numbers.The regression itself. Feeding
linregress::fit_low_level_regression_modelthe same shape of data through the same wrapper produces correct results:So the corruption is upstream of the cast and of the regression: the
BenchmarkResultvalues reaching the analysis, or the per-prefix aggregation inprocess_storage_results, are already wrong. The non-determinism across identical runs points that way too, a deterministic mis-parameterisation would produce the same wrong number every time.--default-pov-mode measureddoes not help. It selects a different branch inprocess_storage_results, but the accumulation atwriter.rs:643(prefix_result.proof_size += pov_overhead * *reads) runs in that branch as well.Suggested direction
Two things look worth doing independently of the root cause:
Clamp in
min_squares_iqrasmedian_slopesalready does — a one-line change atanalysis.rs:419. It would not fix the bad input, but a weight file is a consensus artefact: emittingu128-range garbage is worse than emitting a clamped value.Reject implausible
proof_sizeoutput. A calculated proof size that exceeds, say, the maximum PoV a block can carry is never a legitimate result and could fail the run instead of being written to a file that compiles.Happy to test a patch, the reproduction is stable in the sense that 50 steps reliably produces garbage on our benchmark, even though the specific value varies.
Steps to reproduce
The affected benchmark declares one linear component and calls a handler that reads a
StorageMapbelonging to another pallet which does not implementMaxEncodedLen(pallet-ismp'sRequestCommitments, reported as ~50UNKNOWN KEYentries):./node benchmark pallet \ --chain dev --pallet my_pallet --extrinsic 'dispatch_post' \ --steps 50 --repeat 20 --wasm-execution=compiled \ --output weights.rs --template frame-weight-template.hbsNotably,
--steps 3produces a correct value (3550) and--steps 50does not, so the number of sampled points decides whether the output is usable.