circom-ecdsa BigMod Remainder Range-Check Omission / Non-Canonical Remainder Near Miss (2022-03-30)
- Name: circom-ecdsa
BigModremainder range-check omission / non-canonical remainder near miss - Date: 2022-03-30
- Protocol: circom-ecdsa / BigInt
- Chain: Ethereum ecosystem
- Category: bit-length precondition omission / bigint remainder canonicalization failure / library-level near miss
- Loss: no public live exploit was documented, but any downstream circuit that treated
BigMod’smod[]output as a proper canonical bigint remainder could have been reasoning about a weaker statement than intended - Summary: circom-ecdsa’s
BigMod(n, k)circuit used a helperlong_divroutine to witness a quotientdiv[]and remaindermod[]for big-integer modulo operations. The circuit correctly range-constrained each quotient limb withNum2Bits(n)but forgot to do the same for the remainder limbs. That omission mattered because the bigint library explicitly distinguished betweenproper representationand widersigned overflow representation: without per-limb width constraints onmod[], the remainder could satisfy the arithmetic identity while still living outside the canonicaln-bit representation downstream consumers expected. In other words,a = q*b + rcould hold in the circuit, yetrwas not guaranteed to be the normal proper-form remainder other gadgets assumed they were receiving. Andrew He reported the issue in March 2022, and the merged fix added amod_range_checks[k]loop so each remainder limb now passes throughNum2Bits(n)before the result can be trusted as a canonical bigint object. - Trigger / entry point:
- The affected circuit was
BigMod(n, k)incircom-ecdsa’s bigint library. BigModobtained quotient and remainder witness arrays from the helperlong_divfunction.- In the vulnerable code shape, the circuit instantiated one
component range_checks[k + 1]array and appliedNum2Bits(n)only todiv[i]for0 <= i <= k. - There was no sibling
mod_range_checks[k]loop formod[i], so the quotient was range-constrained into proper form while the remainder was merely assigned from the witness.
- The affected circuit was
- Root cause:
- The circuit proved the algebraic division relation but not the full representation claim downstream consumers were relying on.
- In this bigint library,
proper representationis a stronger statement thansome field elements that satisfy the same arithmetic equation. - Because
mod[]lacked per-limb width checks, the remainder could remain in a wider or signed-overflow-style representation even while the top-level modulo relation still held. - The circuit therefore silently downgraded
canonical remainder objectintoarithmetic witness that happens to satisfy the equality.
- Failure path:
- A caller uses
BigMod(n, k)and expectsmod[]to be a properk-limb base-2^nremainder. - The witness generator produces quotient limbs
div[]and remainder limbsmod[]vialong_div. - The circuit range-checks every quotient limb with
Num2Bits(n)but leaves every remainder limb unconstrained at that representation boundary. - The arithmetic identity tying
a,b,div[], andmod[]can still verify even ifmod[]is not in canonical proper representation. - A downstream gadget or application that assumes
mod[]is canonical may then derive proofs, comparisons, or reductions from a weaker object than intended.
- A caller uses
- What the attacker or failure needed to be true:
- A downstream circuit had to consume
BigModunder the assumption thatmod[]was already in proper representation. - The vulnerable pre-fix
BigModimplementation had to be in use. - The prover had to control witnesses tightly enough to exploit the missing remainder-width constraint.
- No later canonicalization step could collapse or reject the non-canonical remainder before it influenced further logic.
- A downstream circuit had to consume
- Technical anatomy worth remembering:
- This is a pure
bit-length precondition omissioncase: the arithmetic helper relation was enforced, but one output object’s canonical-width assumption was never proven. - The asymmetry is the tell. The quotient already had a
Num2Bits(n)loop; the remainder did not. That is exactly the kind of half-complete canonicalization reviewers should treat as suspicious in bigint circuits. - The library’s own representation model made the bug sharper. The circom-ecc spec defines a
proper BigInt representationas limbs satisfying0 <= a[i] < 2^n, whilesigned overflow representationonly requires each limb to stay below roughlyr/2and interprets it throughtoZ(...). Leavingmod[]without limb-width checks therefore did not merely leave the remaindera little loose; it left it in the broader signed-overflow family when downstream gadgets were likely expecting proper base-2^nform. - The fix was minimal and revealing: split the old quotient-only
range_checksarray intodiv_range_checksplus a newmod_range_checks[k]loop, each usingNum2Bits(n)on the corresponding limb. No exotic arithmetic redesign was needed; the real missing invariant was the width proof on the output object.
- This is a pure
- Representative circuits and artifacts:
- Vulnerable circuit:
circuits/bigint.circomtemplate BigMod(n, k) - Vulnerable witness source:
long_div(...)feedingdiv[]andmod[] - Missing invariant before the fix: no
Num2Bits(n)range check overmod[i] - Spec artifact that made the bug semantically sharp: the circom-ecc spec’s distinction between
proper BigInt representationandsigned overflow representation - Report / fix PR:
0xPARC/circom-ecdsa#10—BigMod incorrectly omits range checks on the remainder - Merged fix commit:
08c2c905b918b563c81a71086e493cb9d39c5a08 - Patch shape: rename quotient checks to
div_range_checksand add a newmod_range_checks[k]loop applyingNum2Bits(n)to everymod[i]
- Vulnerable circuit:
- Response / mitigation:
- The maintainers merged the patch adding explicit
Num2Bits(n)checks on the remainder limbs. - That restored the missing guarantee that
mod[]is a proper canonical bigint object, not merely a witness satisfying the arithmetic equation. - The durable review lesson is broader than this one library: every bigint helper must prove canonical representation for every output other gadgets are expected to interpret semantically, not just for the output that looked more obviously dangerous at first glance.
- The maintainers merged the patch adding explicit
Pattern tags
- bit-length-precondition-omission
BigModproved enough to show a division relation but not enough to guarantee that the remainder object lived in the canonicaln-bit limb representation downstream circuits assumed.- The decisive failure was output-width asymmetry:
div[]was range-checked into proper form, whilemod[]was still trusted as if the same proof had already happened.
Three remainder-canonicalization promotions this incident collapsed
-
A remainder-shaped object existed at all
long_div(...)emitted a witness arraymod[], and the top-level division identity could still be satisfied with that witness.- That much only proved
some field-element remainder object participates in this arithmetic relation; it did not yet provethis is the proper base-2^n bigint remainder other gadgets mean when they say mod.
-
That witness still counted as canonical bigint remainder truth
- The live bug sat here.
BigModquietly promotedmod[] satisfies the arithmetic identity alongside q and bintomod[] has already been proved to live in the canonical n-bit limb representation. - In other words, downstream consumers were handed a remainder object that looked like proper bigint output even though the circuit had never sealed the exact representation boundary that made it a proper-form remainder rather than a wider signed-overflow-style object.
- The live bug sat here.
-
That accepted remainder truth still deserved downstream proof authority
- Once the unchecked
mod[]array was read as canonical bigint truth, later gadgets could compare it, hash it, reduce it again, or otherwise reuse it as if the representation question had already been settled. - The dangerous authority question was therefore not only
did the modulo relation hold?butwhy did a remainder object that never earned canonical-form status still get to inherit downstream statement authority?
- Once the unchecked
Why the exploit worked
- The circuit enforced the arithmetic story but skipped part of the representation story. That gap matters in bigint libraries because consumers usually rely on both at once.
- A non-canonical remainder can still satisfy the top-level equation while failing the stronger claim
this is the unique proper-form remainder object that later gadgets expect. - Because quotient limbs were already range-checked, the remaining omission was easy to miss in review: the code looked like it had already done the serious canonicalization work.
- This is why the bug belongs in
bit-length precondition omission, notvalidation-output nonconsumption. The circuit was not ignoring a boolean check; it was failing to prove the width precondition on one of the outputs whose representation downstream code cared about.
Detection and audit pivots
- In bigint gadgets, list every output and ask which of them are guaranteed to be in proper representation versus merely algebraically related to the inputs.
- When one output array has
Num2Bitschecks and a sibling output array does not, assume the unchecked one is suspect until the representation argument is written out explicitly. - Treat quotient, remainder, carry, borrow, and limb-decomposition outputs as separate canonicalization problems; checking one does not make the others safe.
- Audit helper libraries against their own representation taxonomy. If docs distinguish
properfromsigned overflowforms, any missing range proof can flip consumers into the wrong family. - Build adversarial tests for non-canonical outputs that still satisfy the base arithmetic identity; do not stop at honest quotient / remainder examples.
Why this incident matters
- It gives the corpus a clean bigint-output exemplar for the
bit-length precondition omissionfamily, complementing comparator-style cases like Dark Forest. - It preserves an important zk review habit: arithmetic correctness and representation correctness are different claims, and protocols often need both.
- It also shows how library-level zk bugs can sit latent as capability bugs rather than one named exploit: any downstream consumer that trusted the unchecked remainder could have inherited the weaker statement.
Failure-prevention checklist
- Range-check every semantic output object of a bigint helper, not just the output that looks obviously primary.
- Treat
proper representationas a first-class proof obligation wherever downstream gadgets interpret limb arrays semantically. - Mirror constraints across symmetric outputs like quotient and remainder unless there is a written reason the representations may differ.
- Build regression tests for non-canonical limb families that still satisfy the base arithmetic equation.
- Keep library docs explicit about representation families and make the circuit prove the family each output claims to inhabit.
- Review any helper returning multiple arrays for
one is canonicalized, one is merely assignedasymmetry. - During audits, ask not only
is the equation true?but alsois the resulting object in the exact representation the next gadget assumes? - Prefer checked wrapper templates whose outputs are already canonical over helpers that hand later callers a raw arithmetic witness and expect them to remember extra range proofs.
Audit prompts
- Which outputs of this bigint helper are guaranteed to be in proper representation, and where is each guarantee enforced?
- Could a witness satisfy the top-level equation while leaving one output in signed-overflow or otherwise non-canonical form?
- Are downstream consumers comparing, hashing, or reusing an output as if it were canonical even though the helper never proved that representation?
- If quotient limbs are range-checked but remainder limbs are not, what exact argument says the remainder is still uniquely safe?
- Does the library’s spec distinguish representation families more carefully than the circuit actually enforces?
- After a refactor, which tests would fail if one output array silently lost its
Num2Bitschecks while the rest of the arithmetic still verified?
Lessons that should already be standard
- Bigint arithmetic is not finished when the equation balances; the output object still needs a canonical representation proof.
- Quotient-safe does not imply remainder-safe.
- Representation-family bugs are especially dangerous in helper libraries because many downstream circuits inherit the same unchecked assumption at once.
- Asymmetric canonicalization is a review smell. If sibling outputs are supposed to be semantically comparable, they usually need parallel width proofs.
- A circuit that distinguishes
properfromsigned overflowrepresentation in prose must also distinguish them in constraints. - The minimal patch often reveals the true missing invariant: here it was just the absent
Num2Bits(n)loop onmod[].
Internal linkages
- Core recurring pattern: bit-length-precondition-omission.
- Comparator-family cousin: dark-forest-v0-3-rangeproof-lessthan-bit-length-precondition-omission-near-miss-2021-09-30 — same hidden-width family, but on
LessThan(bits)inputs instead of bigint remainder limbs. - Contrast family: validation-output-nonconsumption — there a computed validator result is dropped; here the circuit consumes the arithmetic relation but never proved one output object’s representation-width assumptions.
- Positive-design companion: helper-domain-proof-locality-and-output-canonicalization-discipline — the sharper healthy-side companion when the question is where bigint helper-domain admission, quotient/remainder output sealing, and downstream semantic reuse should have stayed one local proof story.
- Adjacent statement-binding companion: proof-validation-output-consumption-and-statement-binding-discipline — useful when widening the review from
which width proof is missing?to the broader questionwhich helper assumptions and output objects are actually part of the proved statement?
Control surface
-
The leverage sat in
BigMod’s quotient and remainder outputs, the exact point wherelong_divwitness material became proof-constrained objects, and the missingNum2Bits(n)loop onmod[]that left the remainder outside guaranteed canonical form. -
The recurring healthy-side question is simple: did the circuit prove only
some remainder works, or did it provethe remainder object consumers are about to trust is in the proper representation they think they received? -
Sources:
- https://github.com/0xPARC/circom-ecdsa/pull/10
- https://patch-diff.githubusercontent.com/raw/0xPARC/circom-ecdsa/pull/10.patch
- https://api.github.com/repos/0xPARC/circom-ecdsa/pulls/10
- https://hackmd.io/hIfysDw4TtC_6RR4gzdjBw?view
- https://raw.githubusercontent.com/0xPARC/zk-bug-tracker/main/README.md
-
Last reviewed: 2026-08-04 UTC