Repository navigation
Security fixes: bind PublicMerkleRoot(0) circuits, pin the demo's powers of tau - #12
Merged
Merged
Conversation
tools/groth16_snarkjs.py used --values 0, its default, to mean "no binding". But PublicMerkleRoot(0), a circuit whose only leaf is the sighash, is a valid case of the convention (public_merkle.circom and merkle.depth_of give it a tree of depth 1), and the README maps it to `--values 0`. Such a circuit got a program without the sig_all_hash check, so anyone who saw a spend could reuse its witness in another transaction to the same output. Now --values N (N >= 0) or --leaves always binds, at depth merkle.depth_of(N + 1), and a program without binding needs an explicit --unbound. Giving neither, or both, a negative --values, or a --values that disagrees with --leaves is refused. The tool's checks of the key, the proof and the leaves are no longer asserts, so python -O keeps them, and public signals and values must be below r. Programs for bound circuits are unchanged byte for byte. The README and docs/design.md say what --unbound gives up, and tests/test_tools.py (in make and CI) checks the depth table, the refusals, the -O case and a witness whose leaves do not give the public signal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tools/circom_demo.py took the phase-1 powers of tau from a cached file, from PTAU, or from a download of a fixed S3 URL, and checked none of them. snarkjs takes alpha, beta and the IC points of the key from that file, and the one phase-2 contribution changes only delta, so a substituted file with a known trapdoor gives a key for which anyone can prove false statements. curl also ran without --fail, so an HTTP error body was cached and reused. The file is now used only if its BLAKE2b-512 digest is the one recorded for its size (PTAU_BLAKE2B gives another). The download uses curl --fail with HTTPS only, is hashed before it is kept, and a refused or failed download leaves nothing behind. After the contribution, `snarkjs zkey verify` checks the key against the circuit and the powers of tau and prints the contribution hash. The contribution's entropy is random instead of a fixed string (snarkjs adds its own randomness either way). The setup and trusted-setup files are ignored by git outside build/ too. The recorded digest is that of ppot_0080_16.ptau as the URL served it on 2026-10-14; compare it with the list in the ceremony's README (github.com/privacy-ethereum/perpetualpowersoftau). tests/test_tools.py checks that a wrong cached file and a wrong download are refused. 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.
The two issues the security audit (
build/security-audit/) reported, each with tests. Programs for bound circuits are unchanged byte for byte.This is the first of four stacked PRs: security fixes, generator checks and tests, deploy, CI and docs. Merge them in order, and delete each branch on merge so that GitHub retargets the next PR to
main.Summary
--values 0, or no--values--values 0binds at depth 1 (PublicMerkleRoot(0)); no flag is refused--unboundassert, stripped bypython -O-OkeepsPTAU, or acurl -sSLdownloadCommits
1. Bind circuits with no public values, and make the unbound mode explicit.
tools/groth16_snarkjs.pyused--values 0, its default, to mean "no binding". ButPublicMerkleRoot(0)is a valid case of the convention: a tree that holds only the sighash, of depth 1 inpublic_merkle.circomandmerkle.depth_of. The README maps it to--values 0. That gave a program without thesig_all_hashcheck, so anyone who saw a spend could reuse its witness in another transaction to the same output. The audit reproduced this: one witness was pruned and executed in three different transactions. It rated it low: it needs a circuit with no public values and a deployer who ignores the "no transaction binding" line.--values N(N ≥ 0) or--leavesalways binds, at depthmerkle.depth_of(N + 1).--unbound.--values;--valuesthat disagrees with--leaves;asserts, sopython -Okeeps them.docs/design.mdsay what--unboundgives up: anyone can reuse the witness, and the spender chooses the public inputs.tests/test_tools.pyis new, in make and CI. It checks:python -O;2. Check the demo's powers of tau against a pinned digest.
curlalso ran without--fail, so an HTTP error body was cached and reused.PTAU), the file is used only if its digest matchesPTAU_BLAKE2Bfor its size. ThePTAU_BLAKE2Benvironment variable supplies a digest for another file.curl --failwith HTTPS only, redirects included;snarkjs zkey verifychecks the key against the circuit and the powers of tau, and prints the contribution hash.*.ptau,*.zkeyand*.wtnsare ignored outsidebuild/too.tests/test_tools.pychecks that a wrong cached file and a wrong download are refused and leave nothing behind.Please check one value before merging. The recorded digest is that of
ppot_0080_16.ptauasPTAU_URLserved it on 2026-10-14:I could not reach the ceremony's README from here. Please compare this with the digest listed for
ppot_0080_16.ptauin privacy-ethereum/perpetualpowersoftau.Intentional behaviour changes
groth16_snarkjs.py program vk.json --out x, with no flag, now fails. Give--values Nor--unbound.--unboundtoo.--values 0now builds a bound program.main(argv=None)takes an argument list, so tests can call it.circom_demo.py setup:zkey verify.Testing
make exampleregeneratesexamples/byte for byte with--values 1.tests/test_tools.pypasses.--values 0on the example key.main: "no transaction binding", and nojet_sig_all_hashin the program.--values 1.circom_demo.py setupandproveran end to end with the pinned file:ZKey Ok!;🤖 Generated with Claude Code