Repository navigation
S5: add the babysubgroup tag and require it in EscalarMulAny - #152
Draft
OBrezhniev wants to merge 1 commit into
Draft
OBrezhniev wants to merge 1 commit into
OBrezhniev wants to merge 1 commit into
Conversation
OBrezhniev
force-pushed
the
fix/circomlib_buses_review
branch
from
September 29, 2026 18:39
b85175e to
7198ad7
Compare
EscalarMulAny stated in prose that its point must be in the prime order
subgroup. Nothing enforced it, and the consequence was concrete: for the
order-2 point (0,-1) it returned the identity, silently wrong, because
its zero-point substitution keys on pin.x == 0, which within the
subgroup is true of the identity alone and outside it is not.
A tag cannot ride through arithmetic here: a declared output carries
exactly the tags it declares, so BabyAdd's {babyedwards} output strips
anything extra (checked: a {babyedwards, babysubgroup} value passed
through a {babyedwards} output no longer satisfies a consumer requiring
both). The tag is therefore granted at the source and required at the
point of use.
Granting, in babyjub.circom:
BabySubgroupClear() 8*pin. The curve has 8*r points with r prime, so
this lands in the subgroup for any point of the
curve. It is the cofactor clearing the EdDSA
verifiers already did with three BabyDbl. 18
constraints.
BabySubgroupCheck() pin unchanged. p is in the subgroup iff p = 8*q
for some point q of the curve, so the prover
supplies q = (8^-1 mod r)*p as a witness, computed
by a witness-time double-and-add, and the template
checks q is on the curve and 8*q == p. No witness
exists otherwise. 21 constraints, against a full
scalar multiplication for checking r*p directly.
Requiring: EscalarMulAny's pin is {babyedwards, babysubgroup}. A caller
passing an untagged point gets error[T3001]: missing tags required by
input signal. Its docstring now says what depends on the property.
The three EdDSA verifiers use BabySubgroupClear in place of their three
BabyDbl and so obtain the tag for free. Their small-order-key rejection
tested (4*A).x == 0 on an intermediate the template no longer exposes;
it now tests (8*A).x == 0 on the cleared point. Same predicate: (4*A).x
== 0 iff 4*A is the identity or (0,-1) iff 8*A is the identity, and on
the tagged point x == 0 is exact. Constraint counts are unchanged:
eddsapedersen_test 17671, eddsapedersen_old_test 17703, eddsaposeidon
7373. escalarmulany_test gains the 21 of the check in its wrapper.
Tests: babysubgroup.js, 13 cases. The facts the templates rest on are
asserted against circomlibjs first (8*r points; (0,-1) has order 2 and
is outside the subgroup; the identity is inside; the inv8 witness
works). Check accepts G, 2G, (r-1)G and the identity unchanged and
rejects (0,-1) and G + (0,-1); Clear returns 8*p in the subgroup for
points in and out of it and sends (0,-1) to the identity; both verifiers
still reject a key of order 2, Poseidon only when enabled. Dropping
q8 === pin fails exactly the two rejection cases. Full suite 131
passing. The correctness fuzz's (0,-1) row, previously two wrong
answers, is now three rejections; 807 legitimate cases, 0 wrong.
One thing the tests are built around: circomlibjs's wasm field never
returns from F.sqrt on a non-residue. An earlier version of this test
searched for random points of the curve that way and hung the whole
suite. Off-subgroup points are now built as G + (0,-1), with no roots.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D1w729xzwemQigkf3PCisD
OBrezhniev
force-pushed
the
fix/circomlib_buses_review
branch
from
September 29, 2026 20:08
7198ad7 to
d5f158d
Compare
OBrezhniev
force-pushed
the
fix/babysubgroup-tag
branch
from
September 29, 2026 20:08
5e57cd7 to
4bdb5d3
Compare
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.
Stacked on #149 (
fix/circomlib_buses_review); this PR shows only its own commits. Independent of the other stacked PRs unless stated. Full suite passes on the branch.EscalarMulAnywith the order-2 point (0,−1) returned the identity, silently wrong; its docstring said in prose that the point must be in the subgroup. Two granting templates (BabySubgroupClear, 8·p, 18 constraints;BabySubgroupCheck, p unchanged, 21 constraints via a witness q with 8q = p) and a requirement onEscalarMulAny. EdDSA verifiers get the tag for free from the cofactor clearing they already did; their constraint counts are unchanged. Merges clean with option A; one trivial README table conflict with option B.Add the babysubgroup tag and require it in EscalarMulAny
EscalarMulAny stated in prose that its point must be in the prime order
subgroup. Nothing enforced it, and the consequence was concrete: for the
order-2 point (0,-1) it returned the identity, silently wrong, because
its zero-point substitution keys on pin.x == 0, which within the
subgroup is true of the identity alone and outside it is not.
A tag cannot ride through arithmetic here: a declared output carries
exactly the tags it declares, so BabyAdd's {babyedwards} output strips
anything extra (checked: a {babyedwards, babysubgroup} value passed
through a {babyedwards} output no longer satisfies a consumer requiring
both). The tag is therefore granted at the source and required at the
point of use.
Granting, in babyjub.circom:
BabySubgroupClear() 8pin. The curve has 8r points with r prime, so
this lands in the subgroup for any point of the
curve. It is the cofactor clearing the EdDSA
verifiers already did with three BabyDbl. 18
constraints.
BabySubgroupCheck() pin unchanged. p is in the subgroup iff p = 8q
for some point q of the curve, so the prover
supplies q = (8^-1 mod r)p as a witness, computed
by a witness-time double-and-add, and the template
checks q is on the curve and 8q == p. No witness
exists otherwise. 21 constraints, against a full
scalar multiplication for checking rp directly.
Requiring: EscalarMulAny's pin is {babyedwards, babysubgroup}. A caller
passing an untagged point gets error[T3001]: missing tags required by
input signal. Its docstring now says what depends on the property.
The three EdDSA verifiers use BabySubgroupClear in place of their three
BabyDbl and so obtain the tag for free. Their small-order-key rejection
tested (4A).x == 0 on an intermediate the template no longer exposes;
it now tests (8A).x == 0 on the cleared point. Same predicate: (4A).x
== 0 iff 4A is the identity or (0,-1) iff 8*A is the identity, and on
the tagged point x == 0 is exact. Constraint counts are unchanged:
eddsapedersen_test 17671, eddsapedersen_old_test 17703, eddsaposeidon
7373. escalarmulany_test gains the 21 of the check in its wrapper.
Tests: babysubgroup.js, 13 cases. The facts the templates rest on are
asserted against circomlibjs first (8r points; (0,-1) has order 2 and
is outside the subgroup; the identity is inside; the inv8 witness
works). Check accepts G, 2G, (r-1)G and the identity unchanged and
rejects (0,-1) and G + (0,-1); Clear returns 8p in the subgroup for
points in and out of it and sends (0,-1) to the identity; both verifiers
still reject a key of order 2, Poseidon only when enabled. Dropping
q8 === pin fails exactly the two rejection cases. Full suite 131
passing. The correctness fuzz's (0,-1) row, previously two wrong
answers, is now three rejections; 807 legitimate cases, 0 wrong.
One thing the tests are built around: circomlibjs's wasm field never
returns from F.sqrt on a non-residue. An earlier version of this test
searched for random points of the curve that way and hung the whole
suite. Off-subgroup points are now built as G + (0,-1), with no roots.
🤖 Generated with Claude Code
https://claude.ai/code/session_01D1w729xzwemQigkf3PCisD