You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Circuit does not constrain sender-encryption blinders to be nonzero #330
The transaction circuit does not constrain the four sender-encryption blinders to be nonzero (sender_enc.rs). With a zero blinder the sender ciphertext degenerates: c1 becomes the identity and c2 the plain sender key, both of which are public inputs.
This is latent and self-contained:
Honestly built transactions cannot hit it — core rejects a zero blinder when constructing the encryption.
A prover choosing a zero blinder only degrades the privacy of its own sender data; it is not a consensus issue and does not affect any other party.
The gap predates this work (it is present on master).
The natural place to close it is the circuit revisit for the Phoenix return in 2.0, alongside the other degenerate-sender-ciphertext handling, where a nonzero-blinder constraint can be added and the verifier data regenerated once. #322 added the identity-key rejection for the output keys; this tracks the remaining blinder case.
Update — also covers the equal-blinder case (addressed in #331). Beyond the zero blinder above, an output note's two component blinders being equal is a sibling degenerate case: equal blinders give both sender ciphertexts the same c1, so their c2 differ by the sender-fixed A − B, letting a passive observer fingerprint and link the sender. #331 constrains each blinder to be nonzero and the two of a note to be distinct, enforced in-circuit, and lands ahead of the 2.0 revisit — before the 0.36 keys are finalized, to avoid a second verifier-data rotation.
A security review pass surfaced a second degenerate-blinder case next to the zero-blinder one tracked here. It is also privacy-only (no consensus impact), and the fix lives in the same place.
Mechanism. The two ElGamal component encryptions of the sender key share one note key but must use independent nonces. The blinders are supplied by the caller and neither the core API nor the circuit checks that they differ:
Sender::encrypt takes blinder: &[JubJubScalar; 2] and applies the two independently, with no distinctness check — core/src/note.rs:598. Note::new likewise passes sender_blinder straight through — core/src/note.rs:117.
assert_sender_enc constrains each encryption but never blinder_A != blinder_B — circuits/src/sender_enc.rs:102 (the two encrypt_unchecked calls at :111 and :119).
ElGamal has c1 = rG, so with r_A == r_B = r the two ciphertexts are C_A = (rG, A + rN) and C_B = (rG, B + rN). Any passive observer then:
detects the reuse (both c1 points are equal), and
computes C_{B,2} − C_{A,2} = B − A.
B − A is a stable value per sender public key (A, B). An observer can match it against any known address, or simply use it to link every transaction that reuses the nonce this way — even when a fresh r is chosen per transaction, as long as the two components within a transaction share it.
Exploitability. Only reachable if the transaction builder passes two equal blinders (e.g. [r; 2], a cloned scalar, or the same deterministic derivation for both components). Independent sampling of the two blinders avoids it entirely — worth confirming in the wallet-core transaction builder.
Fix (same home as the nonzero-blinder constraint).
Cheap, immediate: sample both blinders internally, or reject blinder_A == blinder_B in Sender::encrypt.
Belt-and-suspenders: add an r_A != r_B constraint per output during the 2.0 circuit revisit, alongside the nonzero-blinder constraint, when the verifier data is regenerated once.
Activity
HDauven commentedon Oct 13, 2026
Sibling case: equal (nonzero) sender-encryption blinders leak a stable sender fingerprint
A security review pass surfaced a second degenerate-blinder case next to the zero-blinder one tracked here. It is also privacy-only (no consensus impact), and the fix lives in the same place.
Mechanism. The two ElGamal component encryptions of the sender key share one note key but must use independent nonces. The blinders are supplied by the caller and neither the core API nor the circuit checks that they differ:
Sender::encrypttakesblinder: &[JubJubScalar; 2]and applies the two independently, with no distinctness check —core/src/note.rs:598.Note::newlikewise passessender_blinderstraight through —core/src/note.rs:117.assert_sender_encconstrains each encryption but neverblinder_A != blinder_B—circuits/src/sender_enc.rs:102(the twoencrypt_uncheckedcalls at:111and:119).ElGamal has
c1 = rG, so withr_A == r_B = rthe two ciphertexts areC_A = (rG, A + rN)andC_B = (rG, B + rN). Any passive observer then:c1points are equal), andC_{B,2} − C_{A,2} = B − A.B − Ais a stable value per sender public key(A, B). An observer can match it against any known address, or simply use it to link every transaction that reuses the nonce this way — even when a freshris chosen per transaction, as long as the two components within a transaction share it.Exploitability. Only reachable if the transaction builder passes two equal blinders (e.g.
[r; 2], a cloned scalar, or the same deterministic derivation for both components). Independent sampling of the two blinders avoids it entirely — worth confirming in the wallet-core transaction builder.Fix (same home as the nonzero-blinder constraint).
blinder_A == blinder_BinSender::encrypt.r_A != r_Bconstraint per output during the 2.0 circuit revisit, alongside the nonzero-blinder constraint, when the verifier data is regenerated once.circuits: Constrain sender-encryption blinders to be nonzero and dist…
circuits: Constrain sender-encryption blinders to be nonzero and dist…