From cc8c6e31cb21bf699803858f7b30815615f9fd6c Mon Sep 17 00:00:00 2001 From: David Rusu Date: Thu, 13 Jun 2024 12:12:08 -0400 Subject: [PATCH] cl: partial_tx; input; output --- cl/src/error.rs | 4 ++ cl/src/input.rs | 123 +++++++++++++++++++++++++++++++++++++++++++ cl/src/main.rs | 13 +++-- cl/src/note.rs | 108 +++++++++++++++++++++++-------------- cl/src/nullifier.rs | 85 ++++++++++++++++-------------- cl/src/output.rs | 38 +++++++++++++ cl/src/partial_tx.rs | 31 +++++++++++ cl/src/test_util.rs | 7 +++ 8 files changed, 327 insertions(+), 82 deletions(-) create mode 100644 cl/src/error.rs create mode 100644 cl/src/input.rs create mode 100644 cl/src/output.rs create mode 100644 cl/src/partial_tx.rs create mode 100644 cl/src/test_util.rs diff --git a/cl/src/error.rs b/cl/src/error.rs new file mode 100644 index 0000000..03d01d1 --- /dev/null +++ b/cl/src/error.rs @@ -0,0 +1,4 @@ +#[derive(Debug)] +pub enum Error { + ProofFailed, +} diff --git a/cl/src/input.rs b/cl/src/input.rs new file mode 100644 index 0000000..958688f --- /dev/null +++ b/cl/src/input.rs @@ -0,0 +1,123 @@ +/// This module defines the partial transaction structure. +/// +/// Partial transactions, as the name suggests, are transactions +/// which on their own may not balance (i.e. \sum inputs != \sum outputs) +use crate::{ + error::Error, + note::{Note, NoteCommitment}, + nullifier::{Nullifier, NullifierNonce, NullifierSecret}, +}; +use jubjub::{ExtendedPoint, Scalar}; + +#[derive(Debug, PartialEq, Eq)] +pub struct Input { + pub note_comm: NoteCommitment, + pub nullifier: Nullifier, + pub balance: ExtendedPoint, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct InputWitness { + note: Note, + nf_sk: NullifierSecret, + nonce: NullifierNonce, + balance_blinding: Scalar, +} + +// as we don't have SNARKS hooked up yet, the witness will be our proof +pub struct InputProof(InputWitness); + +impl Input { + pub fn from_witness(w: InputWitness) -> Self { + Self { + note_comm: w.note.commit(w.nf_sk.commit(), w.nonce), + nullifier: Nullifier::new(w.nf_sk, w.nonce), + balance: w.note.balance(w.balance_blinding), + } + } + + pub fn prove(&self, w: &InputWitness) -> Result { + if &Input::from_witness(w.clone()) != self { + Err(Error::ProofFailed) + } else { + Ok(InputProof(w.clone())) + } + } + + pub fn verify(&self, proof: &InputProof) -> bool { + // verification checks the relation + // - nf_pk == hash(nf_sk) + // - note_comm == commit(note || nf_pk) + // - nullifier == hash(nf_sk || nonce) + // - balance == v * hash_to_curve(Unit) + blinding * H + + let witness = &proof.0; + + let nf_pk = witness.nf_sk.commit(); + self.note_comm == witness.note.commit(nf_pk, witness.nonce) + && self.nullifier == Nullifier::new(witness.nf_sk, witness.nonce) + && self.balance == witness.note.balance(witness.balance_blinding) + } +} + +#[cfg(test)] +mod test { + use group::ff::Field; + + use super::*; + use crate::{nullifier::NullifierNonce, test_util::seed_rng}; + + #[test] + fn test_input_proof() { + let mut rng = seed_rng(0); + + let note = Note::new(10, "NMO"); + let nf_sk = NullifierSecret::random(&mut rng); + let nonce = NullifierNonce::random(&mut rng); + let balance_blinding = Scalar::random(&mut rng); + + let witness = InputWitness { + note, + nf_sk, + nonce, + balance_blinding, + }; + + let input = Input::from_witness(witness.clone()); + let proof = input.prove(&witness).unwrap(); + + assert!(input.verify(&proof)); + + let wrong_witnesses = [ + InputWitness { + note: Note::new(11, "NMO"), + ..witness.clone() + }, + InputWitness { + note: Note::new(10, "ETH"), + ..witness.clone() + }, + InputWitness { + nf_sk: NullifierSecret::random(&mut rng), + ..witness.clone() + }, + InputWitness { + nonce: NullifierNonce::random(&mut rng), + ..witness.clone() + }, + InputWitness { + balance_blinding: Scalar::random(&mut rng), + ..witness.clone() + }, + ]; + + for wrong_witness in wrong_witnesses { + assert!(input.prove(&wrong_witness).is_err()); + + let wrong_note = Input::from_witness(wrong_witness.clone()); + let wrong_proof = wrong_note.prove(&wrong_witness).unwrap(); + + assert!(!input.verify(&wrong_proof)); + } + } +} diff --git a/cl/src/main.rs b/cl/src/main.rs index 878d5bc..bd4e5ae 100644 --- a/cl/src/main.rs +++ b/cl/src/main.rs @@ -1,6 +1,13 @@ -mod crypto; -mod note; -mod nullifier; +pub mod crypto; +pub mod error; +pub mod input; +pub mod note; +pub mod nullifier; +pub mod output; +pub mod partial_tx; + +#[cfg(test)] +mod test_util; fn main() { println!("Hello, world!"); diff --git a/cl/src/note.rs b/cl/src/note.rs index a64c927..d6f4866 100644 --- a/cl/src/note.rs +++ b/cl/src/note.rs @@ -1,13 +1,22 @@ +use blake2::{Blake2s256, Digest}; +use group::GroupEncoding; use jubjub::{ExtendedPoint, Scalar}; use lazy_static::lazy_static; -use crate::crypto; +use crate::{ + crypto, + nullifier::{NullifierCommitment, NullifierNonce}, +}; lazy_static! { static ref PEDERSON_COMMITMENT_BLINDING_POINT: ExtendedPoint = crypto::hash_to_curve(b"NOMOS_CL_PEDERSON_COMMITMENT_BLINDING"); } +#[derive(Debug, PartialEq, Eq)] +pub struct NoteCommitment([u8; 32]); + +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Note { pub value: u64, pub unit: String, @@ -21,53 +30,72 @@ impl Note { } } + pub fn unit_point(&self) -> ExtendedPoint { + crypto::hash_to_curve(self.unit.as_bytes()) + } + pub fn balance(&self, blinding: Scalar) -> ExtendedPoint { let value_scalar = Scalar::from(self.value); - let unit_point = crypto::hash_to_curve(self.unit.as_bytes()); + self.unit_point() * value_scalar + *PEDERSON_COMMITMENT_BLINDING_POINT * blinding + } - unit_point * value_scalar + *PEDERSON_COMMITMENT_BLINDING_POINT * blinding + pub fn commit(&self, nf_pk: NullifierCommitment, nonce: NullifierNonce) -> NoteCommitment { + let mut hasher = Blake2s256::new(); + hasher.update(b"NOMOS_CL_NOTE_COMMIT"); + hasher.update(self.value.to_le_bytes()); + hasher.update(self.unit_point().to_bytes()); + hasher.update(nf_pk.as_bytes()); + hasher.update(nonce.as_bytes()); + + let commit_bytes: [u8; 32] = hasher.finalize().into(); + NoteCommitment(commit_bytes) } } -#[test] -fn test_balance_zero_unitless() { - // Zero is the same across all units - let r = Scalar::from(32); - assert_eq!( - Note::new(0, "NMO").balance(r), - Note::new(0, "ETH").balance(r) - ); -} +#[cfg(test)] +mod test { + use super::*; -#[test] -fn test_balance_blinding() { - // balances are blinded - let r1 = Scalar::from(12); - let r2 = Scalar::from(8); - let a = Note::new(10, "NMO"); - assert_ne!(a.balance(r1), a.balance(r2)); - assert_eq!(a.balance(r1), a.balance(r1)); -} + #[test] + fn test_balance_zero_unitless() { + // Zero is the same across all units + let r = Scalar::from(32); + assert_eq!( + Note::new(0, "NMO").balance(r), + Note::new(0, "ETH").balance(r) + ); + } -#[test] -fn test_balance_units() { - // Unit's differentiate between values. - let nmo = Note::new(10, "NMO"); - let eth = Note::new(10, "ETH"); - let r = Scalar::from(1337); - assert_ne!(nmo.balance(r), eth.balance(r)); -} + #[test] + fn test_balance_blinding() { + // balances are blinded + let r1 = Scalar::from(12); + let r2 = Scalar::from(8); + let a = Note::new(10, "NMO"); + assert_ne!(a.balance(r1), a.balance(r2)); + assert_eq!(a.balance(r1), a.balance(r1)); + } -#[test] -fn test_balance_homomorphism() { - let r = Scalar::from(32); - let ten = Note::new(10, "NMO"); - let eight = Note::new(8, "NMO"); - let two = Note::new(2, "NMO"); - assert_eq!(ten.balance(r) - eight.balance(r), two.balance(0.into())); + #[test] + fn test_balance_units() { + // Unit's differentiate between values. + let nmo = Note::new(10, "NMO"); + let eth = Note::new(10, "ETH"); + let r = Scalar::from(1337); + assert_ne!(nmo.balance(r), eth.balance(r)); + } - assert_eq!( - ten.balance(54.into()) - ten.balance(48.into()), - Note::new(0, "NMO").balance(6.into()) - ); + #[test] + fn test_balance_homomorphism() { + let r = Scalar::from(32); + let ten = Note::new(10, "NMO"); + let eight = Note::new(8, "NMO"); + let two = Note::new(2, "NMO"); + assert_eq!(ten.balance(r) - eight.balance(r), two.balance(0.into())); + + assert_eq!( + ten.balance(54.into()) - ten.balance(48.into()), + Note::new(0, "NMO").balance(6.into()) + ); + } } diff --git a/cl/src/nullifier.rs b/cl/src/nullifier.rs index bc7ff32..f34656f 100644 --- a/cl/src/nullifier.rs +++ b/cl/src/nullifier.rs @@ -6,8 +6,7 @@ // nonce is used to disambiguate when the same nullifier // secret is used for multiple notes. use blake2::{Blake2s256, Digest}; -use hex; -use rand_core::{RngCore, SeedableRng}; +use rand_core::RngCore; // Maintained privately by note holder #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -31,16 +30,16 @@ pub struct NullifierNonce([u8; 16]); pub struct Nullifier([u8; 32]); impl NullifierSecret { - fn random(mut rng: impl RngCore) -> Self { + pub fn random(mut rng: impl RngCore) -> Self { let mut sk = [0u8; 16]; rng.fill_bytes(&mut sk); Self(sk) } - fn commit(&self) -> NullifierCommitment { + pub fn commit(&self) -> NullifierCommitment { let mut hasher = Blake2s256::new(); hasher.update(b"NOMOS_CL_NULL_COMMIT"); - hasher.update(&self.0); + hasher.update(self.0); let commit_bytes: [u8; 32] = hasher.finalize().into(); NullifierCommitment(commit_bytes) @@ -48,60 +47,68 @@ impl NullifierSecret { } impl NullifierCommitment { - pub fn to_hex(&self) -> String { - hex::encode(&self.0) + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } + + pub fn hex(&self) -> String { + hex::encode(self.0) } } impl NullifierNonce { - fn random(mut rng: impl RngCore) -> Self { + pub fn random(mut rng: impl RngCore) -> Self { let mut nonce = [0u8; 16]; rng.fill_bytes(&mut nonce); Self(nonce) } + + pub fn as_bytes(&self) -> &[u8; 16] { + &self.0 + } } impl Nullifier { - fn new(sk: NullifierSecret, nonce: NullifierNonce) -> Self { + pub fn new(sk: NullifierSecret, nonce: NullifierNonce) -> Self { let mut hasher = Blake2s256::new(); hasher.update(b"NOMOS_CL_NULLIFIER"); - hasher.update(&sk.0); - hasher.update(&nonce.0); + hasher.update(sk.0); + hasher.update(nonce.0); let nf_bytes: [u8; 32] = hasher.finalize().into(); Self(nf_bytes) } } -fn seed_rng(seed: u64) -> impl rand_core::RngCore { - let mut bytes = [0u8; 32]; - (&mut bytes[..8]).copy_from_slice(&seed.to_le_bytes()); - rand_chacha::ChaCha12Rng::from_seed(bytes) -} +#[cfg(test)] +mod test { + use super::*; + use crate::test_util::seed_rng; -#[test] -fn test_nullifier_commitment_vectors() { - assert_eq!( - NullifierSecret([0u8; 16]).commit().to_hex(), - "384318f9864fe57647bac344e2afdc500a672dedb29d2dc63b004e940e4b382a" - ); - assert_eq!( - NullifierSecret([1u8; 16]).commit().to_hex(), - "0fd667e6bb39fbdc35d6265726154b839638ea90bcf4e736953ccf27ca5f870b" - ); - assert_eq!( - NullifierSecret([u8::MAX; 16]).commit().to_hex(), - "1cb78e487eb0b3116389311fdde84cd3f619a4d7f487b29bf5a002eed3784d75" - ); -} + #[test] + fn test_nullifier_commitment_vectors() { + assert_eq!( + NullifierSecret([0u8; 16]).commit().hex(), + "384318f9864fe57647bac344e2afdc500a672dedb29d2dc63b004e940e4b382a" + ); + assert_eq!( + NullifierSecret([1u8; 16]).commit().hex(), + "0fd667e6bb39fbdc35d6265726154b839638ea90bcf4e736953ccf27ca5f870b" + ); + assert_eq!( + NullifierSecret([u8::MAX; 16]).commit().hex(), + "1cb78e487eb0b3116389311fdde84cd3f619a4d7f487b29bf5a002eed3784d75" + ); + } -#[test] -fn test_nullifier_same_sk_different_nonce() { - let sk = NullifierSecret::random(seed_rng(0)); - let nonce_1 = NullifierNonce::random(seed_rng(1)); - let nonce_2 = NullifierNonce::random(seed_rng(2)); - let nf_1 = Nullifier::new(sk, nonce_1); - let nf_2 = Nullifier::new(sk, nonce_2); + #[test] + fn test_nullifier_same_sk_different_nonce() { + let sk = NullifierSecret::random(seed_rng(0)); + let nonce_1 = NullifierNonce::random(seed_rng(1)); + let nonce_2 = NullifierNonce::random(seed_rng(2)); + let nf_1 = Nullifier::new(sk, nonce_1); + let nf_2 = Nullifier::new(sk, nonce_2); - assert_ne!(nf_1, nf_2); + assert_ne!(nf_1, nf_2); + } } diff --git a/cl/src/output.rs b/cl/src/output.rs new file mode 100644 index 0000000..111627f --- /dev/null +++ b/cl/src/output.rs @@ -0,0 +1,38 @@ +use jubjub::{ExtendedPoint, Scalar}; + +use crate::{ + note::{Note, NoteCommitment}, + nullifier::{NullifierCommitment, NullifierNonce}, +}; + +pub struct Output { + pub note_comm: NoteCommitment, + pub balance: ExtendedPoint, +} + +pub struct OutputWitness { + note: Note, + nf_pk: NullifierCommitment, + nonce: NullifierNonce, + balance_blinding: Scalar, +} + +// as we don't have SNARKS hooked up yet, the witness will be our proof +pub struct OutputProof(OutputWitness); + +impl Output { + pub fn prove(&self, w: OutputWitness) -> OutputProof { + OutputProof(w) + } + + pub fn verify(&self, proof: &OutputProof) -> bool { + // verification checks the relation + // - note_comm == commit(note || nf_pk) + // - balance == v * hash_to_curve(Unit) + blinding * H + + let witness = &proof.0; + + self.note_comm == witness.note.commit(witness.nf_pk, witness.nonce) + && self.balance == witness.note.balance(witness.balance_blinding) + } +} diff --git a/cl/src/partial_tx.rs b/cl/src/partial_tx.rs new file mode 100644 index 0000000..08369c0 --- /dev/null +++ b/cl/src/partial_tx.rs @@ -0,0 +1,31 @@ +use jubjub::ExtendedPoint; + +use crate::input::{Input, InputProof}; +use crate::output::{Output, OutputProof}; + +pub struct PartialTx { + inputs: Vec<(Input, InputProof)>, + outputs: Vec<(Output, OutputProof)>, +} + +impl PartialTx { + pub fn verify(&self) -> bool { + self.inputs.iter().all(|(i, p)| i.verify(p)) + && self.outputs.iter().all(|(o, p)| o.verify(p)) + } + + pub fn balance(&self) -> ExtendedPoint { + let in_sum = self + .inputs + .iter() + .map(|(i, _)| i.balance) + .sum::(); + let out_sum = self + .outputs + .iter() + .map(|(o, _)| o.balance) + .sum::(); + + in_sum - out_sum + } +} diff --git a/cl/src/test_util.rs b/cl/src/test_util.rs new file mode 100644 index 0000000..b213248 --- /dev/null +++ b/cl/src/test_util.rs @@ -0,0 +1,7 @@ +use rand_core::SeedableRng; + +pub fn seed_rng(seed: u64) -> impl rand_core::RngCore { + let mut bytes = [0u8; 32]; + (&mut bytes[..8]).copy_from_slice(&seed.to_le_bytes()); + rand_chacha::ChaCha12Rng::from_seed(bytes) +}