From 2658bbd36fa5485b5781db989f663fc01094f76d Mon Sep 17 00:00:00 2001 From: xevisalle Date: Mon, 13 May 2024 17:39:02 +0200 Subject: [PATCH] Add transfer circuit --- Cargo.toml | 16 +- src/lib.rs | 3 + src/note.rs | 9 +- src/transfer.rs | 405 ++++++++++++++++++++ tests/{gadgets.rs => encryption_gadgets.rs} | 0 tests/note_test.rs | 6 +- tests/transfer_gadget.rs | 234 +++++++++++ 7 files changed, 665 insertions(+), 8 deletions(-) create mode 100644 src/transfer.rs rename tests/{gadgets.rs => encryption_gadgets.rs} (100%) create mode 100644 tests/transfer_gadget.rs diff --git a/Cargo.toml b/Cargo.toml index 5cf6568..79c76f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,18 +16,20 @@ dusk-bls12_381 = { version = "0.13", default-features = false } bls12_381-bls = { version = "0.3", default-features = false } dusk-jubjub = { version = "0.14", default-features = false, features = ["zeroize"] } dusk-poseidon = { version = "0.33", default-features = false } -jubjub-schnorr = { version = "0.3", default-features = false } +poseidon-merkle = { version = "0.5", features = ["rkyv-impl", "zk", "size_32"] } +jubjub-schnorr = { version = "0.3", default-features = false, features = ["double"] } subtle = { version = "^2.2.1", default-features = false } ff = { version = "0.13", default-features = false } aes-gcm = "0.10" zeroize = { version = "1", default-features = false, features = ["derive"] } rkyv = { version = "0.7", optional = true, default-features = false } bytecheck = { version = "0.6", optional = true, default-features = false } +rand = "0.8" [dev-dependencies] assert_matches = "1.3" -rand = "0.8" rkyv = { version = "0.7", default-features = false, features = ["size_32"] } +lazy_static = "1.4" [features] default = [] # "alloc" is suggested as default feature but would be breaking change @@ -43,9 +45,15 @@ rkyv-impl = [ ] zk = [ "dusk-plonk", + "jubjub-schnorr/alloc", ] [[test]] -name = "gadgets" -path = "tests/gadgets.rs" +name = "encryption_gadgets" +path = "tests/encryption_gadgets.rs" +required-features = ["zk"] + +[[test]] +name = "transfer_gadget" +path = "tests/transfer_gadget.rs" required-features = ["zk"] diff --git a/src/lib.rs b/src/lib.rs index 396d484..348b06a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,6 +23,9 @@ pub mod error; pub mod fee; /// Transparent and Obfuscated Notes pub mod note; +/// Transfer gadgets +#[cfg(feature = "zk")] +pub mod transfer; /// Phoenix Core Keys & Addresses mod keys; diff --git a/src/note.rs b/src/note.rs index 4d74604..24d6348 100644 --- a/src/note.rs +++ b/src/note.rs @@ -17,6 +17,7 @@ use dusk_jubjub::{ use crate::aes; use dusk_poseidon::sponge::hash; + use ff::Field; use rand_core::{CryptoRng, RngCore}; @@ -264,7 +265,7 @@ impl Note { } /// Return the type of the note - pub const fn note(&self) -> NoteType { + pub const fn note_type(&self) -> NoteType { self.note_type } @@ -331,6 +332,12 @@ impl Ownable for Note { } } +impl Ownable for &Note { + fn stealth_address(&self) -> &StealthAddress { + &self.stealth_address + } +} + // Serialize into 105 + ENCRYPTION_SIZE bytes, where 105 is the size of all the // note elements without the encryption. ENCRYPTION_SIZE = PLAINTEXT_SIZE + // ENCRYPTION_EXTRA_SIZE diff --git a/src/transfer.rs b/src/transfer.rs new file mode 100644 index 0000000..5b9e633 --- /dev/null +++ b/src/transfer.rs @@ -0,0 +1,405 @@ +// 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, GENERATOR, GENERATOR_NUMS, GENERATOR_NUMS_EXTENDED, +}; +use dusk_plonk::prelude::*; +use dusk_poseidon::sponge; +use jubjub_schnorr::{gadgets, SignatureDouble}; +use poseidon_merkle::{zk::opening_gadget, Item, Opening, Tree}; + +use rand::rngs::StdRng; +use rand_core::{CryptoRng, RngCore, SeedableRng}; + +extern crate alloc; +use alloc::vec::Vec; + +use crate::Note; +use crate::{Error as PhoenixError, SecretKey, ViewKey}; + +const OUTPUT: usize = 2; + +/// Struct representing a note willing to be spent, in a way +/// suitable for being introduced in the transfer circuit +#[derive(Debug, Clone)] +pub struct InputNote { + pub(crate) merkle_opening: Opening<(), H, A>, + pub(crate) note: Note, + pub(crate) note_pk_p: JubJubAffine, + pub(crate) value: u64, + pub(crate) blinding_factor: JubJubScalar, + pub(crate) nullifier: BlsScalar, + pub(crate) signature: SignatureDouble, +} + +#[derive(Debug, Clone)] +struct WitnessInputNote { + note_pk: WitnessPoint, + note_pk_p: WitnessPoint, + note_type: Witness, + pos: Witness, + value: Witness, + blinding_factor: Witness, + nullifier: Witness, + signature_u: Witness, + signature_r: WitnessPoint, + signature_r_p: WitnessPoint, +} + +impl InputNote { + /// Create a circuit input note + pub fn new( + note: &Note, + merkle_opening: poseidon_merkle::Opening<(), H, A>, + sk: &SecretKey, + skeleteon_hash: BlsScalar, + rng: &mut (impl RngCore + CryptoRng), + ) -> Result, PhoenixError> { + let note_sk = sk.gen_note_sk(note); + let note_pk_p = + JubJubAffine::from(GENERATOR_NUMS_EXTENDED * note_sk.as_ref()); + + let vk = ViewKey::from(sk); + let value = note.value(Some(&vk))?; + let blinding_factor = note.blinding_factor(Some(&vk))?; + + let nullifier = sponge::hash(&[ + note_pk_p.get_u(), + note_pk_p.get_v(), + note.pos.into(), + ]); + + let signature = note_sk.sign_double(rng, skeleteon_hash); + + Ok(crate::transfer::InputNote { + merkle_opening, + note: note.clone(), + note_pk_p, + value, + blinding_factor, + nullifier, + signature, + }) + } + + fn append_to_circuit(&self, composer: &mut Composer) -> WitnessInputNote { + 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); + + let note_type = composer + .append_witness(BlsScalar::from(self.note.note_type() as u64)); + let pos = composer.append_witness(BlsScalar::from(*self.note.pos())); + + let value = composer.append_witness(self.value); + let blinding_factor = composer.append_witness(self.blinding_factor); + + 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()); + + WitnessInputNote { + note_pk, + note_pk_p, + + note_type, + pos, + value, + blinding_factor, + + nullifier, + + signature_u, + signature_r, + signature_r_p, + } + } +} + +/// Struct representing a note willing to be created, in a way +/// suitable for being introduced in the transfer circuit +#[derive(Debug, Clone)] +pub struct OutputNote { + pub(crate) value: u64, + pub(crate) value_commitment: JubJubAffine, + pub(crate) blinding_factor: JubJubScalar, +} + +#[derive(Debug, Clone)] +struct WitnessOutputNote { + value: Witness, + value_commitment: WitnessPoint, + blinding_factor: Witness, +} + +impl OutputNote { + /// Create a circuit output note + pub fn new( + note: &Note, + vk: &ViewKey, + ) -> Result { + Ok(crate::transfer::OutputNote { + value: note.value(Some(vk))?, + value_commitment: note.value_commitment.into(), + blinding_factor: note.blinding_factor(Some(vk))?, + }) + } + + fn append_to_circuit(&self, composer: &mut Composer) -> WitnessOutputNote { + let value = composer.append_witness(self.value); + let value_commitment = + composer.append_public_point(self.value_commitment); + let blinding_factor = composer.append_witness(self.blinding_factor); + + WitnessOutputNote { + value, + value_commitment, + blinding_factor, + } + } +} + +/// Transfer gadget expecting I input notes to be spent and O output +/// notes to be created. +pub fn gadget( + composer: &mut Composer, + input_notes: &[InputNote; I], + output_notes: &[OutputNote; OUTPUT], + skeleton_hash: &BlsScalar, + root: &BlsScalar, + crossover: u64, + max_fee: u64, +) -> Result<(), Error> { + let skeleton_hash_pi = composer.append_public(*skeleton_hash); + let root_pi = composer.append_public(*root); + + let mut input_notes_sum = Composer::ZERO; + + // NULLIFY ALL INPUT NOTES + for input_note in input_notes { + // APPEND THE WITNESSES TO THE CIRCUIT + let w_input_note = input_note.append_to_circuit(composer); + + // VERIFY THE DOUBLE KEY SCHNORR SIGNATURE + gadgets::verify_signature_double( + composer, + w_input_note.signature_u, + w_input_note.signature_r, + w_input_note.signature_r_p, + w_input_note.note_pk, + w_input_note.note_pk_p, + skeleton_hash_pi, + )?; + + // COMPUTE AND ASSERT THE NULLIFIER + let nullifier = sponge::gadget( + composer, + &[ + *w_input_note.note_pk_p.x(), + *w_input_note.note_pk_p.y(), + w_input_note.pos, + ], + ); + composer.assert_equal(nullifier, w_input_note.nullifier); + + // PERFORM A RANGE CHECK ([0, 2^64 - 1]) ON THE VALUE OF THE NOTE + composer.component_range::<32>(w_input_note.value); + + // SUM UP ALL THE SPENT VALUES + let constraint = Constraint::new() + .left(1) + .a(input_notes_sum) + .right(1) + .b(w_input_note.value); + input_notes_sum = composer.gate_add(constraint); + + // COMMIT TO THE VALUE OF THE NOTE + let pc_1 = + composer.component_mul_generator(w_input_note.value, GENERATOR)?; + let pc_2 = composer.component_mul_generator( + w_input_note.blinding_factor, + GENERATOR_NUMS, + )?; + let value_commitment = composer.component_add_point(pc_1, pc_2); + + // COMPUTE THE NOTE HASH + let note_hash = sponge::gadget( + composer, + &[ + w_input_note.note_type, + *value_commitment.x(), + *value_commitment.y(), + *w_input_note.note_pk.x(), + *w_input_note.note_pk.y(), + w_input_note.pos, + ], + ); + + // VERIFY THE MERKLE OPENING + let root = + opening_gadget(composer, &input_note.merkle_opening, note_hash); + composer.assert_equal(root, root_pi); + } + + let mut output_sum = Composer::ZERO; + + // COMMIT TO ALL OUTPUT NOTES + for output_note in output_notes { + // APPEND THE WITNESSES TO THE CIRCUIT + let w_output_note = output_note.append_to_circuit(composer); + + // PERFORM A RANGE CHECK ([0, 2^64 - 1]) ON THE VALUE OF THE NOTE + composer.component_range::<32>(w_output_note.value); + + // SUM UP ALL THE CREATED NOTE VALUES + let constraint = Constraint::new() + .left(1) + .a(output_sum) + .right(1) + .b(w_output_note.value); + output_sum = composer.gate_add(constraint); + + // COMMIT TO THE VALUE OF THE NOTE + let pc_1 = + composer.component_mul_generator(w_output_note.value, GENERATOR)?; + let pc_2 = composer.component_mul_generator( + w_output_note.blinding_factor, + GENERATOR_NUMS, + )?; + let value_commitment = composer.component_add_point(pc_1, pc_2); + + composer.assert_equal_point( + w_output_note.value_commitment, + value_commitment, + ); + } + + let max_fee = composer.append_public(max_fee); + let crossover = composer.append_public(crossover); + + // SUM UP THE CROSSOVER AND THE MAX FEE + let constraint = Constraint::new() + .left(1) + .a(output_sum) + .right(1) + .b(max_fee) + .fourth(1) + .d(crossover); + output_sum = composer.gate_add(constraint); + + // VERIFY BALANCE + composer.assert_equal(input_notes_sum, output_sum); + + Ok(()) +} + +/// Declaration of the transfer circuit +#[derive(Debug)] +pub struct TransferCircuit { + input_notes: [InputNote; I], + output_notes: [OutputNote; OUTPUT], + skeleton_hash: BlsScalar, + root: BlsScalar, + crossover: u64, + max_fee: u64, +} + +impl Default + for TransferCircuit +{ + fn default() -> Self { + let mut rng = StdRng::seed_from_u64(0xbeef); + + let sk = SecretKey::random(&mut rng); + let vk = ViewKey::from(&sk); + + let mut tree = Tree::<(), H, A>::new(); + let skeleton_hash = BlsScalar::default(); + + let mut input_notes = Vec::new(); + let note = Note::empty(); + let item = Item { + hash: note.hash(), + data: (), + }; + tree.insert(*note.pos(), item); + + for _ in 0..I { + let merkle_opening = tree.opening(*note.pos()).expect("Tree read."); + let input_note = InputNote::new( + ¬e, + merkle_opening, + &sk, + skeleton_hash, + &mut rng, + ) + .expect("Note created properly."); + + input_notes.push(input_note); + } + + let output_note_1 = + OutputNote::new(¬e, &vk).expect("Note created properly."); + let output_note_2 = + OutputNote::new(¬e, &vk).expect("Note created properly."); + + let output_notes = [output_note_1, output_note_2]; + + let root = BlsScalar::default(); + let crossover = u64::default(); + let max_fee = u64::default(); + + Self { + input_notes: input_notes.try_into().unwrap(), + output_notes, + skeleton_hash, + root, + crossover, + max_fee, + } + } +} + +impl TransferCircuit { + /// Create a new transfer circuit + pub fn new( + input_notes: [InputNote; I], + output_notes: [OutputNote; OUTPUT], + skeleton_hash: BlsScalar, + root: BlsScalar, + crossover: u64, + max_fee: u64, + ) -> Self { + Self { + input_notes, + output_notes, + skeleton_hash, + root, + crossover, + max_fee, + } + } +} + +impl Circuit + for TransferCircuit +{ + fn circuit(&self, composer: &mut Composer) -> Result<(), Error> { + gadget::( + composer, + &self.input_notes, + &self.output_notes, + &self.skeleton_hash, + &self.root, + self.crossover, + self.max_fee, + )?; + Ok(()) + } +} diff --git a/tests/gadgets.rs b/tests/encryption_gadgets.rs similarity index 100% rename from tests/gadgets.rs rename to tests/encryption_gadgets.rs diff --git a/tests/note_test.rs b/tests/note_test.rs index 856d4e9..b8b7814 100644 --- a/tests/note_test.rs +++ b/tests/note_test.rs @@ -23,7 +23,7 @@ fn transparent_note() -> Result<(), Error> { let note = Note::transparent(&mut rng, &pk, value); - assert_eq!(note.note(), NoteType::Transparent); + assert_eq!(note.note_type(), NoteType::Transparent); assert_eq!(value, note.value(None)?); Ok(()) @@ -43,7 +43,7 @@ fn transparent_stealth_note() -> Result<(), Error> { let note = Note::transparent_stealth(sa, value); - assert_eq!(note.note(), NoteType::Transparent); + assert_eq!(note.note_type(), NoteType::Transparent); assert_eq!(value, note.value(None)?); assert_eq!(sa, *note.stealth_address()); @@ -62,7 +62,7 @@ fn obfuscated_note() -> Result<(), Error> { let blinding_factor = JubJubScalar::random(&mut rng); let note = Note::obfuscated(&mut rng, &pk, value, blinding_factor); - assert_eq!(note.note(), NoteType::Obfuscated); + assert_eq!(note.note_type(), NoteType::Obfuscated); assert_eq!(value, note.value(Some(&vk))?); Ok(()) diff --git a/tests/transfer_gadget.rs b/tests/transfer_gadget.rs new file mode 100644 index 0000000..d363157 --- /dev/null +++ b/tests/transfer_gadget.rs @@ -0,0 +1,234 @@ +// 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 rand_core::{CryptoRng, OsRng, RngCore}; + +use phoenix_core::transfer::{InputNote, OutputNote}; +use phoenix_core::{ + transfer::TransferCircuit, Note, PublicKey, SecretKey, ViewKey, +}; +use poseidon_merkle::{Item, Tree}; + +use dusk_plonk::prelude::*; + +#[macro_use] +extern crate lazy_static; + +static LABEL: &[u8; 12] = b"dusk-network"; +const CAPACITY: usize = 17; // capacity required for the setup + +const HEIGHT: usize = 17; +const ARITY: usize = 4; + +struct TestingParameters { + pp: PublicParameters, + input_notes: [InputNote; 4], + output_notes: [OutputNote; 2], + skeleton_hash: BlsScalar, + root: BlsScalar, + crossover: u64, + max_fee: u64, +} + +lazy_static! { + static ref TP: TestingParameters = { + let pp = PublicParameters::setup(1 << CAPACITY, &mut OsRng).unwrap(); + + let mut rng = OsRng; + let sk = SecretKey::random(&mut rng); + + let mut tree = Tree::<(), HEIGHT, ARITY>::new(); + + let skeleton_hash = BlsScalar::from(1234u64); + + // create and insert into the tree 4 testing notes + let input_notes = + create_test_input_notes::<4>(&mut tree, &sk, skeleton_hash, &mut rng); + + // retrieve the root from the tree after inserting the notes + let root = tree.root().hash; + + // create 2 testing circuit output notes + let output_notes = [ + create_test_output_note(&sk, &mut rng), + create_test_output_note(&sk, &mut rng), + ]; + + let crossover = 0; + let max_fee = 0; + + TestingParameters { pp, input_notes, output_notes, skeleton_hash, root, crossover, max_fee } + }; +} + +fn create_and_insert_test_note( + tree: &mut Tree<(), HEIGHT, ARITY>, + pk: &PublicKey, + pos: u64, + value: u64, + rng: &mut (impl RngCore + CryptoRng), +) -> Note { + let mut note = Note::transparent(rng, pk, value); + note.set_pos(pos); + + let item = Item { + hash: note.hash(), + data: (), + }; + tree.insert(*note.pos(), item); + + note +} + +fn create_test_input_notes( + tree: &mut Tree<(), HEIGHT, ARITY>, + sk: &SecretKey, + skeleton_hash: BlsScalar, + rng: &mut (impl RngCore + CryptoRng), +) -> [InputNote; I] { + let pk = PublicKey::from(sk); + + let mut notes = Vec::new(); + for i in 0..I { + notes.push(create_and_insert_test_note( + tree, + &pk, + i.try_into().unwrap(), + 0, + rng, + )); + } + + 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 = + InputNote::new(¬es[i], merkle_opening, &sk, skeleton_hash, rng) + .expect("Note created properly."); + + input_notes.push(input_note); + } + + input_notes.try_into().unwrap() +} + +fn create_test_output_note( + sk: &SecretKey, + rng: &mut (impl RngCore + CryptoRng), +) -> OutputNote { + let note = Note::transparent(rng, &PublicKey::from(sk), 0); + OutputNote::new(¬e, &ViewKey::from(sk)).expect("Note created properly.") +} + +#[test] +fn test_transfer_circuit_1_2() { + let (prover, verifier) = + Compiler::compile::>(&TP.pp, LABEL) + .expect("failed to compile circuit"); + + let input_notes = [TP.input_notes[0].clone()]; + + let (proof, public_inputs) = prover + .prove( + &mut OsRng, + &TransferCircuit::new( + input_notes, + TP.output_notes.clone(), + TP.skeleton_hash, + TP.root, + TP.crossover, + TP.max_fee, + ), + ) + .expect("failed to prove"); + + verifier + .verify(&proof, &public_inputs) + .expect("failed to verify proof"); +} + +#[test] +fn test_transfer_circuit_2_2() { + let (prover, verifier) = + Compiler::compile::>(&TP.pp, LABEL) + .expect("failed to compile circuit"); + + let input_notes = [TP.input_notes[0].clone(), TP.input_notes[1].clone()]; + + let (proof, public_inputs) = prover + .prove( + &mut OsRng, + &TransferCircuit::new( + input_notes, + TP.output_notes.clone(), + TP.skeleton_hash, + TP.root, + TP.crossover, + TP.max_fee, + ), + ) + .expect("failed to prove"); + + verifier + .verify(&proof, &public_inputs) + .expect("failed to verify proof"); +} + +#[test] +fn test_transfer_circuit_3_2() { + let (prover, verifier) = + Compiler::compile::>(&TP.pp, LABEL) + .expect("failed to compile circuit"); + + let input_notes = [ + TP.input_notes[0].clone(), + TP.input_notes[1].clone(), + TP.input_notes[2].clone(), + ]; + + let (proof, public_inputs) = prover + .prove( + &mut OsRng, + &TransferCircuit::new( + input_notes, + TP.output_notes.clone(), + TP.skeleton_hash, + TP.root, + TP.crossover, + TP.max_fee, + ), + ) + .expect("failed to prove"); + + verifier + .verify(&proof, &public_inputs) + .expect("failed to verify proof"); +} + +#[test] +fn test_transfer_circuit_4_2() { + let (prover, verifier) = + Compiler::compile::>(&TP.pp, LABEL) + .expect("failed to compile circuit"); + + let (proof, public_inputs) = prover + .prove( + &mut OsRng, + &TransferCircuit::new( + TP.input_notes.clone(), + TP.output_notes.clone(), + TP.skeleton_hash, + TP.root, + TP.crossover, + TP.max_fee, + ), + ) + .expect("failed to prove"); + + verifier + .verify(&proof, &public_inputs) + .expect("failed to verify proof"); +}