diff --git a/circuits/CHANGELOG.md b/circuits/CHANGELOG.md index 88e2881..759b7e3 100644 --- a/circuits/CHANGELOG.md +++ b/circuits/CHANGELOG.md @@ -9,8 +9,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Constrain `note_pk`, `note_pk_p`, the sender public key and the output note + public keys to the prime-order subgroup where they enter the transaction + circuit [#313] +- 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] - Raise the MSRV to Rust 1.96.1 [#302] +### Fixed + +- Reject an input `note_pk_p` with a small-order component [#313] +- Reject a sender public key whose `A` or `B` is the identity [#314] + ## [0.8.1] - 2026-06-15 ### Changed @@ -150,6 +162,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Update `poseidon-merkle` to v0.6 [#179] +[#313]: https://github.com/dusk-network/phoenix/issues/313 +[#314]: https://github.com/dusk-network/phoenix/issues/314 +[#311]: https://github.com/dusk-network/phoenix/issues/311 [#302]: https://github.com/dusk-network/phoenix/issues/302 [#280]: https://github.com/dusk-network/phoenix/issues/280 [#255]: https://github.com/dusk-network/phoenix/issues/255 diff --git a/circuits/Cargo.toml b/circuits/Cargo.toml index 114b33e..cc2a5e2 100644 --- a/circuits/Cargo.toml +++ b/circuits/Cargo.toml @@ -14,15 +14,15 @@ edition.workspace = true [dependencies] dusk-bytes = "0.1" phoenix-core = { version = "0.35.1", default-features = false, path = "../core" } -dusk-bls12_381 = { version = "0.14", default-features = false } -dusk-jubjub = { version = "0.15", default-features = false } -poseidon-merkle = "0.9.0-rc.0" -dusk-poseidon = "0.42.0-rc.0" -jubjub-schnorr = "0.7.0-rc.0" -jubjub-elgamal = "0.5.0" +dusk-bls12_381 = { version = "0.16", default-features = false } +dusk-jubjub = { version = "0.16", default-features = false } +poseidon-merkle = "0.11.0" +dusk-poseidon = "0.44.0" +jubjub-schnorr = "0.7.0" +jubjub-elgamal = "0.6.0" rkyv = { version = "0.7", default-features = false, optional = true } bytecheck = { version = "0.6", default-features = false, optional = true } -dusk-plonk = { version = "0.22.1", default-features = false, optional = true } +dusk-plonk = { version = "0.24.0", default-features = false, optional = true } [dev-dependencies] rand = { version = "0.8", default-features = false, features = ["std_rng"] } diff --git a/circuits/README.md b/circuits/README.md index 8e02993..f740a08 100644 --- a/circuits/README.md +++ b/circuits/README.md @@ -9,3 +9,11 @@ This library contains the implementation of the Phoenix-circuits, to prove, in z 3. Nullification: the nullifier is calculated correctly. 4. Minting: the value commitment for the newly minted notes are computed correctly. 5. Balance integrity: the sum of the values of all spent notes is equal to the sum of the values of all minted notes + the gas fee + a deposit, where a deposit refers to funds being transfered to a contract. + +## 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: + +```sh +PHOENIX_EXTENDED_CRS=/path/to/devnet-piecrust-plus9.crs make test +``` diff --git a/circuits/src/circuit_impl.rs b/circuits/src/circuit_impl.rs index 2220cf2..a4f70a3 100644 --- a/circuits/src/circuit_impl.rs +++ b/circuits/src/circuit_impl.rs @@ -7,9 +7,10 @@ //! The [`Circuit`] trait implementation on [`TxCircuit`]. use dusk_bls12_381::BlsScalar; -use dusk_jubjub::{GENERATOR, GENERATOR_NUMS}; +use dusk_jubjub::{GENERATOR, GENERATOR_NUMS, JubJubExtended}; use dusk_plonk::prelude::{ - Circuit, Composer, Constraint, Error as PlonkError, Witness, WitnessPoint, + Circuit, Composer, Constraint, Error as PlonkError, + TorsionFreeWitnessPoint, Witness, WitnessPoint, }; use dusk_poseidon::{Domain, HashGadget}; use jubjub_schnorr::gadgets; @@ -65,7 +66,7 @@ impl Circuit for TxCircuit { signature_u, signature_r, signature_r_p, - ) = input_note_info.append_to_circuit(composer); + ) = input_note_info.append_to_circuit(composer)?; // Verify: 2. Ownership gadgets::verify_signature_double( @@ -87,7 +88,7 @@ impl Circuit for TxCircuit { composer.assert_equal(computed_nullifier, nullifier); // Perform a range check ([0, 2^64 - 1]) on the value of the note - composer.component_range::<32>(value); + composer.component_range_bits::<64>(value); // Sum up all the input note values let constraint = Constraint::new() @@ -133,13 +134,13 @@ impl Circuit for TxCircuit { // Append the witnesses to the circuit let value = composer.append_witness(output_note_info.value); // Append the value-commitment as public input - let expected_value_commitment = - composer.append_public_point(output_note_info.value_commitment); + let expected_value_commitment = composer + .append_public_point(output_note_info.value_commitment)?; let value_blinder = composer.append_witness(output_note_info.value_blinder); // Perform a range check ([0, 2^64 - 1]) on the value of the note - composer.component_range::<32>(value); + composer.component_range_bits::<64>(value); // Sum up all the output note values let constraint = @@ -156,7 +157,7 @@ impl Circuit for TxCircuit { // Verify: 4. Minting composer.assert_equal_point( expected_value_commitment, - computed_value_commitment, + computed_value_commitment.into(), ); } @@ -206,28 +207,38 @@ impl Circuit for TxCircuit { } } +/// The witnesses [`InputNoteInfo::append_to_circuit`] appends: `note_pk`, +/// `note_pk_p`, `note_type`, `pos`, `value`, `value_blinder`, `nullifier` and +/// the signature's `u`, `R` and `R'`. +type InputNoteWitnesses = ( + TorsionFreeWitnessPoint, + TorsionFreeWitnessPoint, + Witness, + Witness, + Witness, + Witness, + Witness, + Witness, + WitnessPoint, + WitnessPoint, +); + impl InputNoteInfo { fn append_to_circuit( &self, composer: &mut Composer, - ) -> ( - WitnessPoint, - WitnessPoint, - Witness, - Witness, - Witness, - Witness, - Witness, - Witness, - WitnessPoint, - WitnessPoint, - ) { + ) -> Result { // Append the nullifier as public-input let nullifier = composer.append_public(self.nullifier); - let note_pk = composer - .append_point(*self.note.stealth_address().note_pk().as_ref()); - let note_pk_p = composer.append_point(self.note_pk_p); + // Each key is prover-supplied, so its subgroup membership is + // constrained where it enters the circuit. For `note_pk_p` this makes + // the nullifier, which hashes it, depend on the note alone. + let note_pk = append_subgroup_point( + composer, + *self.note.stealth_address().note_pk().as_ref(), + )?; + let note_pk_p = append_subgroup_point(composer, self.note_pk_p)?; let note_type = composer .append_witness(BlsScalar::from(self.note.note_type() as u64)); @@ -237,10 +248,15 @@ impl InputNoteInfo { let value_blinder = composer.append_witness(self.value_blinder); let signature_u = composer.append_witness(*self.signature.u()); - let signature_r = composer.append_point(self.signature.R()); - let signature_r_p = composer.append_point(self.signature.R_prime()); + // The signature equation sets `R = u·G + c·note_pk`, with `note_pk` in + // the subgroup, so `R` is in it without a separate check. + let signature_r = composer.append_point(*self.signature.R())?; + // The signature equation sets `R' = u·G' + c·note_pk_p`, with + // `note_pk_p` in the subgroup, so `R'` is in it without a separate + // check. + let signature_r_p = composer.append_point(*self.signature.R_prime())?; - ( + Ok(( note_pk, note_pk_p, note_type, @@ -251,6 +267,16 @@ impl InputNoteInfo { signature_u, signature_r, signature_r_p, - ) + )) } } + +/// Appends a prover-supplied point and constrains it to the prime-order +/// subgroup. +pub(crate) fn append_subgroup_point( + composer: &mut Composer, + point: impl Into, +) -> Result { + let point = composer.append_point(point)?; + Ok(composer.assert_torsion_free_point(point)) +} diff --git a/circuits/src/sender_enc.rs b/circuits/src/sender_enc.rs index 9ee562e..af03bf6 100644 --- a/circuits/src/sender_enc.rs +++ b/circuits/src/sender_enc.rs @@ -12,6 +12,8 @@ use jubjub_elgamal as elgamal; use jubjub_schnorr::{Signature as SchnorrSignature, gadgets}; use phoenix_core::{OUTPUT_NOTES, PublicKey}; +use crate::circuit_impl::append_subgroup_point; + /// Gadget to prove a valid origin for a given transaction. pub(crate) fn gadget( composer: &mut Composer, @@ -26,14 +28,21 @@ pub(crate) fn gadget( payload_hash: Witness, ) -> Result<(), Error> { // VERIFY A SIGNATURE FOR EACH KEY 'A' AND 'B' - let sender_pk_A = composer.append_point(sender_pk.A()); - let sender_pk_B = composer.append_point(sender_pk.B()); + // The keys are prover-supplied, so their subgroup membership is + // constrained where they enter the circuit. The signature gadgets + // additionally constrain them to be non-identity. + let sender_pk_A = append_subgroup_point(composer, *sender_pk.A())?; + let sender_pk_B = append_subgroup_point(composer, *sender_pk.B())?; let sig_A_u = composer.append_witness(*signatures.0.u()); - let sig_A_R = composer.append_point(signatures.0.R()); + // The signature equation sets `R = u·G + c·A`, with `A` in the subgroup, + // so `R` is in it without a separate check. + let sig_A_R = composer.append_point(*signatures.0.R())?; let sig_B_u = composer.append_witness(*signatures.1.u()); - let sig_B_R = composer.append_point(signatures.1.R()); + // The signature equation sets `R = u·G + c·B`, with `B` in the subgroup, + // so `R` is in it without a separate check. + let sig_B_R = composer.append_point(*signatures.1.R())?; gadgets::verify_signature( composer, @@ -51,8 +60,13 @@ pub(crate) fn gadget( )?; // ENCRYPT EACH KEY 'A' and 'B' USING EACH OUTPUT 'NPK' - let note_pk_0 = composer.append_public_point(output_npk[0]); - let note_pk_1 = composer.append_public_point(output_npk[1]); + // The output note-pks are public inputs. Their subgroup membership is + // constrained in-circuit rather than left to every verifier, since they + // are the ElGamal keys the sender public key is encrypted to. + let note_pk_0 = composer.append_public_point(output_npk[0])?; + let note_pk_0 = composer.assert_torsion_free_point(note_pk_0); + let note_pk_1 = composer.append_public_point(output_npk[1])?; + let note_pk_1 = composer.assert_torsion_free_point(note_pk_1); let blinder_A_0 = composer.append_witness(sender_blinder[0][0]); let blinder_B_0 = composer.append_witness(sender_blinder[0][1]); @@ -87,9 +101,9 @@ pub(crate) fn gadget( fn assert_sender_enc( composer: &mut Composer, - sender_pk_A: WitnessPoint, - sender_pk_B: WitnessPoint, - note_pk: WitnessPoint, + sender_pk_A: TorsionFreeWitnessPoint, + sender_pk_B: TorsionFreeWitnessPoint, + note_pk: TorsionFreeWitnessPoint, blinder: (Witness, Witness), sender_enc: [(JubJubAffine, JubJubAffine); 2], ) -> Result<(), Error> { @@ -112,11 +126,11 @@ fn assert_sender_enc( let sender_enc_A = sender_enc[0]; let sender_enc_B = sender_enc[1]; - composer.assert_equal_public_point(enc_A_c1, sender_enc_A.0); - composer.assert_equal_public_point(enc_A_c2, sender_enc_A.1); + composer.assert_equal_public_point(enc_A_c1.into(), sender_enc_A.0)?; + composer.assert_equal_public_point(enc_A_c2.into(), sender_enc_A.1)?; - composer.assert_equal_public_point(enc_B_c1, sender_enc_B.0); - composer.assert_equal_public_point(enc_B_c2, sender_enc_B.1); + composer.assert_equal_public_point(enc_B_c1.into(), sender_enc_B.0)?; + composer.assert_equal_public_point(enc_B_c2.into(), sender_enc_B.1)?; Ok(()) } diff --git a/circuits/tests/assets/456f644ee101e40eea33af55ad915f4e3c4fd04572da1b00cda57a09050080c3.vd b/circuits/tests/assets/456f644ee101e40eea33af55ad915f4e3c4fd04572da1b00cda57a09050080c3.vd new file mode 100644 index 0000000..e8d86b0 Binary files /dev/null and b/circuits/tests/assets/456f644ee101e40eea33af55ad915f4e3c4fd04572da1b00cda57a09050080c3.vd differ diff --git a/circuits/tests/assets/63cacc7adb37b19b6d10066e2d01ac2939422d9496d27f03dbc875b5458a36a5.vd b/circuits/tests/assets/63cacc7adb37b19b6d10066e2d01ac2939422d9496d27f03dbc875b5458a36a5.vd new file mode 100644 index 0000000..fbeba71 Binary files /dev/null and b/circuits/tests/assets/63cacc7adb37b19b6d10066e2d01ac2939422d9496d27f03dbc875b5458a36a5.vd differ diff --git a/circuits/tests/assets/929b3a0024cd60e3d56deb039e8a112e78caa3f23060193f21f533ad0557b8e2.vd b/circuits/tests/assets/929b3a0024cd60e3d56deb039e8a112e78caa3f23060193f21f533ad0557b8e2.vd new file mode 100644 index 0000000..fbecf4a Binary files /dev/null and b/circuits/tests/assets/929b3a0024cd60e3d56deb039e8a112e78caa3f23060193f21f533ad0557b8e2.vd differ diff --git a/circuits/tests/assets/bf184dc7e0569daef4e74bc6b681e5651b0d5a054b9cfc656d91b83d54343f71.vd b/circuits/tests/assets/bf184dc7e0569daef4e74bc6b681e5651b0d5a054b9cfc656d91b83d54343f71.vd new file mode 100644 index 0000000..5e18c7a Binary files /dev/null and b/circuits/tests/assets/bf184dc7e0569daef4e74bc6b681e5651b0d5a054b9cfc656d91b83d54343f71.vd differ diff --git a/circuits/tests/common/mod.rs b/circuits/tests/common/mod.rs index 72ff334..6a46bc2 100644 --- a/circuits/tests/common/mod.rs +++ b/circuits/tests/common/mod.rs @@ -5,7 +5,7 @@ // Copyright (c) DUSK NETWORK. All rights reserved. use std::fs; -use std::io::Read; +use std::io::{Read, Write}; use std::path::PathBuf; use std::sync::LazyLock; @@ -45,15 +45,20 @@ fn verify_crs(data: &[u8]) -> bool { hash == CRS_HASH } +/// The production CRS, read from the cache or downloaded. +/// +/// It has `2^17 + 6` powers, which fit the 1- and 2-input circuits only: +/// `dusk-plonk` 0.24 needs `2^17 + 9` for the 3- and 4-input circuits, see +/// [`EXTENDED_PP`]. pub fn load_production_crs() -> PublicParameters { let cache_path = crs_cache_path(); // try loading from cache - if let Ok(data) = fs::read(&cache_path) { - if verify_crs(&data) { - return PublicParameters::from_slice(&data) - .expect("failed to deserialize cached CRS"); - } + if let Ok(data) = fs::read(&cache_path) + && verify_crs(&data) + { + return PublicParameters::from_slice(&data) + .expect("failed to deserialize cached CRS"); } // download from network @@ -79,10 +84,67 @@ pub fn load_production_crs() -> PublicParameters { .expect("failed to deserialize downloaded CRS") } +/// The variable naming the file of the extended CRS. +const EXTENDED_CRS_VAR: &str = "PHOENIX_EXTENDED_CRS"; + +/// SHA-256 of the production CRS extended to `2^17 + 9` powers of the same +/// secret: the production file with three more powers appended. +const EXTENDED_CRS_HASH: &str = + "888fbab6c9d267c26199d9fd1a8c21f315590459ae180bc9e6a27aa433479d03"; + +/// The extended CRS, read from the file `PHOENIX_EXTENDED_CRS` names and +/// checked against its SHA-256, or `None` if the variable is unset. +/// +/// The 3- and 4-input circuits need it. Until it is hosted publicly next to +/// the production CRS, their tests skip when it is not given. +pub static EXTENDED_PP: LazyLock> = + LazyLock::new(|| { + let path = std::env::var_os(EXTENDED_CRS_VAR)?; + let data = fs::read(&path).unwrap_or_else(|e| { + panic!("failed to read the extended CRS at {path:?}: {e}") + }); + let hash = format!("{:x}", Sha256::digest(&data)); + assert_eq!(hash, EXTENDED_CRS_HASH, "extended CRS hash mismatch"); + + Some( + PublicParameters::from_slice(&data) + .expect("failed to deserialize the extended CRS"), + ) + }); + +/// The extended CRS, or `None` after telling that `test` skips without it. +/// +/// The notice goes to the process' stderr, so it shows without +/// `--nocapture`. +pub fn extended_crs(test: &str) -> Option<&'static PublicParameters> { + let pp = EXTENDED_PP.as_ref(); + if pp.is_none() { + let _ = writeln!( + std::io::stderr(), + "SKIPPED {test}: needs the CRS extended to 2^17 + 9 powers; set \ + {EXTENDED_CRS_VAR} to its path" + ); + } + pp +} + +/// The CRS to compile an `I`-input circuit with: the production CRS for one +/// and two inputs, the extended CRS for more, see [`extended_crs`]. +// Each test binary uses a different subset of the helpers. +#[allow(dead_code)] +pub fn crs(test: &str) -> Option<&'static PublicParameters> { + match I { + 0..=2 => Some(&TP.pp), + _ => extended_crs(test), + } +} + pub static LABEL: &[u8; 12] = b"dusk-network"; pub const HEIGHT: usize = 17; +// Each test binary reads a different subset of the fields. +#[allow(dead_code)] pub struct TestingParameters { pub pp: PublicParameters, pub input_notes_info: Vec>, @@ -90,6 +152,7 @@ pub struct TestingParameters { pub root: BlsScalar, pub deposit: u64, pub max_fee: u64, + pub sender_sk: SecretKey, pub sender_pk: PublicKey, pub output_npk: [JubJubAffine; OUTPUT_NOTES], pub signatures: (SchnorrSignature, SchnorrSignature), @@ -153,6 +216,7 @@ pub static TP: LazyLock = LazyLock::new(|| { root, deposit, max_fee, + sender_sk, sender_pk, output_npk, signatures: (sig_a, sig_b), @@ -236,6 +300,8 @@ fn create_test_input_notes_information( input_notes_info } +// Each test binary uses a different subset of the helpers. +#[allow(dead_code)] pub fn create_output_note_information( rng: &mut (impl RngCore + CryptoRng), value: u64, @@ -251,7 +317,8 @@ pub fn create_output_note_information( TP.sender_pk.A(), None, &sender_blinder_a, - ); + ) + .expect("the sender key encrypts to the note public key"); let sender_blinder_b = sender_blinder[1]; let (sender_enc_b, _) = ElGamal::encrypt( @@ -259,7 +326,8 @@ pub fn create_output_note_information( TP.sender_pk.B(), None, &sender_blinder_b, - ); + ) + .expect("the sender key encrypts to the note public key"); let sender_enc_a = (sender_enc_a.c1().into(), sender_enc_a.c2().into()); let sender_enc_b = (sender_enc_b.c1().into(), sender_enc_b.c2().into()); diff --git a/circuits/tests/torsion.rs b/circuits/tests/torsion.rs new file mode 100644 index 0000000..e5d48a1 --- /dev/null +++ b/circuits/tests/torsion.rs @@ -0,0 +1,360 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Regressions for the subgroup checks of the transaction circuit. +//! +//! The circuit constrains every key the prover supplies, and the note public +//! keys of the outputs, to the prime-order subgroup where they enter it. Each +//! test adds an offset to one of these keys and recomputes what depends on the +//! key, the signatures, the note, its nullifier and the encryptions, so that +//! every other equation of the circuit holds. With the identity as the offset, +//! the transaction must prove, which checks the construction; with the +//! order-two point `T`, it must be unsatisfiable. +//! +//! 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. + +#![cfg(feature = "plonk")] +#![allow(non_snake_case)] + +use std::sync::LazyLock; + +use dusk_bls12_381::BlsScalar; +use dusk_bytes::Serializable; +use dusk_jubjub::{ + GENERATOR_EXTENDED, GENERATOR_NUMS_EXTENDED, JubJubAffine, JubJubExtended, + JubJubScalar, +}; +use dusk_plonk::prelude::{Compiler, Error as PlonkError, PublicParameters}; +use dusk_poseidon::{Domain, Hash}; +use ff::Field; +use jubjub_schnorr::{ + PublicKey as NotePublicKey, Signature as SchnorrSignature, SignatureDouble, +}; +use phoenix_circuits::TxCircuit; +use phoenix_core::{Note, PublicKey, StealthAddress, value_commitment}; +use poseidon_merkle::{Item, Tree}; +use rand::SeedableRng; +use rand::rngs::StdRng; + +mod common; +use common::*; + +/// A test SRS that fits every input variant. Soundness does not depend on the +/// SRS, and the production CRS only fits the 1- and 2-input circuits under +/// `dusk-plonk`'s blinding degree. +static PP: LazyLock = LazyLock::new(|| { + PublicParameters::setup(1 << 17, &mut StdRng::seed_from_u64(0x7042)) + .expect("failed to generate the test SRS") +}); + +/// The order-two point `(0, -1)`. +fn order_two_point() -> JubJubExtended { + let point = JubJubExtended::from(JubJubAffine::from_raw_unchecked( + BlsScalar::zero(), + -BlsScalar::one(), + )); + assert!(!bool::from(point.is_identity())); + assert!(bool::from(point.double().is_identity())); + point +} + +/// The key a test adds an offset to. +#[derive(Clone, Copy, Debug, PartialEq)] +enum Shifted { + /// `note_pk` of the last input. + NotePk, + /// `note_pk_p` of the first input. + NotePkP, + /// `A` of the sender public key. + SenderA, + /// `B` of the sender public key. + SenderB, + /// The note public key of the given output. + OutputNotePk(usize), +} + +/// The double-signature challenge of `jubjub-schnorr`. +fn double_challenge( + R: &JubJubExtended, + R_p: &JubJubExtended, + pk: &JubJubExtended, + pk_p: &JubJubExtended, + message: BlsScalar, +) -> JubJubScalar { + let [R_x, R_y] = R.to_hash_inputs(); + let [R_p_x, R_p_y] = R_p.to_hash_inputs(); + let [pk_x, pk_y] = pk.to_hash_inputs(); + let [pk_p_x, pk_p_y] = pk_p.to_hash_inputs(); + let domain = BlsScalar::from(u64::from_be_bytes(*b"JJSCHDBL")); + + Hash::digest_truncated( + Domain::Other, + &[ + domain, R_x, R_y, R_p_x, R_p_y, pk_x, pk_y, pk_p_x, pk_p_y, message, + ], + )[0] +} + +/// The single-signature challenge of `jubjub-schnorr`. +fn challenge( + R: &JubJubExtended, + pk: &JubJubExtended, + message: BlsScalar, +) -> JubJubScalar { + let [R_x, R_y] = R.to_hash_inputs(); + let [pk_x, pk_y] = pk.to_hash_inputs(); + + Hash::digest_truncated(Domain::Other, &[R_x, R_y, pk_x, pk_y, message])[0] +} + +/// A nonce `r` whose challenge `c` is even, and `c`. A signature with an even +/// challenge holds for a key plus `T` as well, as `c · T` vanishes. +fn even_challenge( + challenge: impl Fn(JubJubScalar) -> JubJubScalar, +) -> (JubJubScalar, JubJubScalar) { + let mut rng = StdRng::seed_from_u64(0xe7e); + loop { + let r = JubJubScalar::random(&mut rng); + let c = challenge(r); + if c.to_bytes()[0] & 1 == 0 { + return (r, c); + } + } +} + +/// A double signature of the payload hash with `sk`, for the keys `pk` and +/// `pk_p`. +fn sign_double( + sk: &JubJubScalar, + pk: &JubJubExtended, + pk_p: &JubJubExtended, +) -> SignatureDouble { + let (r, c) = even_challenge(|r| { + let R = GENERATOR_EXTENDED * r; + let R_p = GENERATOR_NUMS_EXTENDED * r; + double_challenge(&R, &R_p, pk, pk_p, TP.payload_hash) + }); + let u = r - c * sk; + let (R, R_p) = (GENERATOR_EXTENDED * r, GENERATOR_NUMS_EXTENDED * r); + + // Both equations the circuit checks hold. + assert_eq!(GENERATOR_EXTENDED * u + pk * c, R); + assert_eq!(GENERATOR_NUMS_EXTENDED * u + pk_p * c, R_p); + + let mut bytes = [0u8; SignatureDouble::SIZE]; + bytes[..32].copy_from_slice(&u.to_bytes()); + bytes[32..64].copy_from_slice(&JubJubAffine::from(R).to_bytes()); + bytes[64..].copy_from_slice(&JubJubAffine::from(R_p).to_bytes()); + SignatureDouble::from_bytes(&bytes) + .expect("the nonces are prime-order points") +} + +/// A signature of the payload hash with `sk`, for the key `pk`. +fn sign(sk: &JubJubScalar, pk: &JubJubExtended) -> SchnorrSignature { + let (r, c) = even_challenge(|r| { + challenge(&(GENERATOR_EXTENDED * r), pk, TP.payload_hash) + }); + let u = r - c * sk; + let R = GENERATOR_EXTENDED * r; + + // The equation the circuit checks holds. + assert_eq!(GENERATOR_EXTENDED * u + pk * c, R); + + let mut bytes = [0u8; SchnorrSignature::SIZE]; + bytes[..32].copy_from_slice(&u.to_bytes()); + bytes[32..].copy_from_slice(&JubJubAffine::from(R).to_bytes()); + SchnorrSignature::from_bytes(&bytes) + .expect("the nonce is a prime-order point") +} + +/// The ElGamal encryption of `message` to `key` with `blinder`, as the +/// circuit computes it. +fn encrypt( + key: &JubJubExtended, + message: &JubJubExtended, + blinder: &JubJubScalar, +) -> (JubJubAffine, JubJubAffine) { + let c1 = GENERATOR_EXTENDED * blinder; + let c2 = message + key * blinder; + (c1.into(), c2.into()) +} + +/// The `I`-input transaction spending the first test notes, with `offset` +/// added to the key `shifted` names. What depends on that key is recomputed +/// for it, so that every other equation of the circuit holds. +fn transaction( + shifted: Shifted, + offset: JubJubExtended, +) -> TxCircuit { + // `offset` for the key `shifted` names, the identity for the others. + let offset_of = |key: Shifted| { + if key == shifted { + offset + } else { + JubJubExtended::identity() + } + }; + let mut rng = StdRng::seed_from_u64(0xc0b); + + let mut inputs: [InputNoteInfo; I] = + core::array::from_fn(|i| TP.input_notes_info[i].clone()); + if shifted == Shifted::NotePk { + let input = &mut inputs[I - 1]; + let stealth_address = *input.note.stealth_address(); + let note_sk = TP.sender_sk.gen_note_sk(&stealth_address); + let note_pk = stealth_address.note_pk().as_ref() + offset; + let stealth_address = StealthAddress::from_raw_unchecked( + *stealth_address.R(), + NotePublicKey::from_raw_unchecked(note_pk), + ); + + let mut note = Note::transparent_stealth( + stealth_address, + input.value, + *input.note.sender(), + ); + note.set_pos(*input.note.pos()); + input.note = note; + input.signature = + sign_double(note_sk.as_ref(), ¬e_pk, &input.note_pk_p.into()); + } + if shifted == Shifted::NotePkP { + let input = &mut inputs[0]; + let note_sk = TP.sender_sk.gen_note_sk(input.note.stealth_address()); + let note_pk = *input.note.stealth_address().note_pk().as_ref(); + let note_pk_p = JubJubExtended::from(input.note_pk_p) + offset; + + let [x, y] = note_pk_p.to_hash_inputs(); + let pos = BlsScalar::from(*input.note.pos()); + input.nullifier = Hash::digest(Domain::Other, &[x, y, pos])[0]; + input.note_pk_p = note_pk_p.into(); + input.signature = sign_double(note_sk.as_ref(), ¬e_pk, ¬e_pk_p); + } + + // The inputs in a tree of their own. + let mut tree = Tree::<(), HEIGHT>::new(); + for input in &inputs { + let item = Item { + hash: input.note.hash(), + data: (), + }; + tree.insert(*input.note.pos(), item); + } + for input in &mut inputs { + input.merkle_opening = tree + .opening(*input.note.pos()) + .expect("the note is in the tree"); + } + + let A = TP.sender_pk.A() + offset_of(Shifted::SenderA); + let B = TP.sender_pk.B() + offset_of(Shifted::SenderB); + let mut signatures = TP.signatures; + if shifted == Shifted::SenderA { + signatures.0 = sign(TP.sender_sk.a(), &A); + } + if shifted == Shifted::SenderB { + signatures.1 = sign(TP.sender_sk.b(), &B); + } + + // The inputs hold 25 each, the fee and the deposit take 10. + let values = [25 * I as u64 - 15, 5]; + let output_notes_info = core::array::from_fn(|k| { + let note_pk = JubJubExtended::from(TP.output_npk[k]) + + offset_of(Shifted::OutputNotePk(k)); + let value_blinder = JubJubScalar::random(&mut rng); + let sender_blinder = TP.sender_blinder[k]; + + OutputNoteInfo { + value: values[k], + value_commitment: value_commitment(values[k], value_blinder), + value_blinder, + note_pk: note_pk.into(), + sender_enc: [ + encrypt(¬e_pk, &A, &sender_blinder[0]), + encrypt(¬e_pk, &B, &sender_blinder[1]), + ], + sender_blinder, + } + }); + + TxCircuit { + input_notes_info: inputs, + output_notes_info, + payload_hash: TP.payload_hash, + root: tree.root().hash, + deposit: TP.deposit, + max_fee: TP.max_fee, + sender_pk: PublicKey::new(A, B), + signatures, + } +} + +/// For each key in `shifted`, proves the `I`-input transaction with the +/// identity added to the key, as a control of the construction, then checks +/// that the same construction with `T` added is unsatisfiable. +fn assert_rejected(shifted: &[Shifted]) { + let mut rng = StdRng::seed_from_u64(0x7042); + + let (prover, verifier) = + Compiler::compile::>(&PP, LABEL) + .expect("failed to compile circuit"); + + for &shifted in shifted { + let control = transaction::(shifted, JubJubExtended::identity()); + let (proof, public_inputs) = + prover.prove(&mut rng, &control).unwrap_or_else(|err| { + panic!("{shifted:?} with a zero offset must prove: {err:?}") + }); + verifier + .verify(&proof, &public_inputs) + .unwrap_or_else(|err| { + panic!("{shifted:?} with a zero offset must verify: {err:?}") + }); + + let result = prover + .prove(&mut rng, &transaction::(shifted, order_two_point())); + assert!( + matches!(result, Err(PlonkError::CircuitUnsatisfied)), + "{shifted:?} with a small-order component must be unsatisfiable" + ); + } +} + +#[test] +fn note_pk_p_rejected_1_2() { + assert_rejected::<1>(&[Shifted::NotePkP]); +} + +#[test] +fn note_pk_p_rejected_2_2() { + assert_rejected::<2>(&[Shifted::NotePkP]); +} + +#[test] +fn note_pk_p_rejected_3_2() { + assert_rejected::<3>(&[Shifted::NotePkP]); +} + +#[test] +fn note_pk_p_rejected_4_2() { + assert_rejected::<4>(&[Shifted::NotePkP]); +} + +#[test] +fn note_pk_rejected() { + assert_rejected::<2>(&[Shifted::NotePk]); +} + +#[test] +fn sender_key_rejected() { + assert_rejected::<2>(&[Shifted::SenderA, Shifted::SenderB]); +} + +#[test] +fn output_note_pk_rejected() { + assert_rejected::<2>(&[Shifted::OutputNotePk(0), Shifted::OutputNotePk(1)]); +} diff --git a/circuits/tests/transaction.rs b/circuits/tests/transaction.rs index 0d6ba05..94de3d1 100644 --- a/circuits/tests/transaction.rs +++ b/circuits/tests/transaction.rs @@ -16,10 +16,13 @@ use common::*; #[test] fn test_transfer_circuit_1_2() { + let Some(pp) = crs::<1>("test_transfer_circuit_1_2") else { + return; + }; let mut rng = StdRng::seed_from_u64(0xc0b); let (prover, verifier) = - Compiler::compile::>(&TP.pp, LABEL) + Compiler::compile::>(pp, LABEL) .expect("failed to compile circuit"); let input_notes_info = [TP.input_notes_info[0].clone()]; @@ -63,10 +66,13 @@ fn test_transfer_circuit_1_2() { #[test] fn test_transfer_circuit_2_2() { + let Some(pp) = crs::<2>("test_transfer_circuit_2_2") else { + return; + }; let mut rng = StdRng::seed_from_u64(0xc0b); let (prover, verifier) = - Compiler::compile::>(&TP.pp, LABEL) + Compiler::compile::>(pp, LABEL) .expect("failed to compile circuit"); let input_notes_info = [ @@ -113,10 +119,13 @@ fn test_transfer_circuit_2_2() { #[test] fn test_transfer_circuit_3_2() { + let Some(pp) = crs::<3>("test_transfer_circuit_3_2") else { + return; + }; let mut rng = StdRng::seed_from_u64(0xc0b); let (prover, verifier) = - Compiler::compile::>(&TP.pp, LABEL) + Compiler::compile::>(pp, LABEL) .expect("failed to compile circuit"); let input_notes_info = [ @@ -164,10 +173,13 @@ fn test_transfer_circuit_3_2() { #[test] fn test_transfer_circuit_4_2() { + let Some(pp) = crs::<4>("test_transfer_circuit_4_2") else { + return; + }; let mut rng = StdRng::seed_from_u64(0xc0b); let (prover, verifier) = - Compiler::compile::>(&TP.pp, LABEL) + Compiler::compile::>(pp, LABEL) .expect("failed to compile circuit"); let input_notes_info = [ diff --git a/circuits/tests/verifier_data.rs b/circuits/tests/verifier_data.rs index 4574919..602bdea 100644 --- a/circuits/tests/verifier_data.rs +++ b/circuits/tests/verifier_data.rs @@ -4,13 +4,24 @@ // // Copyright (c) DUSK NETWORK. All rights reserved. -//! Tests that verify proofs generated by the current circuit implementation -//! against the stored verifier data from the deployed transfer contract. +//! Proofs of the current circuits against stored verifier data. //! -//! These tests use the production CRS (Common Reference String) to ensure -//! compatibility with the on-chain verifier. The CRS is loaded from -//! `~/.dusk/rusk/devnet-piecrust.crs` if available, otherwise downloaded -//! from the network. +//! `DEPLOYED_VD_*` are copies of the verifier data embedded in rusk's transfer +//! contract, compiled from the circuits of `phoenix-circuits` 0.8. The circuits +//! have changed since, and with them their keys: a proof of the current +//! circuits must not verify against the deployed verifier data. `VD_*` are the +//! verifier data of the current circuits, which compiling them reproduces and +//! their proofs verify against. +//! +//! The files are named as rusk-recovery names verifier data: by the blake3 +//! hash of the compressed circuit description followed by the major and minor +//! version of `dusk-plonk`, big-endian `u16`s, 0.20 for the deployed data and +//! 0.24 for the current. +//! +//! The 1- and 2-input circuits compile with the production CRS, the 3- and +//! 4-input ones with the extended CRS only, see `common::EXTENDED_PP`. The +//! extended CRS appends powers of the same secret to the production one, so +//! both compile the 1- and 2-input circuits to the same verifier data. #![cfg(feature = "plonk")] @@ -22,28 +33,52 @@ use rand::rngs::StdRng; mod common; use common::*; -const VD_EXEC_1_2: &[u8] = include_bytes!( +const DEPLOYED_VD_1_2: &[u8] = include_bytes!( "assets/c8fed2bfcc0e0e64709586b56636fc1831be5f0227e533363e9a49b8fae5cd2f.vd" ); -const VD_EXEC_2_2: &[u8] = include_bytes!( +const DEPLOYED_VD_2_2: &[u8] = include_bytes!( "assets/98c9786a8cf36f19bcbdf97f4bc140fe402ae5f72cef3f60f24b96071c0faa73.vd" ); -const VD_EXEC_3_2: &[u8] = include_bytes!( +const DEPLOYED_VD_3_2: &[u8] = include_bytes!( "assets/1210b96327d25a0403be7b8e027cfe964370700b94ec7f47d22128ecbe7e9803.vd" ); -const VD_EXEC_4_2: &[u8] = include_bytes!( +const DEPLOYED_VD_4_2: &[u8] = include_bytes!( "assets/0095785bd378e5cd3c7427c03b6d4420966c03156bf045b556f22419252fc8bc.vd" ); -fn prove_and_verify_against_stored_vd( - input_notes_info: [InputNoteInfo; I], +const VD_1_2: &[u8] = include_bytes!( + "assets/bf184dc7e0569daef4e74bc6b681e5651b0d5a054b9cfc656d91b83d54343f71.vd" +); +const VD_2_2: &[u8] = include_bytes!( + "assets/456f644ee101e40eea33af55ad915f4e3c4fd04572da1b00cda57a09050080c3.vd" +); +const VD_3_2: &[u8] = include_bytes!( + "assets/63cacc7adb37b19b6d10066e2d01ac2939422d9496d27f03dbc875b5458a36a5.vd" +); +const VD_4_2: &[u8] = include_bytes!( + "assets/929b3a0024cd60e3d56deb039e8a112e78caa3f23060193f21f533ad0557b8e2.vd" +); + +/// Compiles the `I`-input circuit to `vd`, and proves a transaction whose +/// proof verifies against `vd` and not against `deployed_vd`. +fn assert_verifier_data( + test: &str, output_values: [u64; 2], - stored_vd: &[u8], + vd: &[u8], + deployed_vd: &[u8], ) { + let Some(pp) = crs::(test) else { + return; + }; let mut rng = StdRng::seed_from_u64(0xc0b); - let (prover, _) = Compiler::compile::>(&TP.pp, LABEL) - .expect("failed to compile circuit"); + let (prover, verifier) = + Compiler::compile::>(pp, LABEL) + .expect("failed to compile circuit"); + assert!( + verifier.to_bytes() == vd, + "the circuit compiles to other verifier data than the stored one" + ); let output_notes_info = [ create_output_note_information( @@ -63,8 +98,10 @@ fn prove_and_verify_against_stored_vd( let (proof, public_inputs) = prover .prove( &mut rng, - &TxCircuit { - input_notes_info, + &TxCircuit:: { + input_notes_info: core::array::from_fn(|i| { + TP.input_notes_info[i].clone() + }), output_notes_info, payload_hash: TP.payload_hash, root: TP.root, @@ -76,46 +113,89 @@ fn prove_and_verify_against_stored_vd( ) .expect("failed to prove"); - let stored_verifier = Verifier::try_from_bytes(stored_vd) - .expect("failed to deserialize stored verifier data"); - - stored_verifier + Verifier::try_from_bytes(vd) + .expect("failed to deserialize stored verifier data") .verify(&proof, &public_inputs) .expect("proof failed to verify against stored verifier data"); + + let deployed = Verifier::try_from_bytes(deployed_vd) + .expect("failed to deserialize deployed verifier data"); + assert!( + deployed.verify(&proof, &public_inputs).is_err(), + "a proof of the current circuit verifies against the deployed \ + verifier data" + ); +} + +/// Compiles the `I`-input circuit with the production and the extended CRS, +/// to the same verifier data. +fn assert_crs_extension_keeps_vd(test: &str) { + let Some(extended) = extended_crs(test) else { + return; + }; + + let (_, production) = + Compiler::compile::>(&TP.pp, LABEL) + .expect("failed to compile circuit with the production CRS"); + let (_, extended) = + Compiler::compile::>(extended, LABEL) + .expect("failed to compile circuit with the extended CRS"); + assert!( + production.to_bytes() == extended.to_bytes(), + "the production and extended CRS compile to other verifier data" + ); } #[test] fn test_verifier_data_1_2() { - let input_notes_info = [TP.input_notes_info[0].clone()]; - prove_and_verify_against_stored_vd(input_notes_info, [10, 5], VD_EXEC_1_2); + assert_verifier_data::<1>( + "test_verifier_data_1_2", + [10, 5], + VD_1_2, + DEPLOYED_VD_1_2, + ); } #[test] fn test_verifier_data_2_2() { - let input_notes_info = [ - TP.input_notes_info[0].clone(), - TP.input_notes_info[1].clone(), - ]; - prove_and_verify_against_stored_vd(input_notes_info, [35, 5], VD_EXEC_2_2); + assert_verifier_data::<2>( + "test_verifier_data_2_2", + [35, 5], + VD_2_2, + DEPLOYED_VD_2_2, + ); } #[test] fn test_verifier_data_3_2() { - let input_notes_info = [ - TP.input_notes_info[0].clone(), - TP.input_notes_info[1].clone(), - TP.input_notes_info[2].clone(), - ]; - prove_and_verify_against_stored_vd(input_notes_info, [35, 30], VD_EXEC_3_2); + assert_verifier_data::<3>( + "test_verifier_data_3_2", + [35, 30], + VD_3_2, + DEPLOYED_VD_3_2, + ); } #[test] fn test_verifier_data_4_2() { - let input_notes_info = [ - TP.input_notes_info[0].clone(), - TP.input_notes_info[1].clone(), - TP.input_notes_info[2].clone(), - TP.input_notes_info[3].clone(), - ]; - prove_and_verify_against_stored_vd(input_notes_info, [60, 30], VD_EXEC_4_2); + assert_verifier_data::<4>( + "test_verifier_data_4_2", + [60, 30], + VD_4_2, + DEPLOYED_VD_4_2, + ); +} + +#[test] +fn test_crs_extension_keeps_verifier_data_1_2() { + assert_crs_extension_keeps_vd::<1>( + "test_crs_extension_keeps_verifier_data_1_2", + ); +} + +#[test] +fn test_crs_extension_keeps_verifier_data_2_2() { + assert_crs_extension_keeps_vd::<2>( + "test_crs_extension_keeps_verifier_data_2_2", + ); } diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index 9be8e31..10c7b72 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -17,6 +17,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Update `jubjub-schnorr` to v0.7.0 [#311] +- Update `dusk-bls12_381` and `dusk-jubjub` to v0.16, `dusk-poseidon` to + v0.44.0 and `jubjub-elgamal` to v0.6.0 [#311] +- Panic in `Note::new`, `Note::transparent`, `Note::obfuscated` and + `Sender::encrypt` when an encryption degenerates, which takes a zero sender + blinder or a key point that is not of prime order [#311] +- Return `Error::InvalidEncryption` from `Note::value`, `Note::value_blinder` + and `Note::value_opening` for an obfuscated note whose `R` is the identity or + of small order [#311] - Remove the unused direct `hex` and `assert_matches` dependencies [#308] - Remove the unused direct `base64` dependency [#306] - Raise the MSRV to Rust 1.96.1 [#302] @@ -26,7 +35,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 value blinder, ignoring the supplied blinder [#290] - Return `Error::CommitmentMismatch` when a recovered note opening does not match its value commitment [#290] -- Update `dusk-jubjub` to v0.15.2 for its torsion-rejecting point decoder [#288] ### Fixed @@ -474,6 +482,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Canonical implementation shielded by feature. +[#311]: https://github.com/dusk-network/phoenix/issues/311 [#308]: https://github.com/dusk-network/phoenix/issues/308 [#306]: https://github.com/dusk-network/phoenix/issues/306 [#302]: https://github.com/dusk-network/phoenix/issues/302 diff --git a/core/Cargo.toml b/core/Cargo.toml index 0ade8ab..4716e87 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -12,13 +12,13 @@ edition.workspace = true [dependencies] rand_core = { version = "0.6", default-features = false } dusk-bytes = "0.1" -dusk-bls12_381 = { version = "0.14", default-features = false } -dusk-jubjub = { version = "0.15.2", default-features = false, features = [ +dusk-bls12_381 = { version = "0.16", default-features = false } +dusk-jubjub = { version = "0.16", default-features = false, features = [ "zeroize", ] } -dusk-poseidon = "0.42.0-rc.0" -jubjub-schnorr = "0.7.0-rc.0" -jubjub-elgamal = "0.5.0" +dusk-poseidon = "0.44.0" +jubjub-schnorr = "0.7.0" +jubjub-elgamal = "0.6.0" subtle = { version = "2.6", default-features = false } ff = { version = "0.13", default-features = false } aes-gcm = { version = "0.10", default-features = false, features = [ diff --git a/core/src/keys.rs b/core/src/keys.rs index 4aef965..5c8682a 100644 --- a/core/src/keys.rs +++ b/core/src/keys.rs @@ -12,6 +12,9 @@ pub mod view; /// Hashes a JubJub's ExtendedPoint into a JubJub's Scalar using the JubJub's /// hash to scalar function +/// +/// Uses the legacy construction without a domain separator, which every +/// existing note key is derived with. pub fn hash(p: &JubJubExtended) -> JubJubScalar { - JubJubScalar::hash_to_scalar(&JubJubAffine::from(p).to_bytes()) + JubJubScalar::hash_to_scalar(None, &JubJubAffine::from(p).to_bytes()) } diff --git a/core/src/note.rs b/core/src/note.rs index 576bf8c..fbb5aba 100644 --- a/core/src/note.rs +++ b/core/src/note.rs @@ -105,6 +105,11 @@ impl Note { /// /// For transparent notes, `value_blinder` is ignored and the canonical /// zero blinder is used instead. + /// + /// # Panics + /// + /// Panics if an encryption degenerates, which takes a zero sender blinder + /// or a key point that is not of prime order. pub fn new( rng: &mut R, note_type: NoteType, @@ -134,7 +139,8 @@ impl Note { value_enc } NoteType::Obfuscated => { - let shared_secret = dhke(&r, receiver_pk.A()); + let shared_secret = dhke(&r, receiver_pk.A()) + .expect("The receiver key `A` is of prime order."); let value_blinder = BlsScalar::from(value_blinder); let mut plaintext = value.to_bytes().to_vec(); @@ -166,6 +172,10 @@ impl Note { /// The blinding factor will be constant zero since the value commitment /// exists only to shield the value. The value is not hidden for transparent /// notes, so this can be trivially treated as a constant. + /// + /// # Panics + /// + /// Panics as [`Note::new`] does. pub fn transparent( rng: &mut R, sender_pk: &PublicKey, @@ -217,6 +227,10 @@ impl Note { /// commitment of the note. The tuple (value, value_blinder), known by /// the caller of this function, must be later used to prove the /// knowledge of the value commitment of this note. + /// + /// # Panics + /// + /// Panics as [`Note::new`] does. pub fn obfuscated( rng: &mut R, sender_pk: &PublicKey, @@ -320,7 +334,9 @@ impl Note { vk: &ViewKey, ) -> Result<(u64, JubJubScalar), Error> { let R = self.stealth_address.R(); - let shared_secret = dhke(vk.a(), R); + // An identity or small-order `R` yields no shared secret. + let shared_secret = Option::::from(dhke(vk.a(), R)) + .ok_or(Error::InvalidEncryption)?; let salt = self.stealth_address.to_bytes(); @@ -558,6 +574,11 @@ pub enum Sender { impl Sender { /// Create a new [`Sender`] enum by encrypting the sender's [`PublicKey`] in /// a way that only the receiver of the note can decrypt. + /// + /// # Panics + /// + /// Panics if an encryption degenerates, which takes a zero blinder or a + /// point that is not of prime order. pub fn encrypt( note_pk: &NotePublicKey, sender_pk: &PublicKey, @@ -568,14 +589,16 @@ impl Sender { sender_pk.A(), None, &blinder[0], - ); + ) + .expect("The sender key `A` encrypts to the note public key."); let (sender_enc_B, _) = ElGamal::encrypt( note_pk.as_ref(), sender_pk.B(), None, &blinder[1], - ); + ) + .expect("The sender key `B` encrypts to the note public key."); let sender_enc_A: (JubJubAffine, JubJubAffine) = (sender_enc_A.c1().into(), sender_enc_A.c2().into()); diff --git a/core/tests/note_test.rs b/core/tests/note_test.rs index e43f1b8..f82d9ec 100644 --- a/core/tests/note_test.rs +++ b/core/tests/note_test.rs @@ -4,14 +4,15 @@ // // Copyright (c) DUSK NETWORK. All rights reserved. +use dusk_bls12_381::BlsScalar; use dusk_bytes::Serializable; use dusk_jubjub::{ Fq, GENERATOR_EXTENDED, JubJubAffine, JubJubExtended, JubJubScalar, }; use ff::Field; use phoenix_core::{ - Error, Note, NoteType, PublicKey, SecretKey, Sender, StealthAddress, - ViewKey, value_commitment, + Error, NOTE_VAL_ENC_SIZE, Note, NoteType, PublicKey, SecretKey, Sender, + StealthAddress, ViewKey, aes, value_commitment, }; use rand::rngs::StdRng; use rand::{RngCore, SeedableRng}; @@ -426,3 +427,127 @@ fn note_not_owned() { assert!(note.value(Some(¬_owner_vk)).is_err()); } + +#[test] +#[should_panic(expected = "The receiver key `A` is of prime order.")] +fn obfuscated_note_panics_on_identity_receiver_key() { + let mut rng = StdRng::seed_from_u64(0xc0b); + + let sender_pk = PublicKey::from(&SecretKey::random(&mut rng)); + let receiver_pk = PublicKey::from(&SecretKey::random(&mut rng)); + let receiver_pk = + PublicKey::new(JubJubExtended::identity(), *receiver_pk.B()); + let value_blinder = JubJubScalar::random(&mut rng); + let sender_blinder = [ + JubJubScalar::random(&mut rng), + JubJubScalar::random(&mut rng), + ]; + + Note::obfuscated( + &mut rng, + &sender_pk, + &receiver_pk, + 25, + value_blinder, + sender_blinder, + ); +} + +/// Creates a transparent note with `sender_blinder`. +fn transparent_note_with_sender_blinder(sender_blinder: [JubJubScalar; 2]) { + let mut rng = StdRng::seed_from_u64(0xc0b); + + let sender_pk = PublicKey::from(&SecretKey::random(&mut rng)); + let receiver_pk = PublicKey::from(&SecretKey::random(&mut rng)); + + Note::transparent(&mut rng, &sender_pk, &receiver_pk, 25, sender_blinder); +} + +#[test] +#[should_panic( + expected = "The sender key `A` encrypts to the note public key." +)] +fn note_panics_on_zero_sender_blinder_for_a() { + transparent_note_with_sender_blinder([ + JubJubScalar::zero(), + JubJubScalar::one(), + ]); +} + +#[test] +#[should_panic( + expected = "The sender key `B` encrypts to the note public key." +)] +fn note_panics_on_zero_sender_blinder_for_b() { + transparent_note_with_sender_blinder([ + JubJubScalar::one(), + JubJubScalar::zero(), + ]); +} + +#[test] +fn obfuscated_note_with_identity_r_has_no_opening() { + let mut rng = StdRng::seed_from_u64(0xc0b); + + let sender_pk = PublicKey::from(&SecretKey::random(&mut rng)); + let receiver_sk = SecretKey::random(&mut rng); + let receiver_pk = PublicKey::from(&receiver_sk); + let receiver_vk = ViewKey::from(&receiver_sk); + + let value = 25; + let value_blinder = JubJubScalar::random(&mut rng); + let sender_blinder = [ + JubJubScalar::random(&mut rng), + JubJubScalar::random(&mut rng), + ]; + let note = Note::obfuscated( + &mut rng, + &sender_pk, + &receiver_pk, + value, + value_blinder, + sender_blinder, + ); + + // An identity `R`, with the opening encrypted under the shared secret it + // gives, the identity: without the check of `R`, the opening decrypts and + // matches the value commitment. + let stealth_address = StealthAddress::from_raw_unchecked( + JubJubExtended::identity(), + *note.stealth_address().note_pk(), + ); + let mut plaintext = value.to_bytes().to_vec(); + plaintext.extend_from_slice(&BlsScalar::from(value_blinder).to_bytes()); + let value_enc: [u8; NOTE_VAL_ENC_SIZE] = aes::encrypt( + &JubJubAffine::identity(), + &stealth_address.to_bytes(), + &plaintext, + &mut rng, + ) + .expect("the opening encrypts"); + + // The stealth address follows the note type and the value commitment, + // and the value encryption follows it and the position. + let mut bytes = note.to_bytes(); + let stealth_start = 1 + JubJubAffine::SIZE; + let value_enc_start = stealth_start + StealthAddress::SIZE + u64::SIZE; + bytes[stealth_start..stealth_start + StealthAddress::SIZE] + .copy_from_slice(&stealth_address.to_bytes()); + bytes[value_enc_start..value_enc_start + NOTE_VAL_ENC_SIZE] + .copy_from_slice(&value_enc); + let note = Note::from_bytes(&bytes).expect("the note decodes"); + assert_eq!(note.stealth_address(), &stealth_address); + + assert!(matches!( + note.value_opening(Some(&receiver_vk)), + Err(Error::InvalidEncryption) + )); + assert!(matches!( + note.value(Some(&receiver_vk)), + Err(Error::InvalidEncryption) + )); + assert!(matches!( + note.value_blinder(Some(&receiver_vk)), + Err(Error::InvalidEncryption) + )); +}