Repository navigation
core: Reject equal sender blinders in sender encryption - #332
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
The requirement is missing from the public convenience constructors that accept the same blinders.
1 open finding
What changed in this PR
Documents the distinct sender-blinder requirement to prevent unlinkability issues and circuit rejection.
Changes:
- Documents the precondition on
Note::new. - Explains the security rationale on
Sender::encrypt.
| File | Description |
|---|---|
core/src/note.rs |
Adds sender-blinder requirements and rationale. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
d3235a1 to
81029d2
Compare
moCello
left a comment
There was a problem hiding this comment.
APPROVE — The convenience constructors now carry the requirement, and the explanation of the equal-blinder leak agrees with the circuit and with equal_sender_blinders_rejected. The note below answers the question in the description: the guard is worth adding.
| /// This constructor does not enforce distinctness, since comparing the | ||
| /// blinders would branch on secret data, so the caller must pass distinct | ||
| /// blinders — the transaction builder does so by drawing them | ||
| /// independently. |
There was a problem hiding this comment.
NOTE: Yes to the guard that the description offers. Sender::encrypt can compare the two c1 points, which the note publishes, so the check branches on no secret.
Context: c1 = r·G, and G has prime order, so the two c1 points are equal exactly when the blinders are. Both points go into Sender::Encryption and onto the chain, and JubJubAffine equality is constant-time. Both constructors already panic on a zero blinder, so the guard adds a case to an existing panic. It is also the only check that reaches every sender. In rusk's dusk-core, Transaction::new also calls Sender::encrypt for the gas refund sender, and no circuit checks that sender. So "the transaction circuit rejects such a note" holds only for the two output notes.
Fix: After the conversion to tuples, panic when sender_enc_A.0 == sender_enc_B.0, as the zero case does. Then:
- Replace this doc paragraph with the rule and its reason, and add the equal case to
# PanicsonSender::encryptandNote::new. - Add a test beside
note_panics_on_zero_sender_blinder_for_a. - Extend the
[Unreleased]changelog entry that lists the zero-blinder panic.
A debug_assert! does not do this job, because the Makefiles test with --release.
81029d2 to
a8b9aa6
Compare

Follow-up to a review note on #331:
Sender::encryptandNote::newaccepted two equal sender-encryption blinders. Equal blinders give both component ciphertexts the same first coordinatec1, so their second coordinates differ by the sender-fixedA − B, letting a passive observer link the sender (#330).This rejects equal blinders at runtime, as the existing zero-blinder / non-prime-order degenerate cases already do.
Why enforce, not just document: it is the only check that reaches every sender. The transaction circuit covers the two output notes, but
Transaction::newin rusk'sdusk-corealso encrypts a gas-refund sender that no circuit checks — so a doc-only precondition would leave that sender unguarded.It branches on no secret: the guard compares the two published
c1points (sender_enc_A.0 == sender_enc_B.0), which go intoSender::Encryptionand onto the chain, not the secret blinders.c1 = r·GwithGof prime order, so thec1points are equal exactly when the blinders are, andJubJubAffineequality is constant-time. This resolves the original concern that enforcing would branch on secret data. It is a runtimeassert!, not adebug_assert!, since the Makefiles test with--release.Production is unaffected — the transaction builder draws the two blinders independently.