diff --git a/cl/src/input.rs b/cl/src/input.rs index 1055f3e..f746156 100644 --- a/cl/src/input.rs +++ b/cl/src/input.rs @@ -6,10 +6,13 @@ use crate::{ error::Error, note::{Note, NoteCommitment}, nullifier::{Nullifier, NullifierNonce, NullifierSecret}, + partial_tx::PtxCommitment, }; +use group::{ff::Field, GroupEncoding}; use jubjub::{ExtendedPoint, Scalar}; +use rand_core::RngCore; -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Input { pub note_comm: NoteCommitment, pub nullifier: Nullifier, @@ -18,14 +21,26 @@ pub struct Input { #[derive(Debug, Clone, PartialEq, Eq)] pub struct InputWitness { - note: Note, - nf_sk: NullifierSecret, - nonce: NullifierNonce, - balance_blinding: Scalar, + pub note: Note, + pub nf_sk: NullifierSecret, + pub nonce: NullifierNonce, + pub balance_blinding: Scalar, +} + +impl InputWitness { + pub fn random(note: Note, mut rng: impl RngCore) -> Self { + Self { + note, + nf_sk: NullifierSecret::random(&mut rng), + nonce: NullifierNonce::random(&mut rng), + balance_blinding: Scalar::random(&mut rng), + } + } } // as we don't have SNARKS hooked up yet, the witness will be our proof -pub struct InputProof(InputWitness); +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct InputProof(InputWitness, PtxCommitment); impl Input { pub fn from_witness(w: InputWitness) -> Self { @@ -36,20 +51,21 @@ impl Input { } } - pub fn prove(&self, w: &InputWitness) -> Result { + pub fn prove(&self, w: &InputWitness, ptx_comm: PtxCommitment) -> Result { if &Input::from_witness(w.clone()) != self { Err(Error::ProofFailed) } else { - Ok(InputProof(w.clone())) + Ok(InputProof(w.clone(), ptx_comm)) } } - pub fn verify(&self, proof: &InputProof) -> bool { + pub fn verify(&self, ptx_comm: PtxCommitment, 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 + // - ptx_comm is the same one that was used in proving. let witness = &proof.0; @@ -57,6 +73,15 @@ impl Input { 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) + && ptx_comm == proof.1 + } + + pub(crate) fn to_bytes(&self) -> [u8; 96] { + let mut bytes = [0u8; 96]; + bytes[..32].copy_from_slice(self.note_comm.as_bytes()); + bytes[32..64].copy_from_slice(self.nullifier.as_bytes()); + bytes[64..96].copy_from_slice(&self.balance.to_bytes()); + bytes } } @@ -71,6 +96,8 @@ mod test { fn test_input_proof() { let mut rng = seed_rng(0); + let ptx_comm = PtxCommitment::default(); + let note = Note::new(10, "NMO"); let nf_sk = NullifierSecret::random(&mut rng); let nonce = NullifierNonce::random(&mut rng); @@ -84,9 +111,9 @@ mod test { }; let input = Input::from_witness(witness.clone()); - let proof = input.prove(&witness).unwrap(); + let proof = input.prove(&witness, ptx_comm).unwrap(); - assert!(input.verify(&proof)); + assert!(input.verify(ptx_comm, &proof)); let wrong_witnesses = [ InputWitness { @@ -112,11 +139,39 @@ mod test { ]; for wrong_witness in wrong_witnesses { - assert!(input.prove(&wrong_witness).is_err()); + assert!(input.prove(&wrong_witness, ptx_comm).is_err()); let wrong_input = Input::from_witness(wrong_witness.clone()); - let wrong_proof = wrong_input.prove(&wrong_witness).unwrap(); - assert!(!input.verify(&wrong_proof)); + let wrong_proof = wrong_input.prove(&wrong_witness, ptx_comm).unwrap(); + assert!(!input.verify(ptx_comm, &wrong_proof)); } } + + #[test] + fn test_input_ptx_coupling() { + 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 ptx_comm = PtxCommitment::random(&mut rng); + let proof = input.prove(&witness, ptx_comm).unwrap(); + + assert!(input.verify(ptx_comm, &proof)); + + // The same input proof can not be used in another partial transaction. + let another_ptx_comm = PtxCommitment::random(&mut rng); + assert!(!input.verify(another_ptx_comm, &proof)); + } } diff --git a/cl/src/note.rs b/cl/src/note.rs index 5d0446e..b95b737 100644 --- a/cl/src/note.rs +++ b/cl/src/note.rs @@ -16,6 +16,12 @@ lazy_static! { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct NoteCommitment([u8; 32]); +impl NoteCommitment { + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct Note { pub value: u64, diff --git a/cl/src/nullifier.rs b/cl/src/nullifier.rs index f34656f..4c2da62 100644 --- a/cl/src/nullifier.rs +++ b/cl/src/nullifier.rs @@ -78,6 +78,10 @@ impl Nullifier { let nf_bytes: [u8; 32] = hasher.finalize().into(); Self(nf_bytes) } + + pub(crate) fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } } #[cfg(test)] diff --git a/cl/src/output.rs b/cl/src/output.rs index bd5826b..43d3a81 100644 --- a/cl/src/output.rs +++ b/cl/src/output.rs @@ -1,4 +1,6 @@ +use group::{ff::Field, GroupEncoding}; use jubjub::{ExtendedPoint, Scalar}; +use rand_core::RngCore; use crate::{ error::Error, @@ -14,10 +16,21 @@ pub struct Output { #[derive(Debug, Clone, PartialEq, Eq)] pub struct OutputWitness { - note: Note, - nf_pk: NullifierCommitment, - nonce: NullifierNonce, - balance_blinding: Scalar, + pub note: Note, + pub nf_pk: NullifierCommitment, + pub nonce: NullifierNonce, + pub balance_blinding: Scalar, +} + +impl OutputWitness { + pub fn random(note: Note, owner: NullifierCommitment, mut rng: impl RngCore) -> Self { + Self { + note, + nf_pk: owner, + nonce: NullifierNonce::random(&mut rng), + balance_blinding: Scalar::random(&mut rng), + } + } } // as we don't have SNARKS hooked up yet, the witness will be our proof @@ -49,6 +62,13 @@ impl Output { self.note_comm == witness.note.commit(witness.nf_pk, witness.nonce) && self.balance == witness.note.balance(witness.balance_blinding) } + + pub(crate) fn to_bytes(&self) -> [u8; 64] { + let mut bytes = [0u8; 64]; + bytes[..32].copy_from_slice(self.note_comm.as_bytes()); + bytes[32..64].copy_from_slice(&self.balance.to_bytes()); + bytes + } } #[cfg(test)] diff --git a/cl/src/partial_tx.rs b/cl/src/partial_tx.rs index 08369c0..c7750a9 100644 --- a/cl/src/partial_tx.rs +++ b/cl/src/partial_tx.rs @@ -1,31 +1,145 @@ +use blake2::{Blake2s256, Digest}; use jubjub::ExtendedPoint; +use rand_core::RngCore; -use crate::input::{Input, InputProof}; -use crate::output::{Output, OutputProof}; +use crate::error::Error; +use crate::input::{Input, InputProof, InputWitness}; +use crate::output::{Output, OutputProof, OutputWitness}; +/// The partial transaction commitment couples an input to a partial transaction. +/// Prevents partial tx unbundling. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct PtxCommitment([u8; 32]); + +impl PtxCommitment { + pub fn random(mut rng: impl RngCore) -> Self { + let mut sk = [0u8; 32]; + rng.fill_bytes(&mut sk); + Self(sk) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] pub struct PartialTx { - inputs: Vec<(Input, InputProof)>, - outputs: Vec<(Output, OutputProof)>, + inputs: Vec, + outputs: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PartialTxWitness { + inputs: Vec, + outputs: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PartialTxProof { + inputs: Vec, + outputs: Vec, } 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 from_witness(w: PartialTxWitness) -> Self { + Self { + inputs: Vec::from_iter(w.inputs.into_iter().map(Input::from_witness)), + outputs: Vec::from_iter(w.outputs.into_iter().map(Output::from_witness)), + } + } + + pub fn commitment(&self) -> PtxCommitment { + let mut hasher = Blake2s256::new(); + hasher.update(b"NOMOS_CL_PTX_COMMIT"); + hasher.update(b"INPUTS"); + for input in self.inputs.iter() { + hasher.update(input.to_bytes()); + } + hasher.update(b"OUTPUTS"); + for outputs in self.outputs.iter() { + hasher.update(outputs.to_bytes()); + } + + let commit_bytes: [u8; 32] = hasher.finalize().into(); + PtxCommitment(commit_bytes) + } + + pub fn prove(&self, w: PartialTxWitness) -> Result { + if &Self::from_witness(w.clone()) != self { + return Err(Error::ProofFailed); + } + + let ptx_comm = self.commitment(); + + let input_proofs: Vec = Result::from_iter( + self.inputs + .iter() + .zip(&w.inputs) + .map(|(i, i_w)| i.prove(i_w, ptx_comm)), + )?; + + let output_proofs: Vec = Result::from_iter( + self.outputs + .iter() + .zip(&w.outputs) + .map(|(o, o_w)| o.prove(o_w)), + )?; + + Ok(PartialTxProof { + inputs: input_proofs, + outputs: output_proofs, + }) + } + + pub fn verify(&self, proof: &PartialTxProof) -> bool { + let ptx_comm = self.commitment(); + self.inputs.len() == proof.inputs.len() + && self.outputs.len() == proof.outputs.len() + && self + .inputs + .iter() + .zip(&proof.inputs) + .all(|(i, p)| i.verify(ptx_comm, p)) + && self + .outputs + .iter() + .zip(&proof.outputs) + .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::(); + let in_sum: ExtendedPoint = self.inputs.iter().map(|i| i.balance).sum(); + let out_sum: ExtendedPoint = self.outputs.iter().map(|o| o.balance).sum(); in_sum - out_sum } } + +#[cfg(test)] +mod test { + + use crate::{note::Note, nullifier::NullifierSecret, test_util::seed_rng}; + + use super::*; + + #[test] + fn test_partial_tx_proof() { + let mut rng = seed_rng(0); + + let nmo_10 = InputWitness::random(Note::new(10, "NMO"), &mut rng); + let eth_23 = InputWitness::random(Note::new(23, "ETH"), &mut rng); + let crv_4840 = OutputWitness::random( + Note::new(4840, "CRV"), + NullifierSecret::random(&mut rng).commit(), // transferring to a random owner + &mut rng, + ); + + let ptx_witness = PartialTxWitness { + inputs: vec![nmo_10, eth_23], + outputs: vec![crv_4840], + }; + + let ptx = PartialTx::from_witness(ptx_witness.clone()); + + let ptx_proof = ptx.prove(ptx_witness).unwrap(); + + assert!(ptx.verify(&ptx_proof)); + } +} diff --git a/cl/src/test_util.rs b/cl/src/test_util.rs index b213248..e542e76 100644 --- a/cl/src/test_util.rs +++ b/cl/src/test_util.rs @@ -2,6 +2,6 @@ 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()); + bytes[..8].copy_from_slice(&seed.to_le_bytes()); rand_chacha::ChaCha12Rng::from_seed(bytes) }