Repository navigation
Move to the released crypto stack - #320
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
The circuit still accepts degenerate ElGamal keys and zero blinders that can expose sender information.
2 open findings
What changed in this PR
Updates Phoenix to the released Dusk cryptography stack and adapts core and circuit behavior to the new APIs.
Changes:
- Upgrades cryptographic dependencies and handles fallible DHKE/ElGamal operations.
- Adds circuit subgroup constraints and regression coverage.
- Introduces extended-CRS handling and current verifier-data fixtures.
| File | Description |
|---|---|
core/src/note.rs |
Handles fallible encryption and DHKE. |
core/src/keys.rs |
Preserves legacy key hashing domain. |
core/CHANGELOG.md |
Documents core behavior and dependency changes. |
core/Cargo.toml |
Upgrades core cryptographic dependencies. |
circuits/src/circuit_impl.rs |
Adds subgroup checks and updated range APIs. |
circuits/src/sender_enc.rs |
Adapts sender encryption to checked point types. |
circuits/tests/common/mod.rs |
Adds extended-CRS loading support. |
circuits/tests/transaction.rs |
Selects CRS by circuit input count. |
circuits/tests/verifier_data.rs |
Validates current and deployed verifier data. |
circuits/tests/torsion.rs |
Tests rejection of a torsion-shifted note key. |
circuits/README.md |
Documents extended-CRS test setup. |
circuits/CHANGELOG.md |
Records circuit security and dependency changes. |
circuits/Cargo.toml |
Upgrades circuit cryptographic dependencies. |
circuits/tests/assets/db21d3c579e4d66fb8a91257bc5a7db3da923aa4a2a7f8e078dc703c5bcbfdd1.vd |
Stores current 1-input verifier data. |
circuits/tests/assets/4e5a7190fca948564710dc9c67f5c2d9acd5274244e74676578d21953e6ede6b.vd |
Stores current 2-input verifier data. |
circuits/tests/assets/3331895f9dd3eb73f2f2836e1079d995a6b47161bcdc0fb5e29f1db97be07997.vd |
Stores current 3-input verifier data. |
circuits/tests/assets/a7ffe261ceeca227e1425de38693cc9355c6c31c72a65b87e65aa6d351d3e902.vd |
Stores current 4-input verifier data. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| circuit.input_notes_info[0] = | ||
| with_torsion_note_pk_p(&circuit.input_notes_info[0]); |
There was a problem hiding this comment.
Done. The regressions now cover each point whose subgroup check a witness can isolate: note_pk, note_pk_p, the sender keys and the output note keys. The signature equations already force the nonces into the subgroup, so no witness can isolate those checks; gate_count.rs pins them instead.
There was a problem hiding this comment.
Update: the nonce checks are gone. Each signature equation sets its nonce equal to u·G + c·pk with pk already in the subgroup, so the nonce lies in the subgroup without a check of its own. gate_count.rs is removed with them, and every remaining check has a regression test.
There was a problem hiding this comment.
At the current head: circuits/tests/torsion.rs:302-314
CAUTION: The fix covers each key. The control skips the steps that the shifted cases rebuild, so a shifted case can be unsatisfiable for a reason other than the subgroup check.
Context: Shifted::Nothing uses TP.signatures and the original input notes. It never goes through sign, sign_double, the hand-copied challenge hashes (including the JJSCHDBL domain), the Note::transparent_stealth rebuild or the nullifier recomputation. The module doc says "the same construction without T proves", but no test proves that construction. If jubjub-schnorr changes its challenge, or one of these rebuild steps drifts, every shifted case still returns CircuitUnsatisfied. A torsion assertion that stops constraining the point, or a key wired through TorsionFreeWitnessPoint::new_unchecked, then goes unnoticed.
Fix: Run the same construction with a zero offset as the control for each variant: re-sign, rebuild the note and recompute the nullifier with the identity in place of T, and assert that this transaction proves before asserting that the shifted one does not.
There was a problem hiding this comment.
Done. Each variant now builds the same transaction twice, recomputing the signatures, the note, its nullifier and the encryptions that depend on the key. With the identity as the offset it must prove; with T it must be unsatisfiable. A drift in a hand-copied step now fails the matching control:
- dropping
JJSCHDBLfails the note-key controls; - reordering the single challenge fails the sender-key control;
- swapping the nullifier inputs fails the
note_pk_pcontrols; - changing the rebuilt note fails the
note_pkcontrol.
On the previous head, the JJSCHDBL drift passed every test. Removing any single key check still fails its regression.
b314fab to
78abc5e
Compare
|
Force-pushed: the subgroup commit now has a regression for every point whose check a witness can isolate, plus a gate-count test for the rest, and the sender-key changelog entry moved into the dependency commit. |
78abc5e to
49e44c0
Compare
|
Force-pushed: the signature nonces no longer get a separate subgroup check, since each signature equation already puts the nonce in the subgroup. The gate-count test goes with them, and the verifier data is regenerated. |
moCello
left a comment
There was a problem hiding this comment.
COMMENT — The subgroup constraints are correct where they enter the circuit, and the nonce argument holds. Three mid-weight findings below ask for changes, none of them gating: one on test integrity, and two on missing tests for the new panics and the new decryption error.
- Issue #313 asks to constrain the signature nonces and does not name the output note keys. The change leaves the nonces unchecked, with an argument, and constrains the output keys as well. The issue and the change disagree on scope.
- The identity output key is not constrained on this branch. The author points to #322 for the fix, so this change alone leaves the gap that the earlier thread raised. Merge #322 together with this change.
- The first commit uses the scope
phoenix:, which is not a canonical scope, and it changes both crates. The commit message gives the reason. Whether that exception stands is a maintainer judgment call.
| circuit.input_notes_info[0] = | ||
| with_torsion_note_pk_p(&circuit.input_notes_info[0]); |
There was a problem hiding this comment.
At the current head: circuits/tests/torsion.rs:302-314
CAUTION: The fix covers each key. The control skips the steps that the shifted cases rebuild, so a shifted case can be unsatisfiable for a reason other than the subgroup check.
Context: Shifted::Nothing uses TP.signatures and the original input notes. It never goes through sign, sign_double, the hand-copied challenge hashes (including the JJSCHDBL domain), the Note::transparent_stealth rebuild or the nullifier recomputation. The module doc says "the same construction without T proves", but no test proves that construction. If jubjub-schnorr changes its challenge, or one of these rebuild steps drifts, every shifted case still returns CircuitUnsatisfied. A torsion assertion that stops constraining the point, or a key wired through TorsionFreeWitnessPoint::new_unchecked, then goes unnoticed.
Fix: Run the same construction with a zero offset as the control for each variant: re-sign, rebuild the note and recompute the nullifier with the identity in place of T, and assert that this transaction proves before asserting that the shifted one does not.
Move both crates to dusk-bls12_381 and dusk-jubjub 0.16, dusk-poseidon 0.44.0, jubjub-elgamal 0.6.0 and jubjub-schnorr 0.7.0, and the circuits to dusk-plonk 0.24.0 and poseidon-merkle 0.11.0. Both manifests move in this commit, as the workspace only resolves with both crates on the same stack. The jubjub-schnorr requirement names the release: 0.7.0-rc.0 also admits the release candidate, which is built on dusk-jubjub 0.15. The changelog drops the entry for the unreleased bump to dusk-jubjub 0.15.2, which never shipped and which 0.16 replaces. Adapt core to the API changes: - `hash_to_scalar` takes a domain; pass `None` for the legacy construction every existing note key is derived with. - `dhke` returns a `CtOption`, empty for an identity or small-order point. Value decryption returns `Error::InvalidEncryption` for such an `R`, and note creation expects a receiver key `A` of prime order. - `Encryption::encrypt` returns a `Result`, rejecting a degenerate encryption. Sender encryption expects it to succeed, which takes a nonzero blinder and key points of prime order. New note tests cover the panics for an identity receiver key `A` and for a zero sender blinder, and the `Error::InvalidEncryption` for an identity `R`. The signature gadgets of jubjub-schnorr 0.7.0 reject an identity key, so the transaction circuit now rejects a sender public key whose `A` or `B` is the identity. The circuits compile again once the commit constraining their points to the prime-order subgroup adapts them to the typed point APIs of dusk-plonk 0.24 and jubjub-schnorr 0.7.0.
Every key the prover supplies to the transaction circuit now goes through Plonk's torsion-free assertion, once, where it enters: the input `note_pk` and `note_pk_p`, and the sender public key. The sender keys feed both the signature and the ElGamal gadgets from that single check. The public output note keys, which the sender key is encrypted to, are checked in-circuit too. The signature nonces need no check of their own: each signature equation sets its nonce equal to `u·G + c·pk`, with the key `pk` in the subgroup. For `note_pk_p`, the check makes the nullifier, which hashes it, depend on the note alone. The new regressions add a small-order component to each checked key: `note_pk_p` for every input variant, and `note_pk`, the sender keys `A` and `B` and the two output note keys. Each recomputes the signatures, the note, its nullifier and the encryptions that depend on the key, so that every other equation holds, and checks that the transaction is unsatisfiable. As a control, the same construction with the identity in place of the small-order component proves. This adapts the circuits to dusk-plonk 0.24 and jubjub-schnorr 0.7.0, whose gadgets take subgroup keys: these checks feed their typed point APIs. Replace the deprecated `component_range::<32>` with `component_range_bits::<64>`, which emits the same gates.
dusk-plonk 0.24 needs 2^17 + 9 powers to compile the 3- and 4-input circuits, three more than the production CRS has. Their transaction and verifier-data tests now compile with the CRS extended by those powers, read from the file `PHOENIX_EXTENDED_CRS` names and checked against its SHA-256, and skip with a notice on stderr when it is not given. The 1- and 2-input tests keep the production CRS. The extended CRS is the production CRS with three more powers of the same secret appended. Until it is hosted next to the production CRS, the README documents the variable.
The verifier-data tests checked new proofs against the verifier data deployed in rusk's transfer contract. The circuits have changed since phoenix-circuits 0.8, and their keys with them, so those checks failed. Add the verifier data of the current 1- to 4-input circuits as test assets, named as rusk-recovery names verifier data, and check that compiling the circuits reproduces it and that a proof verifies against it. The checks against the deployed verifier data now expect the proof to fail, which documents the key change. The extended CRS appends powers of the same secret to the production CRS, so the two compile the 1- and 2-input circuits to the same verifier data. A test checks this when the extended CRS is given.
49e44c0 to
a1030fd
Compare
|
On the review notes: #313 now describes the change, with subgroup checks on every key, the output note keys included, and the nonces covered by the signature equations. This PR merges together with #322 as part of the stack. The first commit keeps the |
moCello
left a comment
There was a problem hiding this comment.
COMMENT — The subgroup checks are sound. Each key that the prover supplies gets one check where it enters, and each signature equation puts its nonce in the subgroup. The torsion regressions now prove a control for each variant, and component_range_bits::<64> emits the same gates as component_range::<32>. Approval waits on one CAUTION: the source of the extended CRS and its plan for production. The other points are NOTEs: three findings and the two lines below.
I agree with Copilot's review that the circuit accepts a zero sender blinder. sender_enc.rs:71-75 puts no constraint on the four blinders. A zero blinder makes c1 the identity and c2 the plain sender key, and both are public inputs. Core now panics on a zero blinder, and the answer for #322 names the output keys only. The gap exists on master too, so it is a NOTE: add a nonzero check to #322, or record it in an issue.
Process: d771600 and fa9a1a1 leave tests red. test_verifier_data_1_2 and test_verifier_data_2_2 fail until a1030fd, and the 3- and 4-input tests fail at circuit compilation until fa9a1a1. The new verifier data and the CRS selection belong in d771600, where the circuits change. This is a NOTE and a maintainer judgment call, together with the scope of 7451b06.
|
|
||
| ## Tests | ||
|
|
||
| The circuit tests compile the 1- and 2-input circuits with the production CRS, which they download to `~/.dusk/rusk/devnet-piecrust.crs` once. The 3- and 4-input circuits need the CRS extended to `2^17 + 9` powers, three more than the production CRS has. Until that file is hosted next to the production CRS, their tests skip with a notice unless `PHOENIX_EXTENDED_CRS` names a local copy, which is checked against its SHA-256: |
There was a problem hiding this comment.
CAUTION: With dusk-plonk 0.24, rusk cannot compile the 3- and 4-input transfer circuits with its production CRS, and only test docs record this.
Context: Since dusk-plonk #975, the prover key needs capacity through degree n + 9 instead of n + 6. rusk-recovery caches the descriptions of TxCircuit<NOTES_TREE_DEPTH, 3> and TxCircuit<NOTES_TREE_DEPTH, 4> in rusk-recovery/src/keys/circuits.rs. check_circuits_cache in rusk-recovery/src/keys.rs compiles them with PUB_PARAMS, the CRS that rusk-profile pins as CRS_17_HASH. That is the file that CRS_HASH pins here.
The doc of EXTENDED_CRS_HASH says that the extended file is the production file with three more powers appended, and the module doc of verifier_data.rs says that they are powers of the same secret. The ceremony output holds 1 << 21 powers, and commit 384608ae of dusk-network/trusted-setup raises its conversion limit to (1 << 17) + 9. That conversion is the likely source of the file. The change does not name it, and nothing shows how to reproduce EXTENDED_CRS_HASH. #311, which the dependency entries of both changelogs link, names the extended CRS for tests only.
Severity-rationale: The CRS requirement is new with this change and blocks the rusk upgrade. The source of the extended file needs a maintainer answer before the file goes to production.
Fix: Open a tracking issue for the extended CRS, and link it from the README and from #311. The issue states that rusk needs the file in production to compile the 3- and 4-input TxCircuit. It names the source of the three powers, for example the conversion at trusted-setup 384608ae, so that anyone can reproduce EXTENDED_CRS_HASH. It also gives the pairing check e([τ^(i+1)]₁, [1]₂) = e([τ^i]₁, [τ]₂) that ties each new power to the production ones. It also says where rusk-recovery gets the file.
There was a problem hiding this comment.
The extended CRS is now tracked in #329, with its source and production plan written down: it is the production CRS plus three powers (144 bytes), from the same ceremony output (trusted-setup contribution 15) converted at (1<<17)+9 — the limit raised in trusted-setup 384608ae — so τ is unchanged and every existing key and proof stays valid. #329 gives the pairing check e([τ^(i+1)]₁, [1]₂) = e([τ^i]₁, [τ]₂) that ties each new power to the production ones, and notes that the node's recovery step must ship the file (old file kept for pre-upgrade nodes).
To avoid rebasing the approved stack below it, the README link to #329 and the three notes below are folded into the release PR #327.
| - Update `jubjub-schnorr` to v0.7.0 [#311] | ||
| - Update `dusk-bls12_381` and `dusk-jubjub` to v0.16, `dusk-plonk` to v0.24.0, | ||
| `dusk-poseidon` to v0.44.0, `poseidon-merkle` to v0.11.0 and | ||
| `jubjub-elgamal` to v0.6.0 [#311] |
There was a problem hiding this comment.
NOTE: One entry holds six dependency updates, the note_pk_p constraint shows twice, and the identity rejection of input note keys has no entry.
Context: The repo rule is "One fact per entry". The released sections list one dependency per line, and jubjub-schnorr already has its own line here. core/CHANGELOG.md:21-22 packs four updates the same way. The Changed entry at lines 12-14 and the Fixed entry at line 23 both record the subgroup check on note_pk_p. verify_signature_double of jubjub-schnorr 0.7.0 rejects an identity note_pk or note_pk_p, and 0.7.0-rc.0 accepted both. A note whose note_pk is the identity was spendable with the secret key zero, and now it is unspendable. The changelog records the identity rejection for the sender key only. The new link definitions [#313], [#314], [#311] break the descending order of the block.
Fix: Write one entry per dependency in both changelogs. Remove note_pk_p from the Changed entry and keep the Fixed entry. Add an entry that the circuit rejects an identity input note_pk or note_pk_p. Sort the new link definitions.
There was a problem hiding this comment.
Addressed in #327: one entry per dependency in both changelogs, note_pk_p dropped from the Changed entry (the Fixed entry stays), an entry added for the circuit rejecting an identity input note_pk/note_pk_p, and the new link definitions sorted.
| let Some(pp) = crs::<3>("test_transfer_circuit_3_2") else { | ||
| return; | ||
| }; |
There was a problem hiding this comment.
NOTE: Without PHOENIX_EXTENDED_CRS, the 3- and 4-input tests return before any assertion and report ok, which is the CI case.
Context: The same early return applies to test_transfer_circuit_4_2, and through assert_verifier_data to test_verifier_data_3_2 and test_verifier_data_4_2. Thus CI never compares VD_3_2 or VD_4_2 with a compiled circuit. The stderr notice does not change the result that the harness reports. torsion.rs compiles and proves the 3- and 4-input circuits with PublicParameters::setup(1 << 17, ..), so the transaction tests do not need the extended file. Only the verifier-data tests need the exact CRS.
Fix: Compile test_transfer_circuit_3_2 and test_transfer_circuit_4_2 with a generated SRS, as torsion.rs does, so that they run in CI. Keep the skip on the verifier-data tests only.
There was a problem hiding this comment.
Addressed in #327: test_transfer_circuit_3_2 and _4_2 now prove and verify against a generated SRS, as torsion.rs does, so they run in CI; only the verifier-data tests keep the production-CRS skip.
| // An identity or small-order `R` yields no shared secret. | ||
| let shared_secret = Option::<JubJubAffine>::from(dhke(vk.a(), R)) | ||
| .ok_or(Error::InvalidEncryption)?; |
There was a problem hiding this comment.
NOTE: The doc of StealthAddress::from_bytes_checked still gives two reasons that this change makes false.
Context: The doc in core/src/stealth_address.rs says "The transaction circuit constrains membership of neither point". The circuit now constrains note_pk to the prime-order subgroup, and verify_signature_double rejects an identity note_pk. The doc also says that "an identity R collapses the value-encryption key dhke(vk.a, R) to a shared secret anyone can derive". With dusk-jubjub 0.16, dhke gives no secret for that R, and these lines return Error::InvalidEncryption. The file is not in the diff, but this change makes its doc out of date.
Fix: Rewrite that paragraph of the doc. State that value decryption returns Error::InvalidEncryption for an identity R, and that the circuit rejects a torsion or identity input note_pk. Keep the reasons that still hold, for example the identity output note_pk until #322 merges.
There was a problem hiding this comment.
Addressed in #327: the from_bytes_checked doc now states that the circuit constrains the input note_pk and rejects a torsion or identity input note_pk, that an identity R is rejected at value decryption with Error::InvalidEncryption, and keeps the output-note_pk reason.
|
Thanks for the thorough pass. The one CAUTION — the extended CRS source and production plan — is now written down in #329 (summary on the README thread), which should clear the gate. The README/changelog/test/doc NOTEs are folded into the release PR #327 to avoid rebasing the approved stack below this one; each thread reply points to where it landed. From the review body: the sender-blinder gap (pre-existing and self-contained) is tracked in #330 for the 2.0 circuit revisit, and the commit-structure point I left as the maintainer judgment call you flagged — splitting the circuit change from its verifier-data regeneration is what leaves the intermediate commits red. Re-requesting your review. |


Moves phoenix-core and phoenix-circuits to the released dusk-bls12_381 0.16, dusk-jubjub 0.16, dusk-plonk 0.24, dusk-poseidon 0.44, poseidon-merkle 0.11, jubjub-elgamal 0.6 and jubjub-schnorr 0.7.
hash_to_scalartakes a domain (Nonekeeps the existing note keys),dhkereturns an option, and ElGamal encryption returns aResult. Note creation panics when an encryption degenerates, for an identity receiver keyAor a zero sender blinder, and value decryption returnsError::InvalidEncryptionfor anRthat is the identity or of small order.u·G + c·pk, with the keypkin the subgroup, so the nonce is in the subgroup too. The signature gadgets of jubjub-schnorr 0.7 also reject identity keys, so a sender key with an identityAorBno longer satisfies the circuit.note_pk,note_pk_p, the sender keys and the output note keys), recompute what depends on the key, and check that the transaction is unsatisfiable. As a control, the same construction with the identity in place of that component proves.Aand a zero sender blinder, and theError::InvalidEncryptionfor an identityR.PHOENIX_EXTENDED_CRSand skip without it, as they do in CI.Both manifests move in the first commit, because the workspace only resolves with both crates on the same stack. The circuits compile again from the second commit on. The core changelog drops the entry for the unreleased bump to dusk-jubjub 0.15.2, which 0.16 replaces.
Resolves #311
Resolves #313