Repository navigation
Conversation
Some facts the program's soundness depends on were kept only by construction or by a test, not by the generator. Groth16.compile and the code under it now refuse to emit a program when: - an addition of the loop could meet T = Q for some point of the twist (the slope relations have no inverse of xQ - x), or the loop does not reach [6x + 2] B before its two Frobenius additions (the G2 check needs it): pairing.additions_avoid_q, with the eigenvalues of psi on the cofactor's primes, which tests/test_g2_check.py derives; - the step functions or QCHUNK would carry the loop state outside the bounds they assume: the recorded output bounds were never compared with the input bounds, so a stale or hand-made bounds dict went through; - a witness node occurs more than once in the tree unfolding of main (a spender may unshare a node and give each copy its own value): Emitter.tree_uses counts it; - a projection asked for as a subtree is not a whole subtree with its leaves in order, or a split has components of another width (Sym.subtree, Sym.split); - a binding is asked for a key with more than one public input. chain names its checks with a counter instead of the number of check keys present, so a dropped check can no longer make a later one overwrite an earlier one. All checks are `ensure`, so python -O keeps them. The generated programs are unchanged byte for byte (1 input at depths 0, 1, 2; 2 and 3 inputs). tests/test_invariants.py (in make and CI) checks each refusal, and that every jet_verify of the verifier is reachable from main and none lies under a branch of any of its 8,226 cases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Some tests could pass with the check they were meant to test removed: - tests/harness.py ignored hand's exit status, and the tests paired its result lines with their cases by zip, so a crash partway through a list silently skipped the remaining cases. run() now raises when hand exits non-zero or prints fewer results than witnesses. - An intended rejection was any failure at stage exec or finalize, which a type error passes too. harness.expect_rejected requires "Jet failed during execution" and replaces those asserts. - The wrong input and the tampered proof of tests/test_groth16.py kept the honest r, Q and z, so the challenge rejected them first: the identity and the range check were never exercised in the shipped program. They are now also sealed again, and so are the input plus 2^254 and the input plus r (when it fits in 254 bits), for which the identity holds and only the range check can reject. With the range check or the identity removed by hand, the old cases are still rejected while the new ones are accepted by the program in hand. - tests/test_inputs.py (3 to 5 inputs) ran only the numeric model, with a wrong input that was not sealed again. It is now also sealed again, and for the 3-input key the window factors of step 87, where PairingCheck.prod normalises, run in hand as a program of their own against the numeric model (seconds, where the whole program takes 25 minutes to parse). The test now needs the harness. - tests/test_pairing.py adds a flipped z and a point word re-encoded as itself plus 4p without sealing again; tests/test_fp.py adds words the jets must reduce first (2^256 - 1, p_s + 5, n_s + 7). The programs of the existing cases and their witnesses are unchanged; the new witnesses come after them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Hardening from the security audit (
build/security-audit/). Stacked on the security-fixes PR. Some facts the soundness argument relies on were kept only by construction or by a test, so the generator now checks them. Some tests could pass with the check they were meant to test removed, so they now exercise it. The generated programs are unchanged byte for byte.Summary
additions_avoid_q)Groth16.compilecounts its occurrences in the tree unfoldingsubtree,splitGroth16handin a testtest_groth16x + 2^254andx + r, which only the range check rejectshandCommits
1. Check at generation the invariants the soundness argument rests on.
pairing.additions_avoid_q(STEPS). This runs in everyPairingCheck. No addition can meet T = Q for any point of the twist, and the loop reaches [6x+2]B before its two Frobenius additions. The check uses the eigenvalues of ψ on the cofactor's primes (PSI_EIGEN);tests/test_g2_check.pyderives them and compares.Emitter.tree_uses()counts how often each definition occurs in the tree unfolding ofmain.Groth16.compilerequires exactly 90 witness nodes, each occurring once.Sym.subtreerequires the leaves to be exactly one subtree, in its order (the numeric backend takes them in the order given), andSym.splitrequires matching widths.Groth16(depth > 0)requires one public input.chainnames its checks with a counter, so a dropped check can no longer make a later one overwrite an earlier one.ensure, sopython -Okeeps them.tests/test_invariants.pyis new, in make and CI. It checks:jet_verifynodes of the verifier are reachable frommain;2. Make the tests fail when a check of the program is broken.
harness.run. It raises ifhandexits non-zero or prints fewer results than witnesses.harness.expect_rejectedrequiresJet failed during execution, and replaces the stage-only asserts in five tests.test_groth16.py. Four witnesses are sealed again (r, Q and z recomputed, which anyone can do), so that the challenges pass:Only the range check rejects the last two. The numeric model runs the pairing check before the range checks, so the test also asserts which check rejects each case there: the identity (
verify failed) for the first four,range checkfor the aliases.test_inputs.py.win_factorsat step 87 runs inhandas a program of its own, against the numeric model, on the honest and the extreme run values; a wrong factor must be rejected. Step 87 is wherePairingCheck.prodnormalises. The whole 3-input program takes 25 minutes to parse; this takes seconds.test_pairing.py. It adds a flipped z, and a point word re-encoded as itself plus 4p without sealing again: the challenge must reject both.test_fp.py. It adds words the jets must reduce first: 2^256 - 1, p_s + 5 and n_s + 7.How the new tests were checked
Two controls were removed by hand in a scratch copy, and
tests/test_groth16.pywas run with its numeric-model precheck turned off, so thathandalone decides:x < r(the word still read whole)input + 2^254, sealed, so the test failswrong input, sealed, so the test failsOn the old tests both mutants pass. Similarly, the 3-to-5-input numeric test passes with the identity removed, and the new sealed case catches it.
Testing
mainand at this branch: fp, curve, pairing, g2_check, binding, groth16,INPUTS=2groth16. The programs are identical. The witnesses are identical, with the new cases added after them.make lintis clean.🤖 Generated with Claude Code