circom-pairing CoreVerifyPubkeyG1() BigLessThan Output Omission / Forged-Signature Near Miss (2022-11-25)
- Name: circom-pairing
CoreVerifyPubkeyG1()BigLessThanoutput omission / forged-signature near miss - Date: 2022-11-25
- Protocol: circom-pairing
- Chain: Ethereum ecosystem
- Category: under-constrained circuit / unconsumed validation output / forged-signature near miss
- Loss: no public live exploitation or onchain loss was reported; the bug was found during an audit before the audited bridge-oriented integration was publicly known to have suffered a live signature-forgery event
- Summary: circom-pairing’s
CoreVerifyPubkeyG1()circuit instantiated tenBigLessThan(n, k)gadgets to ensure the public-key coordinates and hash field elements used in BLS12-381 signature verification were properly formatted big integers below the curve primeq. But the outer circuit never constrained any of the gadgets’outsignals. In Circom, that meant the crucialinput < qadmission check was computed but not actually required by the proved statement. Invalid or out-of-range bigints could therefore flow into the core signature-verification relation even though reviewers reading the code would see the validators and assume the checks were enforced. Veridise reported the issue during an audit for Succinct Labs, warning that the bug could allow forged signatures in systems relying on the library. - Trigger / entry point:
- The vulnerable circuit was
CoreVerifyPubkeyG1(n, k)incircuits/bls_signature.circom. - That circuit is the core BLS12-381 signature-verification path for a public key in
G1. - Before entering the rest of the verification logic, it instantiated
component lt[10];withBigLessThan(n, k)to validate thepubkeyandhashlimbs against the BLS12-381 primeqreturned byget_BLS12_381_prime(n, k). - The intended meaning of those gadgets was explicit in the source comment:
check all len k input arrays are correctly formatted bigints < q. - The decisive omission was that none of the ten
lt[i].outsuccess bits were constrained to equal1.
- The vulnerable circuit was
- Root cause:
- circom-pairing separated
run the validation gadgetfromrequire the validation gadget to succeedand accidentally shipped only the first half. BigLessThanwas being used both as a limb-formatting helper and as thevalue < qadmission gate for the core templates beneathCoreVerifyPubkeyG1().- Because the circuit never consumed the
lt[i].outbits, the proof statement did not actually include the promised range checks. - The resulting soundness bug was narrow but dangerous: the outer verification circuit relied on assumptions about input shape that it visually prepared for but never forced into the statement.
- circom-pairing separated
- Failure path:
- An attacker chooses
pubkeyorhashlimb values that violate the intendedvalue < qor proper-format assumptions. CoreVerifyPubkeyG1()still instantiates the tenBigLessThangadgets and wires those malformed inputs into them.- The gadgets compute
lt[i].out = 0for the invalid inputs, but no outer constraint requires those outputs to be1. - Because the validation result is not part of the statement, proof generation can still succeed if the remaining constraints are satisfiable.
- The verifier then accepts a proof for a signature-verification relation that is weaker than reviewers intended:
the helper existedrather thanall checked inputs were valid field elements below q. - Any downstream protocol treating this proof as BLS-signature truth inherits a forged-signature risk.
- An attacker chooses
- What the attacker needed to be true:
- A deployed or funded system needed to rely on circom-pairing’s
CoreVerifyPubkeyG1()relation as a security boundary for BLS12-381 signature validity. - The system needed to assume the circuit itself enforced the
pubkey/hash< qpreconditions rather than re-checking them elsewhere. - No separate wrapper or outer circuit could restore the missing admission check by constraining the same values independently.
- The malformed inputs had to remain useful to the attacker within the broader proving relation, not merely trigger a visible failure.
- A deployed or funded system needed to rely on circom-pairing’s
- Technical anatomy worth remembering:
- The bug was not
BigLessThanis broken`. The helper was doing its job; the outer circuit simply never made the result matter. - This is why the issue is a clean exemplar of
validation-output nonconsumptionrather than generic arithmetic failure. The code visibly contained the right validator, comment, and wiring. - Veridise’s writeup makes the broader lesson explicit: circom-pairing had intentionally pushed some heavy validation out of core templates for performance reasons, which meant the call-site boundary in
CoreVerifyPubkeyG1()became statement-critical. BigLessThanmattered twice here. It was not only the< qcomparator; it also enforced proper bigint formatting of the input registers through its internal bit-decomposition logic. By omittinglt[i].out === 1, the circuit weakened both the range and admission story at once.- The fix was correspondingly small and revealing. The patch did not redesign the library or change the core pairing math; it simply added:
var r = 0;for(var i=0; i<10; i++){ r += lt[i].out; }r === 10;
- That one final equality turned ten advisory helper results into actual statement truth.
- The bug was not
- Representative artifacts:
- Vulnerable circuit:
circuits/bls_signature.circom - Vulnerable template:
CoreVerifyPubkeyG1(n, k) - Validation helper:
BigLessThan(n, k) - Reported missing binding: the unconstrained
lt[i].outoutputs for ten validator instances - Fix pull request:
yi-sun/circom-pairingPR #21,CoreVerifyPubkeyG1 does not enforce lt checks on input. Bug found by Veridise during audit - Fix commit:
c686f0011f8d18e0c11bd87e0a109e9478eb9e61 - Fix commit date from patch metadata:
2022-11-25 15:09:50 -0600 - Primary source anchor: the patched
circuits/bls_signature.circomshows the exact binding edge restored withvar r = 0; ... r === 10; - Public disclosure article: Veridise,
Circom-Pairing: A Million-Dollar ZK Bug Caught Early(2023-01-03) - Audit context highlighted in the disclosure: Succinct Labs relied on BLS12-381 signature verification for a bridge design under audit
- Vulnerable circuit:
- Response / mitigation:
- The maintainers merged a focused fix on 2022-11-25 that aggregated the ten validation outputs and constrained their sum to
10, forcing everyBigLessThancheck to succeed. - Veridise publicly disclosed the issue on 2023-01-03 after the fix, framing it as the most critical bug found in the audit and noting that it could allow forged signatures.
- The practical response lesson is broader than merging one six-line fix: once a shared BLS-signature circuit library admits malformed inputs at the statement boundary, teams need a downstream statement inventory of every integration that compiled or trusted that verifier family, especially bridge or light-client systems that cash out on signature validity.
- The durable mitigation lesson is not merely
remember this one equality. It is to treat every validation gadget output in a zk circuit as part of the statement boundary until proven otherwise.
- The maintainers merged a focused fix on 2022-11-25 that aggregated the ten validation outputs and constrained their sum to
Pattern tags
- validation-output-nonconsumption
- proof-element-admission-boundary-discipline
- The circuit visibly ran the right admission helper, but the proved statement never required that helper to succeed.
Three validator-output promotions this incident collapsed
-
A validator-shaped admission object existed at all
CoreVerifyPubkeyG1()really did instantiate tenBigLessThan(n, k)gadgets over thepubkeyandhashlimbs, and the source comment truthfully advertised them as< qformatting checks.- That only answered the helper-existence question:
some admission machinery ran next to the signature relation.
-
That admission object was treated as canonical statement truth for this signature relation
- The live bug sat here. Reviewers, callers, and downstream integrators were allowed to read
lt[i]as if the intendedvalue < qclaim had already become part of the proved statement, even though none of the tenlt[i].outbits were ever forced to equal1. - In other words, the circuit quietly promoted
the helper computed an answerintothe verifier proved those inputs were valid field elements below q.
- The live bug sat here. Reviewers, callers, and downstream integrators were allowed to read
-
That accepted statement still deserved downstream signature-authority budget
- Once the circuit was treated as a real BLS12-381 signature verifier, bridge or light-client style consumers could have inherited forged-signature risk from a statement that never truly sealed its own admission boundary.
- The practical authority question is the dangerous one:
why should a proof about a helper-adjacent but not helper-binding relation still count as live signature truth for downstream custody or message-authentication systems?
Why the exploit worked
CoreVerifyPubkeyG1()instantiated tenBigLessThanvalidators and even documented their purpose, but none of the tenlt[i].outbits were ever forced to equal1, so the circuit proved only that the helpers existed, not that the checked values were actually belowq.- That omission was enough because the rest of the BLS12-381 verification relation inherited the helpers’ formatting and range assumptions as if they were already statement truth.
- The exploit therefore did not need a broken comparator or a malformed-proof parser bug. It only needed one call-site boundary where
compute validation resultandconstrain validation resultdrifted apart. - Veridise’s writeup makes the more reusable design lesson explicit: once performance-driven refactoring pushes heavy admission logic into helper gadgets, the outer circuit boundary becomes the real security object, and any unconstrained
outbit there is effectively a silent weakening of the verifier statement.
Detection and audit pivots
- Grep for validators whose outputs are advisory-looking wires rather than hard constraints:
LessThan,BigLessThan, subgroup checks, on-curve checks, nullifier checks, formatting checks, and any helper returning anoutor boolean success bit. - For each helper, ask the sharp statement-boundary question: where is the one line that turns
validator returned trueinto mandatory circuit truth rather than dead witness computation? - Negative-test the exact malformed objects the helper was supposed to kill here — out-of-range
pubkeylimbs andhashlimbs — and assert that proof generation or verification fails instead of merely producinglt[i].out = 0internally. - Treat optimization boundaries as security boundaries. If validation was moved out of a core gadget for performance, review the outer wrapper as if it were the canonical verifier, because that is where admission truth can quietly become optional.
- If the vulnerable helper sits in a shared signature, membership, or proof-verification library, do not stop at the source patch. Inventory which compiled circuits, generated verifiers, bridges, or rollup components still rely on the same library boundary, because the real incident object is often
one under-constrained helper family with many downstream proof statementsrather than one repository diff.
Why this incident matters
- It is a very clean audit-caught exemplar for the proofs/verifiers corpus because the unsafe gap is only a few lines long and still severe enough to threaten signature soundness.
- It preserves a recurring lesson for optimized zk systems: moving validation out of core templates for performance is safe only if the outer boundary makes those validations non-optional statement truth.
- It also helps separate two bug classes that are often blurred together. The issue was not malformed proof elements slipping past a parser and not contract/verifier domain drift; it was the circuit itself computing the right validation answer and then never proving that answer had to be true.
Failure-prevention checklist
- Treat every validator output as a settlement-critical object until it is explicitly consumed by the statement.
lt[i].out, subgroup booleans, on-curve checks, and range-check bits should all have one obvious fail-closed binding line. - Write wrapper-level invariants for optimized circuits. If heavy validation is moved out of a core gadget to save constraints, the outer template should own an explicit
all validation bits succeededinvariant that reviewers can find immediately. - Add negative tests for the exact malformed objects the helper was meant to reject, not just happy-path proofs. Here that means out-of-range or improperly formatted
pubkeyandhashlimbs must make proof generation or verification fail. - Centralize helper-consumption patterns so libraries do not repeatedly hand-roll
instantiate validator, forget outputseams at each call site. - Treat shared proof libraries as deployment families, not isolated source patches. Once one statement boundary is found under-constrained, inventory every compiled circuit, generated verifier, or bridge integration that trusted that helper family.
- Prefer statement-shaped assertions over comment-shaped intent. If a source comment says
check ... < q, require one nearby equality or aggregate condition that makes the claim literally true in constraints. - Review performance optimizations as security-boundary changes. Removing inner validation from a core template is not a micro-optimization; it relocates the admission boundary and deserves fresh audit attention.
- Keep regression tests at the exact boundary that failed. The six-line fix here worked because it turned ten advisory outputs into one required invariant; future tests should fail if any of those outputs can return
0without breaking the statement. - Require downstream integrators to pin audited library commits and re-check statement bindings after vendor updates, local forks, or partial cherry-picks.
Audit prompts
- Where is the one line that turns each validator’s success bit from witness-side computation into mandatory statement truth?
- Which helpers in this circuit family return booleans or
outsignals, and which of those outputs are never constrained, aggregated, or otherwise consumed? - If malformed
pubkey,hash, subgroup, or curve-point inputs are supplied, does proof generation fail because the statement rejects them, or do helper outputs merely flip to0off to the side? - Did any optimization move validation out of a lower-level gadget and into a wrapper that now silently owns the real admission boundary?
- If this helper family sits inside a bridge, light client, signature-checker, or vote verifier, which compiled downstream statements still trusted the same under-constrained boundary when disclosure landed?
- Are review comments or naming doing security work that constraints never actually perform?
- Can a simple aggregate invariant like
sum(outputs) === expected_countor per-output=== 1assertion prove that every validator fired successfully, and if so why is it absent? - After a fix, do tests exercise malformed inputs end to end against the exact compiled circuit or generated verifier artifact, not only against source-level helper behavior?
- If one unconstrained validator output were reintroduced tomorrow during optimization, what CI or linting check would catch it before release?
Lessons that should already be standard
- A validator that is instantiated but not consumed is not a validator; it is commentary with wires attached.
- In zk systems, optimization boundaries are statement boundaries. Moving checks outward for performance means the outer wrapper becomes the real security object.
- Reviewers should distrust source comments like
check input < quntil they can point to the exact constraint that makes failure impossible. - The dangerous bug shape is often not
missing cryptographybutcorrect helper, unconstrained result. - Shared circuit libraries need blast-radius thinking. One missed output binding in a reusable signature helper can quietly weaken every downstream verifier compiled from it.
- Tiny patches can fix huge bugs because the real failure may be only one lost statement-binding seam.
The helper ranandthe statement required the helper to succeedare completely different security claims.
Internal linkages
- Core recurring pattern: validation-output-nonconsumption.
- Positive-design companion: proof-element-admission-boundary-discipline — the broader healthy-side question of where malformed proof-facing objects should be forced to die before arithmetic or downstream authority begins.
- Statement-binding companion: proof-validation-output-consumption-and-statement-binding-discipline — the sharper healthy-side companion when the question is where
validator ran,validator succeeded, andvalidator success became statement truthshould have stayed one local proof story instead of a caller-memory obligation. - Live-response / deployment-lineage companion: verification-key-statement-inventory-and-asset-reachability-discipline — useful when the sharper follow-up is which compiled circuits, deployed verifiers, bridges, or downstream applications still trusted the same
validator existed but output never became statement truthfamily after disclosure. - Adjacent field-domain companion: proof-facing-field-domain-canonicalization-discipline — useful when the sharper question is how contract-facing values should be kept in one proving-field domain end to end rather than how an already-instantiated validator output was dropped.
- Adjacent malformed-element family: curve-point-validation-failure — nearby because the underlying purpose here was still proof-element admission, but the decisive bug was narrower: the validator existed and its result simply never became statement truth.
- Adjacent split-statement family: witness-public-input-binding-failure — that family proves the wrong relation by splitting semantic facts across public and private roles; this issue kept one local validation fact outside the relation altogether.
Control surface
-
The leverage sat in one small statement-binding seam:
BigLessThanhelper outputs, the exact lines after helper instantiation, and the boundary where optimized validation was supposed to become mandatory for BLS12-381 signature verification. -
The decisive question was not
did the code instantiate range checks?butdid the proved statement actually require those range checks to succeed before accepting the signature relation? -
Sources:
- https://github.com/yi-sun/circom-pairing/pull/21
- https://patch-diff.githubusercontent.com/raw/yi-sun/circom-pairing/pull/21.patch
- https://raw.githubusercontent.com/yi-sun/circom-pairing/c686f0011f8d18e0c11bd87e0a109e9478eb9e61/circuits/bls_signature.circom
- https://medium.com/veridise/circom-pairing-a-million-dollar-zk-bug-caught-early-c5624b278f25
- https://github.com/0xPARC/zk-bug-tracker
-
Last reviewed: 2026-08-04 UTC