From d6d3dcab12842f650b7bf43060723885a8d0691b Mon Sep 17 00:00:00 2001 From: David Rusu Date: Sat, 15 Jun 2024 00:53:00 -0400 Subject: [PATCH] cl: bundle of ptx --- cl/src/bundle.rs | 158 +++++++++++++++++++++++++++++++++++++++++++ cl/src/main.rs | 1 + cl/src/note.rs | 2 +- cl/src/nullifier.rs | 2 +- cl/src/partial_tx.rs | 22 ++++-- 5 files changed, 177 insertions(+), 8 deletions(-) create mode 100644 cl/src/bundle.rs diff --git a/cl/src/bundle.rs b/cl/src/bundle.rs new file mode 100644 index 0000000..5a8d578 --- /dev/null +++ b/cl/src/bundle.rs @@ -0,0 +1,158 @@ +use std::collections::BTreeSet; + +use jubjub::{ExtendedPoint, Scalar}; + +use crate::{ + error::Error, + note::NoteCommitment, + partial_tx::{PartialTx, PartialTxProof, PartialTxWitness}, +}; + +/// The transaction bundle is a collection of partial transactions. +/// The goal in bundling transactions is to produce a set of partial transactions +/// that balance each other. + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Bundle { + pub partials: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BundleWitness { + pub partials: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BundleProof { + pub partials: Vec, +} + +impl Bundle { + pub fn from_witness(w: BundleWitness) -> Self { + Self { + partials: Vec::from_iter(w.partials.into_iter().map(PartialTx::from_witness)), + } + } + + pub fn balance(&self) -> ExtendedPoint { + self.partials.iter().map(|ptx| ptx.balance()).sum() + } + + pub fn is_balanced(&self, balance_blinding_witness: Scalar) -> bool { + self.balance() == crate::balance::balance(0, "", balance_blinding_witness) + } + + pub fn prove(&self, w: BundleWitness) -> Result { + if &Self::from_witness(w.clone()) != self { + return Err(Error::ProofFailed); + } + if w.partials.len() == self.partials.len() { + return Err(Error::ProofFailed); + } + let input_notes: Vec = self + .partials + .iter() + .flat_map(|ptx| ptx.inputs.iter().map(|i| i.note_comm)) + .collect(); + if input_notes.len() != BTreeSet::from_iter(input_notes.iter()).len() { + return Err(Error::ProofFailed); + } + + let output_notes: Vec = self + .partials + .iter() + .flat_map(|ptx| ptx.outputs.iter().map(|o| o.note_comm)) + .collect(); + if output_notes.len() != BTreeSet::from_iter(output_notes.iter()).len() { + return Err(Error::ProofFailed); + } + + let ptx_proofs = self + .partials + .iter() + .zip(w.partials) + .map(|(ptx, p_w)| ptx.prove(p_w)) + .collect::, _>>()?; + + Ok(BundleProof { + partials: ptx_proofs, + }) + } + + pub fn verify(&self, proof: BundleProof) -> bool { + proof.partials.len() == self.partials.len() + && self + .partials + .iter() + .zip(&proof.partials) + .all(|(p, p_proof)| p.verify(p_proof)) + } +} + +#[cfg(test)] +mod test { + use crate::{ + input::InputWitness, note::Note, nullifier::NullifierSecret, output::OutputWitness, + test_util::seed_rng, + }; + + use super::*; + + #[test] + fn test_bundle_balance() { + let mut rng = seed_rng(0); + + let nmo_10_in = InputWitness::random(Note::new(10, "NMO"), &mut rng); + let eth_23_in = InputWitness::random(Note::new(23, "ETH"), &mut rng); + let crv_4840_out = OutputWitness::random( + Note::new(4840, "CRV"), + NullifierSecret::random(&mut rng).commit(), // transferring to a random owner + &mut rng, + ); + + let mut bundle_witness = BundleWitness { + partials: vec![PartialTxWitness { + inputs: vec![nmo_10_in.clone(), eth_23_in.clone()], + outputs: vec![crv_4840_out.clone()], + }], + }; + + let bundle = Bundle::from_witness(bundle_witness.clone()); + + assert_eq!( + bundle.balance(), + crate::balance::balance(4840, "CRV", crv_4840_out.balance_blinding) + - (crate::balance::balance(10, "NMO", nmo_10_in.balance_blinding) + + crate::balance::balance(23, "ETH", eth_23_in.balance_blinding)) + ); + + let crv_4840_in = InputWitness::random(Note::new(4840, "CRV"), &mut rng); + let nmo_10_out = OutputWitness::random( + Note::new(10, "NMO"), + NullifierSecret::random(&mut rng).commit(), // transferring to a random owner + &mut rng, + ); + let eth_23_out = OutputWitness::random( + Note::new(23, "ETH"), + NullifierSecret::random(&mut rng).commit(), // transferring to a random owner + &mut rng, + ); + + bundle_witness.partials.push(PartialTxWitness { + inputs: vec![crv_4840_in.clone()], + outputs: vec![nmo_10_out.clone(), eth_23_out.clone()], + }); + + let bundle = Bundle::from_witness(bundle_witness); + + let blinding = -nmo_10_in.balance_blinding - eth_23_in.balance_blinding + + crv_4840_out.balance_blinding + - crv_4840_in.balance_blinding + + nmo_10_out.balance_blinding + + eth_23_out.balance_blinding; + + assert_eq!(bundle.balance(), crate::balance::balance(0, "", blinding)); + + assert!(bundle.is_balanced(blinding)); + } +} diff --git a/cl/src/main.rs b/cl/src/main.rs index 1af140c..4c9e6fe 100644 --- a/cl/src/main.rs +++ b/cl/src/main.rs @@ -1,4 +1,5 @@ pub mod balance; +pub mod bundle; pub mod crypto; pub mod error; pub mod input; diff --git a/cl/src/note.rs b/cl/src/note.rs index 1d2cb48..1e1fe37 100644 --- a/cl/src/note.rs +++ b/cl/src/note.rs @@ -4,7 +4,7 @@ use jubjub::{ExtendedPoint, Scalar}; use crate::nullifier::{NullifierCommitment, NullifierNonce}; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct NoteCommitment([u8; 32]); impl NoteCommitment { diff --git a/cl/src/nullifier.rs b/cl/src/nullifier.rs index 4c2da62..d9bbce0 100644 --- a/cl/src/nullifier.rs +++ b/cl/src/nullifier.rs @@ -26,7 +26,7 @@ pub struct NullifierNonce([u8; 16]); // The nullifier attached to input notes to prove an input has not // already been spent. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Nullifier([u8; 32]); impl NullifierSecret { diff --git a/cl/src/partial_tx.rs b/cl/src/partial_tx.rs index 1d079ad..578de88 100644 --- a/cl/src/partial_tx.rs +++ b/cl/src/partial_tx.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeSet; + use blake2::{Blake2s256, Digest}; use jubjub::ExtendedPoint; use rand_core::RngCore; @@ -21,20 +23,20 @@ impl PtxCommitment { #[derive(Debug, Clone, PartialEq, Eq)] pub struct PartialTx { - inputs: Vec, - outputs: Vec, + pub inputs: Vec, + pub outputs: Vec, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct PartialTxWitness { - inputs: Vec, - outputs: Vec, + pub inputs: Vec, + pub outputs: Vec, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct PartialTxProof { - inputs: Vec, - outputs: Vec, + pub inputs: Vec, + pub outputs: Vec, } impl PartialTx { @@ -65,6 +67,14 @@ impl PartialTx { if &Self::from_witness(w.clone()) != self { return Err(Error::ProofFailed); } + let input_note_comms = BTreeSet::from_iter(self.inputs.iter().map(|i| i.note_comm)); + let output_note_comms = BTreeSet::from_iter(self.outputs.iter().map(|o| o.note_comm)); + + if input_note_comms.len() != self.inputs.len() + || output_note_comms.len() != self.outputs.len() + { + return Err(Error::ProofFailed); + } let ptx_comm = self.commitment();