Skip to content

fix(stark): re-tune the Johnson gap, and stop calling a query budget "security" - #976

Open
MauroToscano wants to merge 1 commit into
mainfrom
fix/eta-johnson-gap
Open

fix(stark): re-tune the Johnson gap, and stop calling a query budget "security"#976
MauroToscano wants to merge 1 commit into
mainfrom
fix/eta-johnson-gap

Conversation

@MauroToscano

Copy link
Copy Markdown
Contributor

Re-tunes the Johnson gap in crypto/stark/src/proof/options.rs, and stops describing a
query budget as the security the system delivers.

+147 / −12 over 2 files, one signed commit. Base main.

The defect

options.rs:122 hardcoded the Johnson gap at 1/300:

let proximity = 1.0 - rate.sqrt() - 1.0 / 300.0;

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/300 the gap is tuned to maximise 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 in order to buy it.

⚠ And the code never computed eps_C at all. security_bits appeared in exactly one
place — sizing fri_number_of_queries — so it was a query budget wearing a security
label
, and with_blowup's "targeting 128-bit security" was never a statement about what
was 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:

preset gap (was 1/300) m security_bits (was 128) queries
blowup 2 1/39.6 14 118 219, unchanged
blowup 4 1/32 8 120 110, unchanged
blowup 8 unchanged unchanged 73, unchanged

The budget has to move because the gap feeds BOTH eps_C and bits_per_query: re-tuning
it 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_C floor rests on is drafted and awaiting ratification, and
the 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_C this change exists to fix. "Conservative" here means "no unfounded
claim", not "no downside". Deriving it is follow-up work.

Why nothing downstream can observe this

security_bits is not a field of ProofOptions. 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 new
pure functions, with_blowup's call site, and the proximity expression. Nothing else can
move, and fri_number_of_queries is unchanged at every blowup (219/110/73 and 43/36/44/37
at the higher ones).

with_blowup emits a bit-identical struct, so no downstream consumer — prover,
verifier, static commitments, the LFM registry — can observe this change. with_params has
no non-test caller other than with_blowup itself.

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_pinned pins (blowup, security_bits, grinding, queries) for
blowups 2 and 4, and pins blowup 8 as still being on the 128 budget. The defect this
guards is a constant drifting silently
1/300 sat there for a year while nothing
recomputed what it cost. A pinned tuple makes any future move a deliberate edit with a
failing test attached.

custom_grinding is repaired, and it was latently wrong before this change. It asserted
with_params(4, 128, 22) < with_blowup(4) — "more grinding, fewer queries" — with a silent
premise that both sides share a budget. Once with_blowup carries a per-blowup one, the
test 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 < 110 is false.
It now compares two with_params calls at the same budget, one variable — what it always
meant 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_values keeps all seven counts and gains a note: its agreement
with zisk's pil2-proofman-js calculator is still the reason blowups 8/32/64 hold, but no
longer the reason 2 and 4 do.

Two doc claims deleted as false

  • "the FRI query count is always the security bottleneck — field size is not" — it is not
    the bottleneck; eps_C is, and neither queries nor grinding move it.
  • the 152-bit support figure that followed from it.

Evidence

Laptop, at this head:

cargo test -p stark --lib     224 passed; 0 failed; 0 ignored
cargo fmt --all               no changes
make lint                     exit 0

Box B, 2026-09-09 — a differential: the suite runs at main AND 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 main and predicting an absolute
split never measured would be inventing a number.

                      base 8064a8ef (main)   head b6079603 (this branch)
prover --lib          557 / 0 / 25           557 / 0 / 25        IDENTICAL
stark                 218 / 0 / 0            219 / 0 / 0         +1
make lint                                    exit 0
preconditions         HEAD_ASSERT ok · TREE_CLEAN yes · ARTIFACTS 262 missing=0, both arms

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_blowup emits a bit-identical ProofOptions, 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 IDENTICAL on a delta that is not a movement. The correction was recorded
before 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.

git diff --stat 8064a8ef..b6079603   2 files, both crypto/stark; prover/ untouched
added   #[test] attributes           1   (the_production_posture_is_pinned)
removed #[test] attributes           0
removed test fns                     none

⚠ One gap recorded because it is unexplained, not because it changes anything: the laptop's
debug cargo test -p stark --lib reports 224 tests where box B's --release run of the
same 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:2286 and verifier.rs:1582,1665 bind let security_bits = air.context().proof_options.grinding_factor; — naming the GRINDING factor security_bits.
That is the same confusion this PR removes, in two more files, and is plausibly part of how
security_bits came to read as delivered security. Three lines, no behaviour change; it
gets its own commit so this PR stays one idea.

…"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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants