diff --git a/circuits/CHANGELOG.md b/circuits/CHANGELOG.md index 7487eef..4f0e6b0 100644 --- a/circuits/CHANGELOG.md +++ b/circuits/CHANGELOG.md @@ -7,11 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add Recipient gadget [#197] + ### Changed - Remove `ViewKey` from `TxOutputNote::new()` parameters [#191] - Make `rng` the first param in `TxInputNote::new` [#189] - Rename `crossover` to `deposit` [#190] +- Remove recomputation of `value_commitment` in `TxOutputNote::New()` +- Rename `skeleton_hash` to `payload_hash` [#188] +- Make `TxCircuit` to use the Recipient gadget ## [0.1.0] - 2024-05-22 @@ -29,6 +36,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Update `poseidon-merkle` to v0.6 [#179] +[#197]: https://github.com/dusk-network/phoenix/issues/197 +[#188]: https://github.com/dusk-network/phoenix/issues/188 [#191]: https://github.com/dusk-network/phoenix/issues/191 [#190]: https://github.com/dusk-network/phoenix/issues/190 [#189]: https://github.com/dusk-network/phoenix/issues/189 diff --git a/circuits/src/lib.rs b/circuits/src/lib.rs index 9a61176..524311b 100644 --- a/circuits/src/lib.rs +++ b/circuits/src/lib.rs @@ -11,9 +11,13 @@ #![no_std] mod encryption; +mod recipient; -/// Transaction structs, gadget, and circuit +/// Transaction structs, and circuit pub mod transaction; +/// Recipient Parameters +pub use recipient::RecipientParameters; + /// ElGamal asymmetric cipher pub use encryption::elgamal; diff --git a/circuits/src/recipient.rs b/circuits/src/recipient.rs new file mode 100644 index 0000000..1dda19f --- /dev/null +++ b/circuits/src/recipient.rs @@ -0,0 +1,114 @@ +// 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. + +use dusk_jubjub::JubJubScalar; +use dusk_plonk::prelude::*; +use jubjub_schnorr::{gadgets, Signature}; + +use crate::elgamal; +use phoenix_core::PublicKey; + +use rand::rngs::StdRng; +use rand::SeedableRng; + +use phoenix_core::SecretKey; + +const TX_OUTPUT_NOTES: usize = 2; + +/// Parameters needed to prove a recipient in-circuit +#[derive(Debug, Clone, Copy)] +pub struct RecipientParameters { + /// Public key of the transaction sender + pub sender_pk: PublicKey, + /// Note public keys of each note recipient + pub recipient_npk_vec: [JubJubAffine; TX_OUTPUT_NOTES], + /// Signatures of 'payload_hash' verifiable using 'pk_A' and 'pk_B' + pub sig_vec: [Signature; TX_OUTPUT_NOTES], + /// Asymmetric encryption of 'pk_A' using both recipients 'npk' + pub enc_A_vec: [(JubJubExtended, JubJubExtended); TX_OUTPUT_NOTES], + /// Asymmetric encryption of 'pk_B' using both recipients 'npk' + pub enc_B_vec: [(JubJubExtended, JubJubExtended); TX_OUTPUT_NOTES], + /// Randomness needed to encrypt/decrypt 'pk_A' + pub r_A_vec: [JubJubScalar; TX_OUTPUT_NOTES], + /// Randomness needed to encrypt/decrypt 'pk_B' + pub r_B_vec: [JubJubScalar; TX_OUTPUT_NOTES], +} + +impl Default for RecipientParameters { + fn default() -> Self { + let mut rng = StdRng::seed_from_u64(0xbeef); + + let sk = SecretKey::random(&mut rng); + let sender_pk = PublicKey::from(&sk); + + Self { + sender_pk, + recipient_npk_vec: [ + JubJubAffine::default(), + JubJubAffine::default(), + ], + sig_vec: [Signature::default(), Signature::default()], + enc_A_vec: [(JubJubExtended::default(), JubJubExtended::default()); + TX_OUTPUT_NOTES], + enc_B_vec: [(JubJubExtended::default(), JubJubExtended::default()); + TX_OUTPUT_NOTES], + r_A_vec: [JubJubScalar::default(); TX_OUTPUT_NOTES], + r_B_vec: [JubJubScalar::default(); TX_OUTPUT_NOTES], + } + } +} + +/// Gadget to prove a valid origin for a given transaction. +pub(crate) fn gadget( + composer: &mut Composer, + rp: &RecipientParameters, + payload_hash: Witness, +) -> Result<(), Error> { + // VERIFY A SIGNATURE FOR EACH KEY 'A' AND 'B' + let pk_A = composer.append_point(rp.sender_pk.A()); + let pk_B = composer.append_point(rp.sender_pk.B()); + + let sig_A_u = composer.append_witness(*rp.sig_vec[0].u()); + let sig_A_R = composer.append_point(rp.sig_vec[0].R()); + + let sig_B_u = composer.append_witness(*rp.sig_vec[1].u()); + let sig_B_R = composer.append_point(rp.sig_vec[1].R()); + + gadgets::verify_signature(composer, sig_A_u, sig_A_R, pk_A, payload_hash)?; + gadgets::verify_signature(composer, sig_B_u, sig_B_R, pk_B, payload_hash)?; + + // ENCRYPT EACH KEY 'A' and 'B' USING EACH OUTPUT 'NPK' + let note_pk_1 = composer.append_public_point(rp.recipient_npk_vec[0]); + let note_pk_2 = composer.append_public_point(rp.recipient_npk_vec[1]); + + let r_A_1 = composer.append_witness(rp.r_A_vec[0]); + let r_A_2 = composer.append_witness(rp.r_A_vec[1]); + + let r_B_1 = composer.append_witness(rp.r_B_vec[0]); + let r_B_2 = composer.append_witness(rp.r_B_vec[1]); + + let (enc_A_1_c1, enc_A_1_c2) = + elgamal::encrypt_gadget(composer, note_pk_1, pk_A, r_A_1)?; + let (enc_A_2_c1, enc_A_2_c2) = + elgamal::encrypt_gadget(composer, note_pk_2, pk_A, r_A_2)?; + + let (enc_B_1_c1, enc_B_1_c2) = + elgamal::encrypt_gadget(composer, note_pk_1, pk_B, r_B_1)?; + let (enc_B_2_c1, enc_B_2_c2) = + elgamal::encrypt_gadget(composer, note_pk_2, pk_B, r_B_2)?; + + composer.assert_equal_public_point(enc_A_1_c1, rp.enc_A_vec[0].0); + composer.assert_equal_public_point(enc_A_1_c2, rp.enc_A_vec[0].1); + composer.assert_equal_public_point(enc_A_2_c1, rp.enc_A_vec[1].0); + composer.assert_equal_public_point(enc_A_2_c2, rp.enc_A_vec[1].1); + + composer.assert_equal_public_point(enc_B_1_c1, rp.enc_B_vec[0].0); + composer.assert_equal_public_point(enc_B_1_c2, rp.enc_B_vec[0].1); + composer.assert_equal_public_point(enc_B_2_c1, rp.enc_B_vec[1].0); + composer.assert_equal_public_point(enc_B_2_c2, rp.enc_B_vec[1].1); + + Ok(()) +} diff --git a/circuits/src/transaction.rs b/circuits/src/transaction.rs index dfb1cd2..6b6591b 100644 --- a/circuits/src/transaction.rs +++ b/circuits/src/transaction.rs @@ -5,7 +5,7 @@ // Copyright (c) DUSK NETWORK. All rights reserved. use dusk_jubjub::{ - JubJubAffine, JubJubScalar, GENERATOR, GENERATOR_EXTENDED, GENERATOR_NUMS, + JubJubAffine, JubJubScalar, GENERATOR, GENERATOR_NUMS, GENERATOR_NUMS_EXTENDED, }; use dusk_plonk::prelude::*; @@ -21,6 +21,8 @@ use alloc::vec::Vec; use phoenix_core::{Error as PhoenixError, Note, Ownable, SecretKey, ViewKey}; +use crate::{recipient, recipient::RecipientParameters}; + const TX_OUTPUT_NOTES: usize = 2; /// Struct representing a note willing to be spent, in a way @@ -57,7 +59,7 @@ impl TxInputNote { note: &Note, merkle_opening: poseidon_merkle::Opening<(), H>, sk: &SecretKey, - skeleton_hash: BlsScalar, + payload_hash: BlsScalar, ) -> Result, PhoenixError> { let note_sk = sk.gen_note_sk(note); let note_pk_p = @@ -72,7 +74,7 @@ impl TxInputNote { &[note_pk_p.get_u(), note_pk_p.get_v(), (*note.pos()).into()], )[0]; - let signature = note_sk.sign_double(rng, skeleton_hash); + let signature = note_sk.sign_double(rng, payload_hash); Ok(crate::transaction::TxInputNote { merkle_opening, @@ -139,12 +141,11 @@ struct WitnessTxOutputNote { impl TxOutputNote { /// Crate a new `TxOutputNote`. - pub fn new(value: u64, blinding_factor: JubJubScalar) -> Self { - let value_commitment = JubJubAffine::from( - (GENERATOR_EXTENDED * JubJubScalar::from(value)) - + (GENERATOR_NUMS_EXTENDED * blinding_factor), - ); - + pub fn new( + value: u64, + value_commitment: JubJubAffine, + blinding_factor: JubJubScalar, + ) -> Self { Self { value, value_commitment, @@ -184,22 +185,20 @@ impl TxOutputNote { /// deposit, where a deposit refers to funds being transfered to a contract. /// /// The gadget appends the following public input values to the circuit: -/// - `skeleton_hash` /// - `root` /// - `[nullifier; I]` /// - `[output_value_commitment; 2]` /// - `max_fee` /// - `deposit` -pub fn gadget( +fn nullify_gadget( composer: &mut Composer, - skeleton_hash: &BlsScalar, + payload_hash: &Witness, root: &BlsScalar, tx_input_notes: &[TxInputNote; I], tx_output_notes: &[TxOutputNote; TX_OUTPUT_NOTES], max_fee: u64, deposit: u64, ) -> Result<(), Error> { - let skeleton_hash_pi = composer.append_public(*skeleton_hash); let root_pi = composer.append_public(*root); let mut tx_input_notes_sum = Composer::ZERO; @@ -217,7 +216,7 @@ pub fn gadget( w_tx_input_note.signature_r_p, w_tx_input_note.note_pk, w_tx_input_note.note_pk_p, - skeleton_hash_pi, + *payload_hash, )?; // COMPUTE AND ASSERT THE NULLIFIER @@ -329,10 +328,11 @@ pub fn gadget( pub struct TxCircuit { tx_input_notes: [TxInputNote; I], tx_output_notes: [TxOutputNote; TX_OUTPUT_NOTES], - skeleton_hash: BlsScalar, + payload_hash: BlsScalar, root: BlsScalar, deposit: u64, max_fee: u64, + rp: RecipientParameters, } impl Default for TxCircuit { @@ -342,7 +342,7 @@ impl Default for TxCircuit { let sk = SecretKey::random(&mut rng); let mut tree = Tree::<(), H>::new(); - let skeleton_hash = BlsScalar::default(); + let payload_hash = BlsScalar::default(); let mut tx_input_notes = Vec::new(); let note = Note::empty(); @@ -359,7 +359,7 @@ impl Default for TxCircuit { ¬e, merkle_opening, &sk, - skeleton_hash, + payload_hash, ) .expect("Note created properly."); @@ -379,13 +379,16 @@ impl Default for TxCircuit { let deposit = u64::default(); let max_fee = u64::default(); + let rp = RecipientParameters::default(); + Self { tx_input_notes: tx_input_notes.try_into().unwrap(), tx_output_notes, - skeleton_hash, + payload_hash, root, deposit, max_fee, + rp, } } } @@ -395,33 +398,53 @@ impl TxCircuit { pub fn new( tx_input_notes: [TxInputNote; I], tx_output_notes: [TxOutputNote; TX_OUTPUT_NOTES], - skeleton_hash: BlsScalar, + payload_hash: BlsScalar, root: BlsScalar, deposit: u64, max_fee: u64, + rp: RecipientParameters, ) -> Self { Self { tx_input_notes, tx_output_notes, - skeleton_hash, + payload_hash, root, deposit, max_fee, + rp, } } } impl Circuit for TxCircuit { + /// The circuit has the following public inputs: + /// - `payload_hash` + /// - `root` + /// - `[nullifier; I]` + /// - `[output_value_commitment; 2]` + /// - `max_fee` + /// - `deposit` + /// - `(npk_1, npk_2)` + /// - `(enc_A_npk_1, enc_A_npk_2)` + /// - `(enc_B_npk_1, enc_B_npk_2)` fn circuit(&self, composer: &mut Composer) -> Result<(), Error> { - gadget::( + // Make the payload hash a public input of the circuit + let payload_hash = composer.append_public(self.payload_hash); + + // Nullify all the tx input notes + nullify_gadget::( composer, - &self.skeleton_hash, + &payload_hash, &self.root, &self.tx_input_notes, &self.tx_output_notes, self.max_fee, self.deposit, )?; + + // Prove correctness of the sender keys encryption + recipient::gadget(composer, &self.rp, payload_hash)?; + Ok(()) } } diff --git a/circuits/tests/transaction.rs b/circuits/tests/transaction.rs index a186eee..703fc2b 100644 --- a/circuits/tests/transaction.rs +++ b/circuits/tests/transaction.rs @@ -8,11 +8,19 @@ use rand::rngs::StdRng; use rand::SeedableRng; use rand::{CryptoRng, RngCore}; -use dusk_jubjub::JubJubScalar; -use phoenix_circuits::transaction::{TxCircuit, TxInputNote, TxOutputNote}; -use phoenix_core::{Note, PublicKey, SecretKey}; +use dusk_jubjub::{JubJubScalar, GENERATOR_EXTENDED, GENERATOR_NUMS_EXTENDED}; use dusk_plonk::prelude::*; +use ff::Field; +use jubjub_schnorr::SecretKey as SchnorrSecretKey; + +use phoenix_circuits::{ + elgamal, + transaction::{TxCircuit, TxInputNote, TxOutputNote}, + RecipientParameters, +}; +use phoenix_core::{Note, PublicKey, SecretKey}; + use poseidon_merkle::{Item, Tree}; #[macro_use] @@ -26,25 +34,26 @@ const HEIGHT: usize = 17; struct TestingParameters { pp: PublicParameters, tx_input_notes: [TxInputNote; 4], - skeleton_hash: BlsScalar, + payload_hash: BlsScalar, root: BlsScalar, deposit: u64, max_fee: u64, + rp: RecipientParameters, } lazy_static! { static ref TP: TestingParameters = { - let mut rng = StdRng::seed_from_u64(0xc0b); + let mut rng = StdRng::seed_from_u64(0xc0b); let pp = PublicParameters::setup(1 << CAPACITY, &mut rng).unwrap(); let sk = SecretKey::random(&mut rng); let mut tree = Tree::<(), HEIGHT>::new(); - let skeleton_hash = BlsScalar::from(1234u64); + let payload_hash = BlsScalar::from(1234u64); // create and insert into the tree 4 testing tx input notes let tx_input_notes = - create_test_tx_input_notes::<4>(&mut rng, &mut tree, &sk, skeleton_hash); + create_test_tx_input_notes::<4>(&mut rng, &mut tree, &sk, payload_hash); // retrieve the root from the tree after inserting the notes let root = tree.root().hash; @@ -52,7 +61,75 @@ lazy_static! { let deposit = 5; let max_fee = 5; - TestingParameters { pp, tx_input_notes, skeleton_hash, root, deposit, max_fee } + // Compute the tx output note public keys using + // the recipient public key + let recipient_pk = PublicKey::from(&sk); + + let r = JubJubScalar::random(&mut rng); + let sa = recipient_pk.gen_stealth_address(&r); + let note_pk_1 = sa.note_pk(); + + let r = JubJubScalar::random(&mut rng); + let sa = recipient_pk.gen_stealth_address(&r); + let note_pk_2 = sa.note_pk(); + + let recipient_npk_vec = [ + JubJubAffine::from(note_pk_1.as_ref()), + JubJubAffine::from(note_pk_2.as_ref()), + ]; + + // Encrypt the public key of the sender. We need to encrypt + // both 'A' and 'B', using both tx output note public keys. + // We use the same 'sk' just for testing. + let sender_pk = PublicKey::from(&sk); + + #[allow(non_snake_case)] + let r_A_1 = JubJubScalar::random(&mut rng); + #[allow(non_snake_case)] + let (A_enc_1_c1, A_enc_1_c2) = + elgamal::encrypt(note_pk_1.as_ref(), &sender_pk.A(), &r_A_1); + + #[allow(non_snake_case)] + let r_B_1 = JubJubScalar::random(&mut rng); + #[allow(non_snake_case)] + let (B_enc_1_c1, B_enc_1_c2) = + elgamal::encrypt(note_pk_1.as_ref(), &sender_pk.B(), &r_B_1); + #[allow(non_snake_case)] + let r_A_2 = JubJubScalar::random(&mut rng); + #[allow(non_snake_case)] + let (A_enc_2_c1, A_enc_2_c2) = + elgamal::encrypt(note_pk_2.as_ref(), &sender_pk.A(), &r_A_2); + + #[allow(non_snake_case)] + let r_B_2 = JubJubScalar::random(&mut rng); + #[allow(non_snake_case)] + let (B_enc_2_c1, B_enc_2_c2) = + elgamal::encrypt(note_pk_2.as_ref(), &sender_pk.B(), &r_B_2); + + #[allow(non_snake_case)] + let enc_A_vec = [(A_enc_1_c1, A_enc_1_c2), (A_enc_2_c1, A_enc_2_c2)]; + #[allow(non_snake_case)] + let enc_B_vec = [(B_enc_1_c1, B_enc_1_c2), (B_enc_2_c1, B_enc_2_c2)]; + + #[allow(non_snake_case)] + let r_A_vec = [r_A_1, r_A_2]; + #[allow(non_snake_case)] + let r_B_vec = [r_B_1, r_B_2]; + + // Sign the payload hash using both 'a' and 'b' + let schnorr_sk_a = SchnorrSecretKey::from(sk.a()); + #[allow(non_snake_case)] + let sig_A = schnorr_sk_a.sign(&mut rng, payload_hash); + + let schnorr_sk_b = SchnorrSecretKey::from(sk.b()); + #[allow(non_snake_case)] + let sig_B = schnorr_sk_b.sign(&mut rng, payload_hash); + + let sig_vec = [sig_A, sig_B]; + + let rp = RecipientParameters { sender_pk, recipient_npk_vec, sig_vec, enc_A_vec, enc_B_vec, r_A_vec, r_B_vec }; + + TestingParameters { pp, tx_input_notes, payload_hash, root, deposit, max_fee, rp } }; } @@ -79,7 +156,7 @@ fn create_test_tx_input_notes( rng: &mut (impl RngCore + CryptoRng), tree: &mut Tree<(), HEIGHT>, sk: &SecretKey, - skeleton_hash: BlsScalar, + payload_hash: BlsScalar, ) -> [TxInputNote; I] { let pk = PublicKey::from(sk); @@ -97,14 +174,9 @@ fn create_test_tx_input_notes( let mut input_notes = Vec::new(); for i in 0..I { let merkle_opening = tree.opening(*notes[i].pos()).expect("Tree read."); - let input_note = TxInputNote::new( - rng, - ¬es[i], - merkle_opening, - &sk, - skeleton_hash, - ) - .expect("Note created properly."); + let input_note = + TxInputNote::new(rng, ¬es[i], merkle_opening, &sk, payload_hash) + .expect("Note created properly."); input_notes.push(input_note); } @@ -116,7 +188,12 @@ fn create_test_tx_input_notes( fn create_test_tx_output_note(value: u64) -> TxOutputNote { let blinding_factor = JubJubScalar::from(42u64); - TxOutputNote::new(value, blinding_factor) + let value_commitment = JubJubAffine::from( + (GENERATOR_EXTENDED * JubJubScalar::from(value)) + + (GENERATOR_NUMS_EXTENDED * blinding_factor), + ); + + TxOutputNote::new(value, value_commitment, blinding_factor) } #[test] @@ -141,10 +218,11 @@ fn test_transfer_circuit_1_2() { &TxCircuit::new( input_notes, tx_output_notes, - TP.skeleton_hash, + TP.payload_hash, TP.root, TP.deposit, TP.max_fee, + TP.rp, ), ) .expect("failed to prove"); @@ -177,10 +255,11 @@ fn test_transfer_circuit_2_2() { &TxCircuit::new( input_notes, tx_output_notes, - TP.skeleton_hash, + TP.payload_hash, TP.root, TP.deposit, TP.max_fee, + TP.rp, ), ) .expect("failed to prove"); @@ -216,10 +295,11 @@ fn test_transfer_circuit_3_2() { &TxCircuit::new( input_notes, tx_output_notes, - TP.skeleton_hash, + TP.payload_hash, TP.root, TP.deposit, TP.max_fee, + TP.rp, ), ) .expect("failed to prove"); @@ -249,10 +329,11 @@ fn test_transfer_circuit_4_2() { &TxCircuit::new( TP.tx_input_notes.clone(), tx_output_notes, - TP.skeleton_hash, + TP.payload_hash, TP.root, TP.deposit, TP.max_fee, + TP.rp, ), ) .expect("failed to prove");