13 - Cofactors, subgroups, canonical encodings
1. Why this chapter exists
Every Zcash bug involving curve points eventually reduces to one of
three primitives: a cofactor was not cleared, a subgroup membership
was not checked, or a non-canonical encoding was accepted. The bugs
are mathematically subtle and the engineering invariants are crisp;
the type system can carry the invariants when the deserialiser sets
them up correctly. A contributor who touches any from_bytes path
for a Jubjub or BLS12-381 point must understand each clause of ZIP
216 and the
read_value_commitment,
read_cmu,
and
read_rk
helpers in
zcash_primitives/src/transaction/components/sapling.rs.
Read this chapter, then re-read
chapter 12 with new eyes.
2. Definitions
Group order, prime-order subgroup, cofactor
Let be an elliptic curve over a prime field. Its order (number of points including the identity) is
where is a large prime and is a small integer called the cofactor. The unique subgroup of order is the prime-order subgroup, denoted . We work cryptographically in .
Definition (torsion). Points in are -torsion: small-order points whose orders divide .
| Curve | Field | Order | Cofactor | Prime size |
|---|---|---|---|---|
| Jubjub | (BLS12-381 scalar field) | 8 | 252 bits | |
| BLS12-381 | non-trivial | 255 bits | ||
| BLS12-381 | huge | 255 bits | ||
| Pallas | 1 | 255 bits | ||
| Vesta | 1 | 255 bits | ||
| secp256k1 | 1 | 256 bits |
Memorise: Jubjub has cofactor 8. Pallas and Vesta have cofactor 1. BLS12-381 have non-trivial subgroup-membership concerns even though they are pairing groups; specific checks are required.
Definition (canonical encoding)
A field element is canonically encoded as the little-endian byte string of the unique integer in representing . A non-canonical encoding is any byte string representing an integer but congruent to modulo .
Definition (small-subgroup attack)
Let Alice hold a secret and compute for an adversarial with of small order . Then
If anything derived deterministically from leaks, the adversary recovers . Repeating with different small-order for each prime factor of recovers . For Jubjub, , so each call leaks 3 bits of .
Invariant (ZIP 216, canonical Jubjub element)
A 32-byte sequence is a valid encoding of a Jubjub point if and only if:
- Strip the high bit of byte 31 as parity ; the remaining 255 bits encode canonically with .
- Solve . If or no solution exists, reject.
- Choose if , else . If , the parity bit must be for canonical encoding.
- The point must lie in .
Pre-NU5 code accepted some non-canonical encodings; ZIP 216 codifies the strict consensus rule.
3. The code
3.1 The Sapling deserialiser pattern
The Sapling component-reader in
zcash_primitives/src/transaction/components/sapling.rs
shows the canonical pattern. Every Jubjub-point field has a
dedicated helper that enforces both canonical encoding and
subgroup membership (or notes that the latter is enforced later in
the verifier):
loading...
The call ValueCommitment::from_bytes_not_small_order is exactly
the ZIP 216 path: decode canonically, then enforce
SubgroupPoint::try_from. The wrapping CtOption carries failure
constant-time; the caller is responsible for converting None into
an InvalidInput error after the constant-time check completes.
The read_cmu helper omits the small-order test because the value
is already a field element (the extracted -coordinate), not a
point:
loading...
The read_rk helper defers the small-order check to
SaplingVerificationContext::check_spend, which runs after the
sighash and proof have been bound; canonical encoding is still
enforced here so two distinct wire encodings cannot produce the
same rk:
loading...
3.2 Cofactor-clearing mitigations
Three approaches, used in combination depending on the curve:
- Multiply by the cofactor on receipt ("cofactor clearing"): replace any received with . The result is in by construction.
- Subgroup membership check: verify . For Edwards curves this is the "small-order check"; for Weierstrass curves it is the explicit "scalar mul by returns identity" test.
- Torsion-free encoding: encode and decode only the prime-order subgroup, so off-subgroup points cannot be expressed on the wire.
In librustzcash and dependencies, the idiomatic patterns are:
// Jubjub: decode then explicitly convert
let ext = ExtendedPoint::from_bytes(&bytes).into_option()?;
let sub = SubgroupPoint::try_from(ext)?; // subgroup check
// Pallas: cofactor is 1, but canonical encoding still required
let p = pallas::Affine::from_bytes(&bytes).into_option()?;
A point whose type is SubgroupPoint is guaranteed by the type
system to lie in ; this is a load-bearing type
invariant.
3.3 Why canonical encoding matters
If a parser accepts non-canonical encodings:
- The hash of the encoding is no longer a function of the value; two valid byte strings can hash to different things while representing the same group element. This breaks domain separation and Fiat-Shamir.
- Two transactions can be byte-distinct but semantically identical. TxId uniqueness fails. Memory-pool deduplication is undermined.
The ff crate's PrimeField::from_repr enforces canonical
encoding for and returns CtOption. Code should
always check this:
let x = pallas::Base::from_repr(bytes).into_option()
.ok_or(Error::NonCanonical)?;
unwrap() on a CtOption in any path that processes
attacker-controlled bytes is a code smell.
3.4 The full set of "untrusted points" in the workspace
Whenever a point is read from external bytes, both canonicalisation and a subgroup check (for cofactor curves) are required.
Sapling (Jubjub).
cvvalue commitments in Spend/Output descriptions.rkre-randomised spend keys.epkephemeral public keys.- note-commitment -coordinates.
akin viewing key encoding.- The anchor: a -coordinate plus parity decoded at Merkle root level.
Orchard (Pallas).
cv,rk,epk,cmx.- Pallas has cofactor 1, so only canonical encoding must be checked; subgroup membership is automatic.
BLS12-381 ().
- Groth16 proof elements , , .
For BLS12-381 subgroup membership the obvious "multiply by " is
expensive; the bls12_381 crate exposes is_torsion_free which
uses an efficient endomorphism-based check. This must be invoked on
every proof element.
secp256k1 (transparent).
- Public keys (canonical compressed encoding).
- ECDSA signatures (canonical low- form, per ZIP 215).
3.5 The "extract" operations
Several Zcash primitives publish only the -coordinate of a Jubjub point (or -coordinate for Pallas), not the full point. Examples:
- is the -coordinate of the note commitment.
- The Merkle hash output is extracted from a Pedersen-hash result.
- The Orchard nullifier is the -coordinate of a specific point.
This is fine for uniqueness: two distinct subgroup points share
at most two equal -coordinates, namely and .
For commitment purposes that is enough. When reconstructing a full
point from a published coordinate, the parity must be carried
separately; extract_p and extract_p_bottom are the relevant
functions.
3.6 Twisted Edwards arithmetic and the order-2 torsion
Jubjub is a twisted Edwards curve
Two facts:
- Strongly unified addition formula: the same formula computes for all including (doubling) and (returns identity). This is one reason Edwards form is preferred for in-circuit work.
- Order-2 torsion: the points and are notable. has order 2 and is the annoying low-order point. A "point" decoded as passes basic curve-equation checks but is a 2-torsion element outside .
Pallas/Vesta are short Weierstrass curves . Doubling and addition use distinct formulas; the incomplete addition formula breaks when adding a point to itself or to its negative. Sinsemilla (chapter 16) goes to great lengths to avoid these edge cases in-circuit.
3.7 Constant-time considerations for subgroup checks
A subgroup check must be constant-time (no early termination) to
avoid timing leaks. Both jubjub and bls12_381 crates implement
it that way. Helper functions that conditionally short-circuit on
"probably not in subgroup" tests are unsafe in any path touching
secret-dependent inputs.
When propagating failures, use CtOption and subtle::Choice
rather than Result to keep the failure path constant-time. The
codebase follows this consistently in low-level libraries;
higher-level wallet code is more relaxed because the inputs are
already constant-time-validated by the time they reach there.
3.8 The ExtendedPoint vs SubgroupPoint pattern
The jubjub crate splits:
ExtendedPoint: an arbitrary Jubjub point in (X:Y:T:Z) coords.SubgroupPoint: a point provably in .
From<SubgroupPoint> for ExtendedPoint is total. The other
direction requires TryFrom, returning
Option<SubgroupPoint> after a subgroup check.
ExtendedPoint::from_bytes(bytes) returns an ExtendedPoint; the
subgroup check is not automatic.
The mirror pattern in
pasta_curves is that
pallas::Point is the full type (cofactor 1, equivalent to a
subgroup point already), with from_bytes enforcing canonical
encoding.
4. Failure modes
- Missing subgroup check on
epk. Accepting an off-subgroup during note encryption lets the sender exfiltrate per output. See chapter 12 section 5 and ZIP 216. - Missing canonical-encoding check. Two distinct byte sequences representing the same group element will hash differently under any wire-byte-keyed hash, breaking transaction IDs and Fiat-Shamir transcripts.
unwrap()onCtOptionover attacker bytes. Even if the failure mode is "return None", callingunwrap()re-introduces a branch on a secret-derived boolean and can panic on adversarial input. Useinto_option().ok_or(...).- Conflating cofactor clearing with subgroup checking.
Multiplying by moves a point into but does not
reject the original input; for consensus parsing both the
validity of the source bytes and the resulting subgroup
membership matter. Use the explicit
SubgroupPoint::try_frompattern. - Reading a 2-torsion point as a public key. Decoding on Jubjub passes the curve-equation check and is a valid group element but has order 2. Any KDF derived from leaks the LSB of .
- Variable-time inversion or doubling formulas. Implementing the curve operations with non-constant-time inversion (e.g. Euclidean) leaks scalar bits via timing; chapter 14 covers this.
5. Spec pointers
- ZIP 216 (Require Canonical Jubjub Point Encodings): the formal consensus rule cited in Definition 2.4 above; clause 4 (subgroup membership) is the load-bearing part.
- ZIP 215 (Explicitly Defining and Modifying Ed25519 Validation Rules): the analogous rules for secp256k1 signature canonicalisation.
- Zcash Protocol Specification, section 5.4.9 (Group Hash into Jubjub): the hash-to-curve construction the trapdoor-free generators rely on.
- Zcash Protocol Specification, section 5.4.8.5 (Group Hash into Pallas): same for Pallas.
- Hopwood, Bowe et al., Sapling design notes:
the historical rationale for Jubjub's cofactor and the small-
order constraint on
cvandrk.
6. Exercises
- Trace a subgroup check. In
zcash_primitives/src/transaction/components/sapling.rs, identify the difference in policy betweenread_value_commitment(line 90) andread_rk(line 145). Why does one enforce small-order rejection here and the other defer it to the verification context? - Grep the workspace. Run
grep -r "is_torsion_free\|SubgroupPoint::try_from\|into_option" --include='*.rs'andgrep -r "from_repr\|from_bytes_unchecked\|CtOption" --include='*.rs'. List every site that decodes a curve point without one of these checks. Mark any such site as a candidate audit finding. - Modify and test. In a checkout, add a unit test under
zcash_primitivesthat constructs a 32-byte string encoding the Jubjub point (a 2-torsion element) and asserts thatread_value_commitmentrejects it. The test should pass (the read returns anInvalidInputerror). Cite the publicValueCommitment::from_bytes_not_small_orderentry point as the inner check.
Answers in the code
- Sapling point readers (canonical + small-order policy):
zcash_primitives/src/transaction/components/sapling.rs#L87-L150. - The
SubgroupPoint/ExtendedPointtype pattern lives in the externaljubjubcrate; see its docs for thetry_fromsignature. - The Pallas analogue (
pallas::Affine::from_bytes) lives inpasta_curves.
7. Further reading
- Chapter 14: the constant-time discipline these checks must respect.
- Chapter 16: how the trapdoor-free generators on Jubjub are derived, with the cofactor-clearing step at the end.
- Chapter 12: the subgroup-omission and non-canonical-encoding incidents that motivated ZIP 215 and ZIP 216.