From 8a5008b0ebc49bb02ed33e0f248648f080f0530b Mon Sep 17 00:00:00 2001 From: moana Date: Thu, 8 Aug 2024 10:55:24 +0200 Subject: [PATCH] circuits: Implement serialization Resolves #232 --- circuits/CHANGELOG.md | 11 ++ circuits/Cargo.toml | 1 + circuits/src/lib.rs | 250 ++++++++++++++++++++++++++++++-- circuits/tests/serialization.rs | 208 ++++++++++++++++++++++++++ 4 files changed, 457 insertions(+), 13 deletions(-) create mode 100644 circuits/tests/serialization.rs diff --git a/circuits/CHANGELOG.md b/circuits/CHANGELOG.md index a52c71d..ffb0183 100644 --- a/circuits/CHANGELOG.md +++ b/circuits/CHANGELOG.md @@ -22,6 +22,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Rename `TxInputNote` to `InputNoteInfo` [#229] - Rename `TxOutputNote` to `OutputNoteInfo` [#229] +### Added + +- Add `dusk-bytes` dependency at v0.1 [#232] +- Add `TxCircuit::from_slice` and `TxCircuit::to_var_bytes` [#232] +- Add `InputNoteInfo::from_slice` and `InputNoteInfo::to_var_bytes` [#232] +- Add `Serializable` trait implementation for `OutputNoteInfo` [#232] +- Add `Clone` and `PartialEq` derives for `TxCircuit` [#232] +- Add `PartialEq` derive for `InputNoteInfo` [#232] +- Add `PartialEq` derive for `OutputNoteInfo` [#232] + ## [0.2.1] - 2024-07-03 ### Changed @@ -70,6 +80,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Update `poseidon-merkle` to v0.6 [#179] +[#232]: https://github.com/dusk-network/phoenix/issues/232 [#229]: https://github.com/dusk-network/phoenix/issues/229 [#214]: https://github.com/dusk-network/phoenix/issues/214 [#201]: https://github.com/dusk-network/phoenix/issues/201 diff --git a/circuits/Cargo.toml b/circuits/Cargo.toml index 35e0da8..6d50c64 100644 --- a/circuits/Cargo.toml +++ b/circuits/Cargo.toml @@ -10,6 +10,7 @@ exclude = [".github/workflows/dusk-ci.yml", ".gitignore"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +dusk-bytes = "0.1" phoenix-core = { version = "0.30", default-features = false, path = "../core" } dusk-plonk = { version = "0.19", default-features = false } dusk-jubjub = { version = "0.14", default-features = false } diff --git a/circuits/src/lib.rs b/circuits/src/lib.rs index ef10d69..50cde79 100644 --- a/circuits/src/lib.rs +++ b/circuits/src/lib.rs @@ -16,11 +16,12 @@ mod sender_enc; /// ElGamal asymmetric cipher pub use encryption::elgamal; +use dusk_bytes::{DeserializableSlice, Error as BytesError, Serializable}; use dusk_jubjub::{JubJubAffine, JubJubScalar, GENERATOR, GENERATOR_NUMS}; use dusk_plonk::prelude::*; use dusk_poseidon::{Domain, HashGadget}; use jubjub_schnorr::{gadgets, Signature as SchnorrSignature, SignatureDouble}; -use poseidon_merkle::{zk::opening_gadget, Item, Opening, Tree}; +use poseidon_merkle::{zk::opening_gadget, Item, Opening, Tree, ARITY}; use phoenix_core::{Note, PublicKey, SecretKey, OUTPUT_NOTES}; @@ -28,7 +29,7 @@ extern crate alloc; use alloc::vec::Vec; /// Declaration of the transaction circuit calling the [`gadget`]. -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq)] pub struct TxCircuit { /// All information needed in relation to the transaction input-notes pub input_notes_info: [InputNoteInfo; I], @@ -209,7 +210,7 @@ impl Circuit for TxCircuit { composer.assert_equal(input_notes_sum, tx_output_sum); // Verify: 6. Sender-data - // appends as public input the note-pk of both output-noes: + // appends as public input the note-pk of both output-notes: // `(npk_out_0, npk_out_1)` // and the encryption of the sender-pk.A and sender-pk.B, // encrypted first with the note-pk of one output note: @@ -237,6 +238,88 @@ impl Circuit for TxCircuit { } } +impl TxCircuit { + const SIZE: usize = I * InputNoteInfo::::SIZE + + OUTPUT_NOTES * OutputNoteInfo::SIZE + + 2 * BlsScalar::SIZE + + 2 * u64::SIZE + + PublicKey::SIZE + + 2 * SchnorrSignature::SIZE; + + /// Serialize a [`TxCircuit`] to a vector of bytes. + // Once the new implementation of the `Serializable` trait becomes + // available, we will want that instead, but for the time being we use + // this implementation. + pub fn to_var_bytes(&self) -> Vec { + let mut bytes = Vec::with_capacity(Self::SIZE); + + for info in self.input_notes_info.iter() { + bytes.extend(info.to_var_bytes()); + } + for info in self.output_notes_info.iter() { + bytes.extend(info.to_bytes()); + } + bytes.extend(self.payload_hash.to_bytes()); + bytes.extend(self.root.to_bytes()); + bytes.extend(self.deposit.to_bytes()); + bytes.extend(self.max_fee.to_bytes()); + bytes.extend(self.sender_pk.to_bytes()); + bytes.extend(self.signatures.0.to_bytes()); + bytes.extend(self.signatures.1.to_bytes()); + + bytes + } + + /// Deserialize a [`TxCircuit`] from a slice of bytes. + /// + /// # Errors + /// + /// Will return [`dusk_bytes::Error`] in case of a deserialization error. + // Once the new implementation of the `Serializable` trait becomes + // available, we will want that instead, but for the time being we use + // this implementation. + pub fn from_slice(bytes: &[u8]) -> Result { + if bytes.len() < Self::SIZE { + return Err(BytesError::BadLength { + found: bytes.len(), + expected: Self::SIZE, + }); + } + + let mut input_notes_info = Vec::new(); + for _ in 0..I { + input_notes_info.push(InputNoteInfo::from_slice(bytes)?); + } + + let mut reader = &bytes[I * InputNoteInfo::::SIZE..]; + + let output_notes_info = [ + OutputNoteInfo::from_reader(&mut reader)?, + OutputNoteInfo::from_reader(&mut reader)?, + ]; + let payload_hash = BlsScalar::from_reader(&mut reader)?; + let root = BlsScalar::from_reader(&mut reader)?; + let deposit = u64::from_reader(&mut reader)?; + let max_fee = u64::from_reader(&mut reader)?; + let sender_pk = PublicKey::from_reader(&mut reader)?; + let signature_0 = SchnorrSignature::from_reader(&mut reader)?; + let signature_1 = SchnorrSignature::from_reader(&mut reader)?; + + Ok(Self { + input_notes_info: input_notes_info + .try_into() + .expect("The vector has exactly I elements"), + output_notes_info, + payload_hash, + root, + deposit, + max_fee, + sender_pk, + signatures: (signature_0, signature_1), + }) + } +} + impl Default for TxCircuit { fn default() -> Self { let sk = @@ -300,21 +383,21 @@ impl Default for TxCircuit { /// Struct holding all information needed by the transfer circuit regarding the /// transaction input-notes. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct InputNoteInfo { - /// the merkle opening for the note + /// The merkle opening for the note pub merkle_opening: Opening<(), H>, - /// the input note + /// The input note pub note: Note, - /// the note-public-key prime + /// The note-public-key prime pub note_pk_p: JubJubAffine, - /// the value associated to the note + /// The value associated to the note pub value: u64, - /// the value blinder used to obfuscate the value + /// The value blinder used to obfuscate the value pub value_blinder: JubJubScalar, - /// the nullifier used to spend the note + /// The nullifier used to spend the note pub nullifier: BlsScalar, - /// the signature of the payload-hash, signed with the note-sk + /// The signature of the payload-hash, signed with the note-sk pub signature: SignatureDouble, } @@ -365,11 +448,78 @@ impl InputNoteInfo { signature_r_p, ) } + + const SIZE: usize = (1 + H * ARITY) * Item::SIZE + + H * (u32::BITS as usize / 8) + + Note::SIZE + + JubJubAffine::SIZE + + u64::SIZE + + JubJubScalar::SIZE + + BlsScalar::SIZE + + SignatureDouble::SIZE; + + /// Serialize an [`InputNoteInfo`] to a vector of bytes. + // Once the new implementation of the `Serializable` trait becomes + // available, we will want that instead, but for the time being we use + // this implementation. + pub fn to_var_bytes(&self) -> Vec { + let mut bytes = Vec::with_capacity(Self::SIZE); + + bytes.extend(self.merkle_opening.to_var_bytes()); + bytes.extend(self.note.to_bytes()); + bytes.extend(self.note_pk_p.to_bytes()); + bytes.extend(self.value.to_bytes()); + bytes.extend(self.value_blinder.to_bytes()); + bytes.extend(self.nullifier.to_bytes()); + bytes.extend(self.signature.to_bytes()); + + bytes + } + + /// Deserialize an [`InputNoteInfo`] from a slice of bytes. + /// + /// # Errors + /// + /// Will return [`dusk_bytes::Error`] in case of a deserialization error. + // Once the new implementation of the `Serializable` trait becomes + // available, we will want that instead, but for the time being we use + // this implementation. + pub fn from_slice(bytes: &[u8]) -> Result { + if bytes.len() < Self::SIZE { + return Err(BytesError::BadLength { + found: bytes.len(), + expected: Self::SIZE, + }); + } + + let merkle_opening_size = + (1 + H * ARITY) * Item::SIZE + H * (u32::BITS as usize / 8); + let merkle_opening = + Opening::<(), H>::from_slice(&bytes[..merkle_opening_size])?; + + let mut buf = &bytes[merkle_opening_size..]; + let note = Note::from_reader(&mut buf)?; + let note_pk_p = JubJubAffine::from_reader(&mut buf)?; + let value = u64::from_reader(&mut buf)?; + let value_blinder = JubJubScalar::from_reader(&mut buf)?; + let nullifier = BlsScalar::from_reader(&mut buf)?; + let signature = SignatureDouble::from_reader(&mut buf)?; + + Ok(Self { + merkle_opening, + note, + note_pk_p, + value, + value_blinder, + nullifier, + signature, + }) + } } /// Struct holding all information needed by the transfer circuit regarding the /// transaction output-notes. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct OutputNoteInfo { /// The value of the note pub value: u64, @@ -377,10 +527,84 @@ pub struct OutputNoteInfo { pub value_commitment: JubJubAffine, /// The blinder used to calculate the value commitment pub value_blinder: JubJubScalar, - /// the public key of the note + /// The public key of the note pub note_pk: JubJubAffine, /// The encrypted sender information of the note pub sender_enc: [(JubJubAffine, JubJubAffine); 2], /// The blinder used to encrypt the sender pub sender_blinder: [JubJubScalar; 2], } + +const OUTPUT_NOTE_INFO_SIZE: usize = u64::SIZE + + JubJubAffine::SIZE + + JubJubScalar::SIZE + + JubJubAffine::SIZE + + 4 * JubJubAffine::SIZE + + 2 * JubJubScalar::SIZE; + +impl Serializable for OutputNoteInfo { + type Error = BytesError; + + fn to_bytes(&self) -> [u8; Self::SIZE] { + let mut bytes = [0u8; Self::SIZE]; + let mut offset = 0; + + bytes[..u64::SIZE].copy_from_slice(&self.value.to_bytes()); + offset += u64::SIZE; + bytes[offset..offset + JubJubAffine::SIZE] + .copy_from_slice(&self.value_commitment.to_bytes()); + offset += JubJubAffine::SIZE; + bytes[offset..offset + JubJubScalar::SIZE] + .copy_from_slice(&self.value_blinder.to_bytes()); + offset += JubJubScalar::SIZE; + bytes[offset..offset + JubJubAffine::SIZE] + .copy_from_slice(&self.note_pk.to_bytes()); + offset += JubJubAffine::SIZE; + bytes[offset..offset + JubJubAffine::SIZE] + .copy_from_slice(&self.sender_enc[0].0.to_bytes()); + offset += JubJubAffine::SIZE; + bytes[offset..offset + JubJubAffine::SIZE] + .copy_from_slice(&self.sender_enc[0].1.to_bytes()); + offset += JubJubAffine::SIZE; + bytes[offset..offset + JubJubAffine::SIZE] + .copy_from_slice(&self.sender_enc[1].0.to_bytes()); + offset += JubJubAffine::SIZE; + bytes[offset..offset + JubJubAffine::SIZE] + .copy_from_slice(&self.sender_enc[1].1.to_bytes()); + offset += JubJubAffine::SIZE; + bytes[offset..offset + JubJubScalar::SIZE] + .copy_from_slice(&self.sender_blinder[0].to_bytes()); + offset += JubJubScalar::SIZE; + bytes[offset..offset + JubJubScalar::SIZE] + .copy_from_slice(&self.sender_blinder[1].to_bytes()); + + bytes + } + + fn from_bytes(bytes: &[u8; Self::SIZE]) -> Result { + let mut reader = &bytes[..]; + + let value = u64::from_reader(&mut reader)?; + let value_commitment = JubJubAffine::from_reader(&mut reader)?; + let value_blinder = JubJubScalar::from_reader(&mut reader)?; + let note_pk = JubJubAffine::from_reader(&mut reader)?; + let sender_enc_0_0 = JubJubAffine::from_reader(&mut reader)?; + let sender_enc_0_1 = JubJubAffine::from_reader(&mut reader)?; + let sender_enc_1_0 = JubJubAffine::from_reader(&mut reader)?; + let sender_enc_1_1 = JubJubAffine::from_reader(&mut reader)?; + let sender_blinder_0 = JubJubScalar::from_reader(&mut reader)?; + let sender_blinder_1 = JubJubScalar::from_reader(&mut reader)?; + + Ok(Self { + value, + value_commitment, + value_blinder, + note_pk, + sender_enc: [ + (sender_enc_0_0, sender_enc_0_1), + (sender_enc_1_0, sender_enc_1_1), + ], + sender_blinder: [sender_blinder_0, sender_blinder_1], + }) + } +} diff --git a/circuits/tests/serialization.rs b/circuits/tests/serialization.rs new file mode 100644 index 0000000..4262b2d --- /dev/null +++ b/circuits/tests/serialization.rs @@ -0,0 +1,208 @@ +// 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::rngs::StdRng; +use rand::SeedableRng; +use rand::{CryptoRng, Rng, RngCore}; + +use dusk_bytes::{Error as BytesError, Serializable}; +use dusk_jubjub::{JubJubAffine, JubJubScalar, GENERATOR_EXTENDED}; +use dusk_plonk::prelude::*; +use ff::Field; +use jubjub_schnorr::{Signature as SchnorrSignature, SignatureDouble}; +use poseidon_merkle::{Item, Tree}; + +use phoenix_circuits::{InputNoteInfo, OutputNoteInfo, TxCircuit}; +use phoenix_core::{Note, PublicKey, SecretKey}; + +const HEIGHT: usize = 17; + +#[test] +fn tx_ciruit_1_2() -> Result<(), BytesError> { + let mut rng = StdRng::seed_from_u64(0xbeef); + + let circuit = random_circuit::<1>(&mut rng); + let circuit_bytes = circuit.to_var_bytes(); + + assert_eq!( + circuit, + TxCircuit::::from_slice(&circuit_bytes[..])? + ); + + Ok(()) +} + +#[test] +fn tx_ciruit_2_2() -> Result<(), BytesError> { + let mut rng = StdRng::seed_from_u64(0xbeef); + + let circuit = random_circuit::<1>(&mut rng); + let circuit_bytes = circuit.to_var_bytes(); + + assert_eq!( + circuit, + TxCircuit::::from_slice(&circuit_bytes[..])? + ); + + Ok(()) +} + +#[test] +fn tx_ciruit_3_2() -> Result<(), BytesError> { + let mut rng = StdRng::seed_from_u64(0xbeef); + + let circuit = random_circuit::<1>(&mut rng); + let circuit_bytes = circuit.to_var_bytes(); + + assert_eq!( + circuit, + TxCircuit::::from_slice(&circuit_bytes[..])? + ); + + Ok(()) +} + +#[test] +fn tx_ciruit_4_2() -> Result<(), BytesError> { + let mut rng = StdRng::seed_from_u64(0xbeef); + + let circuit = random_circuit::<1>(&mut rng); + let circuit_bytes = circuit.to_var_bytes(); + + assert_eq!( + circuit, + TxCircuit::::from_slice(&circuit_bytes[..])? + ); + + Ok(()) +} + +fn random_circuit( + rng: &mut (impl RngCore + CryptoRng), +) -> TxCircuit { + let mut input_notes_info = Vec::new(); + for _ in 0..I { + input_notes_info.push(random_input_note_info(rng)); + } + + let sender_pk = PublicKey::from(&SecretKey::random(rng)); + + let mut signature_0_bytes = [0u8; SchnorrSignature::SIZE]; + // generate random signature_0.u + signature_0_bytes[..32] + .copy_from_slice(&JubJubScalar::random(&mut *rng).to_bytes()[..]); + signature_0_bytes[32..] + .copy_from_slice(&random_jubjub_affine(rng).to_bytes()[..]); + + let mut signature_1_bytes = [0u8; SchnorrSignature::SIZE]; + // generate random signature_1.u + signature_1_bytes[..32] + .copy_from_slice(&JubJubScalar::random(&mut *rng).to_bytes()[..]); + // generate random signature_1.R + signature_0_bytes[32..] + .copy_from_slice(&random_jubjub_affine(rng).to_bytes()[..]); + + TxCircuit { + input_notes_info: input_notes_info + .try_into() + .expect("there are exactly I inputs"), + output_notes_info: [ + random_output_note_info(rng), + random_output_note_info(rng), + ], + payload_hash: BlsScalar::random(&mut *rng), + root: BlsScalar::random(&mut *rng), + deposit: rng.gen(), + max_fee: rng.gen(), + sender_pk, + signatures: ( + SchnorrSignature::from_bytes(&signature_0_bytes) + .expect("the signature bytes should be correct"), + SchnorrSignature::from_bytes(&signature_1_bytes) + .expect("the signature bytes should be correct"), + ), + } +} + +fn random_input_note_info( + rng: &mut (impl RngCore + CryptoRng), +) -> InputNoteInfo { + let pk = PublicKey::from(&SecretKey::random(rng)); + let value_blinder = JubJubScalar::random(&mut *rng); + let sender_blinder = [ + JubJubScalar::random(&mut *rng), + JubJubScalar::random(&mut *rng), + ]; + + let mut note = + Note::obfuscated(rng, &pk, &pk, 42, value_blinder, sender_blinder); + note.set_pos(42); + let mut notes_tree = Tree::<(), HEIGHT>::new(); + let item = Item { + hash: note.hash(), + data: (), + }; + notes_tree.insert(*note.pos(), item); + let merkle_opening = notes_tree + .opening(*note.pos()) + .expect("The note should was added at the given position"); + let note_pk_p = random_jubjub_affine(rng); + + let mut signature_bytes = [0u8; SignatureDouble::SIZE]; + // generate random signature.u + signature_bytes[..JubJubScalar::SIZE] + .copy_from_slice(&JubJubScalar::random(&mut *rng).to_bytes()[..]); + let mut offset = JubJubScalar::SIZE; + // generate random signature.R + signature_bytes[offset..offset + JubJubAffine::SIZE] + .copy_from_slice(&random_jubjub_affine(rng).to_bytes()[..]); + offset += JubJubAffine::SIZE; + // generate random signature.R_prime + signature_bytes[offset..offset + JubJubAffine::SIZE] + .copy_from_slice(&random_jubjub_affine(rng).to_bytes()[..]); + + InputNoteInfo { + merkle_opening, + note, + note_pk_p, + value: rng.gen(), + value_blinder: JubJubScalar::random(&mut *rng), + nullifier: BlsScalar::random(&mut *rng), + signature: SignatureDouble::from_bytes(&signature_bytes) + .expect("signature-bytes to be correct"), + } +} + +fn random_output_note_info( + rng: &mut (impl RngCore + CryptoRng), +) -> OutputNoteInfo { + let value_commitment = random_jubjub_affine(rng); + let note_pk = random_jubjub_affine(rng); + + let sender_enc_0_0 = random_jubjub_affine(rng); + let sender_enc_0_1 = random_jubjub_affine(rng); + let sender_enc_1_0 = random_jubjub_affine(rng); + let sender_enc_1_1 = random_jubjub_affine(rng); + + OutputNoteInfo { + value: rng.gen(), + value_commitment, + value_blinder: JubJubScalar::random(&mut *rng), + note_pk, + sender_enc: [ + (sender_enc_0_0, sender_enc_0_1), + (sender_enc_1_0, sender_enc_1_1), + ], + sender_blinder: [ + JubJubScalar::random(&mut *rng), + JubJubScalar::random(&mut *rng), + ], + } +} + +fn random_jubjub_affine(rng: &mut (impl RngCore + CryptoRng)) -> JubJubAffine { + (GENERATOR_EXTENDED * &JubJubScalar::random(&mut *rng)).into() +}