Skip to content

Circuit does not constrain sender-encryption blinders to be nonzero #330

Closed

Description

@HDauven

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.

Activity

  1. HDauven commented on Oct 13, 2026

    @HDauven
    MemberAuthor

    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::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:

    1. detects the reuse (both c1 points are equal), and
    2. 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions