fix(stark): re-tune the Johnson gap, and stop calling a query budget "security" - #976
Open
MauroToscano wants to merge 1 commit into
Open
fix(stark): re-tune the Johnson gap, and stop calling a query budget "security"#976MauroToscano wants to merge 1 commit into
MauroToscano wants to merge 1 commit into
Conversation
…"security" `options.rs` sized the FRI query count from a hardcoded Johnson gap of 1/300 and a flat `security_bits = 128`, and described the result as 128-bit security. It was neither. Delivered soundness is the WORSE of two independent terms: the FRI query error, and the commit-phase / proximity-gaps error `eps_C`. This module computes only the first. `eps_C` is never evaluated here, and grinding attaches to the query round alone, so it cannot buy `eps_C` back. `security_bits` was a query budget wearing a security label. The gap is the whole problem. It is a FREE ANALYSIS PARAMETER — it sets how close the decoding radius sits to the Johnson bound, equivalently the proximity parameter m = sqrt(rate) / (2 * eta) — and it trades the two terms against each other. At 1/300 it maximises bits-per-query, which is exactly the choice that MINIMISES `eps_C`. The system bought query-term security it could not spend and degraded the term that actually binds to do it. Re-tuned per blowup: 1/39.6 at blowup 2, 1/32 at blowup 4 (m = 14 and 8). The budget moves with it — 118 and 120, not 128 — because the gap feeds both `eps_C` and `bits_per_query`, so re-tuning it alone would make each query cheaper and buy more of them (110 -> 119 at blowup 4). ★ QUERY COUNTS DO NOT MOVE: 219 / 110 / 73, unchanged, and 43 / 36 / 44 / 37 at the higher blowups. Prove cost is identical. `security_bits` is not a field of `ProofOptions`, and all five fields it does have are unchanged at every blowup, so `with_blowup` produces a bit-identical struct and no downstream consumer can observe this. `with_params` is called by nothing but `with_blowup` outside the tests. ⚠ blowup 8 is deliberately NOT re-tuned. Its delivered bits are not computed, and inheriting a neighbour's constant would state a soundness claim nobody has checked. It keeps the historical gap and the 128 budget; the cost is that it also keeps the poor `eps_C` the re-tune exists to fix. `the_production_posture_is_pinned` pins (blowup, security_bits, grinding, queries) so the gap cannot drift silently again — it sat at 1/300 for a year while nothing recomputed what it cost, which is the defect as much as the value was. `custom_grinding` now compares two `with_params` calls at the SAME budget: it asserted "more grinding, fewer queries" against `with_blowup`, which no longer carries the same budget, so it was attributing a budget difference to grinding and read backwards. Two doc claims deleted as false: that the query count "is always the security bottleneck" (it is not — `eps_C` binds), and the 152-bit support figure that followed from it.
diegokingston
approved these changes
Sep 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Re-tunes the Johnson gap in
crypto/stark/src/proof/options.rs, and stops describing aquery budget as the security the system delivers.
+147 / −12 over 2 files, one signed commit. Base
main.The defect
options.rs:122hardcoded the Johnson gap at1/300:That constant is a free analysis parameter, not a property of the system. It sets how
close the decoding radius sits to the Johnson bound — equivalently the proximity parameter
m = sqrt(rate) / (2 · eta)— and it trades two error terms against each other.Delivered soundness is the WORSE of them: the FRI query error, and the commit-phase /
proximity-gaps error
eps_C.At
1/300the gap is tuned to maximise bits-per-query, which is exactly the choicethat minimises
eps_C. The system bought query-term security it could not spend anddegraded the term that actually binds in order to buy it.
⚠ And the code never computed
eps_Cat all.security_bitsappeared in exactly oneplace — sizing
fri_number_of_queries— so it was a query budget wearing a securitylabel, and
with_blowup's "targeting 128-bit security" was never a statement about whatwas delivered. Grinding cannot close the gap either: it attaches to the query round alone.
The change
Per-blowup, and the budget moves WITH the gap:
security_bits(was 128)The budget has to move because the gap feeds BOTH
eps_Candbits_per_query: re-tuningit alone makes each query cheaper and buys more of them — 110 → 119 at blowup 4. The
pairs above are chosen to leave every production query count exactly where it was.
Expected gain: +22.1 block-proven bits — ONCE §4 IS RATIFIED. The proximity-gaps
normative statement the
eps_Cfloor rests on is drafted and awaiting ratification, andthe entire figure is downstream of it. Until then this PR's defensible claim is narrower
and does not depend on §4: the gap was tuned against the term that binds, and the
re-tune costs nothing — same queries, same grinding, same prove time.
⚠ blowup 8 is deliberately NOT re-tuned
Its delivered bits are not computed. Inheriting a neighbour's constant would state a
soundness claim nobody has checked, so it keeps the historical gap and the 128 budget.
The cost of that choice, stated rather than left implicit: blowup 8 therefore also
keeps the poor
eps_Cthis change exists to fix. "Conservative" here means "no unfoundedclaim", not "no downside". Deriving it is follow-up work.
Why nothing downstream can observe this
security_bitsis not a field ofProofOptions. The struct has five —blowup_factor,fri_number_of_queries,coset_offset,grinding_factor,fri_final_poly_log_degree— and the non-comment diff touches exactly four things: two newpure functions,
with_blowup's call site, and theproximityexpression. Nothing else canmove, and
fri_number_of_queriesis unchanged at every blowup (219/110/73 and 43/36/44/37at the higher ones).
⇒
with_blowupemits a bit-identical struct, so no downstream consumer — prover,verifier, static commitments, the LFM registry — can observe this change.
with_paramshasno non-test caller other than
with_blowupitself.That is why this PR is safe to read as a documentation-and-constants change despite sitting
under the prover. It is also a claim rather than a hope, so the gate below tests it
directly.
The test is as much the point as the constant
the_production_posture_is_pinnedpins(blowup, security_bits, grinding, queries)forblowups 2 and 4, and pins blowup 8 as still being on the 128 budget. The defect this
guards is a constant drifting silently —
1/300sat there for a year while nothingrecomputed what it cost. A pinned tuple makes any future move a deliberate edit with a
failing test attached.
custom_grindingis repaired, and it was latently wrong before this change. It assertedwith_params(4, 128, 22) < with_blowup(4)— "more grinding, fewer queries" — with a silentpremise that both sides share a budget. Once
with_blowupcarries a per-blowup one, thetest attributes a BUDGET difference to GRINDING: the left side takes the caller's 128 with
the new gap (117 queries), the right side the new budget (110), and
117 < 110is false.It now compares two
with_paramscalls at the same budget, one variable — what it alwaysmeant to test. Verified by reverting the comparison and watching it fail, not by inference:
assertion failed … panicked at proof_options_tests.rs:82.jbr_queries_match_expected_valueskeeps all seven counts and gains a note: its agreementwith zisk's
pil2-proofman-jscalculator is still the reason blowups 8/32/64 hold, but nolonger the reason 2 and 4 do.
Two doc claims deleted as false
the bottleneck;
eps_Cis, and neither queries nor grinding move it.Evidence
Laptop, at this head:
Box B, 2026-09-09 — a differential: the suite runs at
mainAND at this head, one box,same command, result lines and failure names compared. A single arm could not test the
claim, because there is no recorded prover baseline for
mainand predicting an absolutesplit never measured would be inventing a number.
The prover is inert, exactly as claimed: 557 / 0 / 25 on both arms. Zero failures on
either side, so there are no failure names to compare. That is the load-bearing half —
with_blowupemits a bit-identicalProofOptions, and no prover test moves.stark is +1 by construction, and the pre-registration was mis-stated. This PR adds a
test, so the stark arms cannot be identical; the registration said "the arms are
identical" without excepting the branch's own test, and the gate diffs both suites, so it
printed
NOT IDENTICALon a delta that is not a movement. The correction was recordedbefore the base prover arm returned — 14:10:28Z, against an arm that started 14:07:18Z and
whose result line did not yet exist — and the amended rule was: merge iff the prover arms
match exactly and stark's only delta is that one added passing test. Both hold.
⚠ One gap recorded because it is unexplained, not because it changes anything: the laptop's
debug
cargo test -p stark --libreports 224 tests where box B's--releaserun of thesame command at the same commit reports 219. Both arms above are release on one box, so
the comparison is unaffected. The 5-test debug-vs-release delta is not diagnosed.
Follow-up, deliberately not in this PR
prover.rs:2286andverifier.rs:1582,1665bindlet security_bits = air.context().proof_options.grinding_factor;— naming the GRINDING factorsecurity_bits.That is the same confusion this PR removes, in two more files, and is plausibly part of how
security_bitscame to read as delivered security. Three lines, no behaviour change; itgets its own commit so this PR stays one idea.