diff --git a/goas/cl/cl/Cargo.toml b/goas/cl/cl/Cargo.toml index 500bbe3..c37eed3 100644 --- a/goas/cl/cl/Cargo.toml +++ b/goas/cl/cl/Cargo.toml @@ -7,10 +7,6 @@ edition = "2021" [dependencies] serde = {version="1.0", features = ["derive"]} -bincode = "1.3.3" -risc0-groth16 = "1.0.1" -blake2 = "0.10.6" -# jubjub = "0.10.0" group = "0.13.0" rand = "0.8.5" rand_core = "0.6.0" diff --git a/goas/cl/cl/src/balance.rs b/goas/cl/cl/src/balance.rs index b09202d..b49f18b 100644 --- a/goas/cl/cl/src/balance.rs +++ b/goas/cl/cl/src/balance.rs @@ -2,6 +2,9 @@ use curve25519_dalek::{ristretto::RistrettoPoint, traits::VartimeMultiscalarMul, use lazy_static::lazy_static; use rand_core::CryptoRngCore; use serde::{Deserialize, Serialize}; + +use crate::NoteWitness; + lazy_static! { // Precompute of `` static ref PEDERSON_COMMITMENT_BLINDING_POINT: RistrettoPoint = crate::crypto::hash_to_curve(b"NOMOS_CL_PEDERSON_COMMITMENT_BLINDING"); @@ -11,40 +14,38 @@ lazy_static! { pub struct Balance(pub RistrettoPoint); #[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] -pub struct BalanceWitness { - pub value: u64, - pub unit: RistrettoPoint, - pub blinding: Scalar, -} +pub struct BalanceWitness(pub Scalar); impl Balance { + /// A commitment to zero, blinded by the provided balance witness + pub fn zero(blinding: BalanceWitness) -> Self { + // Since, balance commitments are `value * UnitPoint + blinding * H`, when value=0, the commmitment is unitless. + // So we use the generator point as a stand in for the unit point. + // + // TAI: we can optimize this further from `0*G + r*H` to just `r*H` to save a point scalar mult + point addition. + let unit = curve25519_dalek::constants::RISTRETTO_BASEPOINT_POINT; + Self(balance(0, unit, blinding.0)) + } + pub fn to_bytes(&self) -> [u8; 32] { - self.0.compress().to_bytes().into() + self.0.compress().to_bytes() } } impl BalanceWitness { - pub fn new(value: u64, unit: impl Into, blinding: Scalar) -> Self { - Self { - value, - unit: unit_point(&unit.into()), - blinding, - } + pub fn new(blinding: Scalar) -> Self { + Self(blinding) } - pub fn random(value: u64, unit: impl Into, mut rng: impl CryptoRngCore) -> Self { - Self::new(value, unit, Scalar::random(&mut rng)) + pub fn random(mut rng: impl CryptoRngCore) -> Self { + Self::new(Scalar::random(&mut rng)) } - pub fn commit(&self) -> Balance { - Balance(balance(self.value, self.unit, self.blinding)) + pub fn commit(&self, note: &NoteWitness) -> Balance { + Balance(balance(note.value, note.unit, self.0)) } } -pub fn unit_point(unit: &str) -> RistrettoPoint { - crate::crypto::hash_to_curve(unit.as_bytes()) -} - pub fn balance(value: u64, unit: RistrettoPoint, blinding: Scalar) -> RistrettoPoint { let value_scalar = Scalar::from(value); // can vartime leak the number of cycles through the stark proof? @@ -54,97 +55,6 @@ pub fn balance(value: u64, unit: RistrettoPoint, blinding: Scalar) -> RistrettoP ) } -// mod serde_scalar { -// use super::Scalar; -// use serde::de::{self, Visitor}; -// use serde::{Deserializer, Serializer}; -// use std::fmt; - -// // Serialize a SubgroupPoint by converting it to bytes. -// pub fn serialize(scalar: &Scalar, serializer: S) -> Result -// where -// S: Serializer, -// { -// let bytes = scalar.to_bytes(); -// serializer.serialize_bytes(&bytes) -// } - -// // Deserialize a SubgroupPoint by converting it from bytes. -// pub fn deserialize<'de, D>(deserializer: D) -> Result -// where -// D: Deserializer<'de>, -// { -// struct BytesVisitor; - -// impl<'de> Visitor<'de> for BytesVisitor { -// type Value = Scalar; - -// fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { -// formatter.write_str("a valid Scalar in byte representation") -// } - -// fn visit_bytes(self, v: &[u8]) -> Result -// where -// E: de::Error, -// { -// let mut bytes = ::Repr::default(); -// assert_eq!(bytes.len(), v.len()); -// bytes.copy_from_slice(v); - -// Ok(Scalar::from_bytes(&bytes).unwrap()) -// } -// } - -// deserializer.deserialize_bytes(BytesVisitor) -// } -// } - -// mod serde_point { -// use super::SubgroupPoint; -// use group::GroupEncoding; -// use serde::de::{self, Visitor}; -// use serde::{Deserializer, Serializer}; -// use std::fmt; - -// // Serialize a SubgroupPoint by converting it to bytes. -// pub fn serialize(point: &SubgroupPoint, serializer: S) -> Result -// where -// S: Serializer, -// { -// let bytes = point.to_bytes(); -// serializer.serialize_bytes(&bytes) -// } - -// // Deserialize a SubgroupPoint by converting it from bytes. -// pub fn deserialize<'de, D>(deserializer: D) -> Result -// where -// D: Deserializer<'de>, -// { -// struct BytesVisitor; - -// impl<'de> Visitor<'de> for BytesVisitor { -// type Value = SubgroupPoint; - -// fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { -// formatter.write_str("a valid SubgroupPoint in byte representation") -// } - -// fn visit_bytes(self, v: &[u8]) -> Result -// where -// E: de::Error, -// { -// let mut bytes = ::Repr::default(); -// assert_eq!(bytes.len(), v.len()); -// bytes.copy_from_slice(v); - -// Ok(SubgroupPoint::from_bytes(&bytes).unwrap()) -// } -// } - -// deserializer.deserialize_bytes(BytesVisitor) -// } -// } - #[cfg(test)] mod test { @@ -165,52 +75,67 @@ mod test { fn test_balance_zero_unitless() { // Zero is the same across all units let mut rng = rand::thread_rng(); - let r = Scalar::random(&mut rng); + let b = BalanceWitness::random(&mut rng); assert_eq!( - BalanceWitness::new(0, "NMO", r).commit(), - BalanceWitness::new(0, "ETH", r).commit(), + b.commit(&NoteWitness::basic(0, "NMO")), + b.commit(&NoteWitness::basic(0, "ETH")), ); } #[test] fn test_balance_blinding() { // balances are blinded - let r1 = Scalar::from(12u32); - let r2 = Scalar::from(8u32); - let a_w = BalanceWitness::new(10, "NMO", r1); - let b_w = BalanceWitness::new(10, "NMO", r2); - let a = a_w.commit(); - let b = b_w.commit(); + let r_a = Scalar::from(12u32); + let r_b = Scalar::from(8u32); + let bal_a = BalanceWitness::new(r_a); + let bal_b = BalanceWitness::new(r_b); + + let note = NoteWitness::basic(10, "NMO"); + + let a = bal_a.commit(¬e); + let b = bal_b.commit(¬e); + assert_ne!(a, b); - assert_eq!(a.0 - b.0, BalanceWitness::new(0, "NMO", r1 - r2).commit().0); + + let diff_note = NoteWitness::basic(0, "NMO"); + assert_eq!( + a.0 - b.0, + BalanceWitness::new(r_a - r_b).commit(&diff_note).0 + ); } #[test] fn test_balance_units() { // Unit's differentiate between values. - let r = Scalar::from(1337u32); - let nmo = BalanceWitness::new(10, "NMO", r); - let eth = BalanceWitness::new(10, "ETH", r); - assert_ne!(nmo.commit(), eth.commit()); + let b = BalanceWitness::new(Scalar::from(1337u32)); + + let nmo = NoteWitness::basic(10, "NMO"); + let eth = NoteWitness::basic(10, "ETH"); + assert_ne!(b.commit(&nmo), b.commit(ð)); } #[test] fn test_balance_homomorphism() { let mut rng = rand::thread_rng(); - let r1 = Scalar::random(&mut rng); - let r2 = Scalar::random(&mut rng); - let ten = BalanceWitness::new(10, "NMO", 0u32.into()); - let eight = BalanceWitness::new(8, "NMO", 0u32.into()); - let two = BalanceWitness::new(2, "NMO", 0u32.into()); + let b1 = BalanceWitness::random(&mut rng); + let b2 = BalanceWitness::random(&mut rng); + let b_zero = BalanceWitness::new(Scalar::ZERO); + + let ten = NoteWitness::basic(10, "NMO"); + let eight = NoteWitness::basic(8, "NMO"); + let two = NoteWitness::basic(2, "NMO"); + let zero = NoteWitness::basic(0, "NMO"); // Values of same unit are homomorphic - assert_eq!(ten.commit().0 - eight.commit().0, two.commit().0); + assert_eq!( + (b1.commit(&ten).0 - b1.commit(&eight).0), + b_zero.commit(&two).0 + ); // Blinding factors are also homomorphic. assert_eq!( - BalanceWitness::new(10, "NMO", r1).commit().0 - - BalanceWitness::new(10, "NMO", r2).commit().0, - BalanceWitness::new(0, "NMO", r1 - r2).commit().0 + b1.commit(&ten).0 - b2.commit(&ten).0, + BalanceWitness::new(b1.0 - b2.0).commit(&zero).0 ); } } diff --git a/goas/cl/cl/src/bundle.rs b/goas/cl/cl/src/bundle.rs index 0aec2f2..e215f92 100644 --- a/goas/cl/cl/src/bundle.rs +++ b/goas/cl/cl/src/bundle.rs @@ -1,8 +1,6 @@ use serde::{Deserialize, Serialize}; -use curve25519_dalek::{constants::RISTRETTO_BASEPOINT_POINT, ristretto::RistrettoPoint, Scalar}; - -use crate::partial_tx::PartialTx; +use crate::{partial_tx::PartialTx, Balance, BalanceWitness}; /// The transaction bundle is a collection of partial transactions. /// The goal in bundling transactions is to produce a set of partial transactions @@ -13,27 +11,26 @@ pub struct Bundle { pub partials: Vec, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct BundleWitness { - pub balance_blinding: Scalar, + pub balance_blinding: BalanceWitness, } impl Bundle { - pub fn balance(&self) -> RistrettoPoint { - self.partials.iter().map(|ptx| ptx.balance()).sum() + pub fn balance(&self) -> Balance { + Balance(self.partials.iter().map(|ptx| ptx.balance().0).sum()) } - pub fn is_balanced(&self, balance_blinding_witness: Scalar) -> bool { - self.balance() - == crate::balance::balance(0, RISTRETTO_BASEPOINT_POINT, balance_blinding_witness) + pub fn is_balanced(&self, witness: BalanceWitness) -> bool { + self.balance() == Balance::zero(witness) } } #[cfg(test)] mod test { use crate::{ - crypto::hash_to_curve, input::InputWitness, note::NoteWitness, nullifier::NullifierSecret, - output::OutputWitness, partial_tx::PartialTxWitness, + input::InputWitness, note::NoteWitness, nullifier::NullifierSecret, output::OutputWitness, + partial_tx::PartialTxWitness, }; use super::*; @@ -42,86 +39,75 @@ mod test { fn test_bundle_balance() { let mut rng = rand::thread_rng(); - let nmo_10_in = - InputWitness::random(NoteWitness::new(10, "NMO", [0u8; 32], &mut rng), &mut rng); - let eth_23_in = - InputWitness::random(NoteWitness::new(23, "ETH", [0u8; 32], &mut rng), &mut rng); - let crv_4840_out = OutputWitness::random( - NoteWitness::new(4840, "CRV", [0u8; 32], &mut rng), - NullifierSecret::random(&mut rng).commit(), // transferring to a random owner - &mut rng, - ); + let nf_a = NullifierSecret::random(&mut rng); + let nf_b = NullifierSecret::random(&mut rng); + let nf_c = NullifierSecret::random(&mut rng); + + let nmo_10_utxo = + OutputWitness::random(NoteWitness::basic(10, "NMO"), nf_a.commit(), &mut rng); + let nmo_10_in = InputWitness::random(nmo_10_utxo, nf_a, &mut rng); + + let eth_23_utxo = + OutputWitness::random(NoteWitness::basic(23, "ETH"), nf_b.commit(), &mut rng); + let eth_23_in = InputWitness::random(eth_23_utxo, nf_b, &mut rng); + + let crv_4840_out = + OutputWitness::random(NoteWitness::basic(4840, "CRV"), nf_c.commit(), &mut rng); let ptx_unbalanced = PartialTxWitness { - inputs: vec![nmo_10_in.clone(), eth_23_in.clone()], - outputs: vec![crv_4840_out.clone()], + inputs: vec![nmo_10_in, eth_23_in], + outputs: vec![crv_4840_out], }; let bundle_witness = BundleWitness { - balance_blinding: crv_4840_out.note.balance.blinding - - nmo_10_in.note.balance.blinding - - eth_23_in.note.balance.blinding, + balance_blinding: BalanceWitness::new( + crv_4840_out.balance_blinding.0 + - nmo_10_in.balance_blinding.0 + - eth_23_in.balance_blinding.0, + ), }; let mut bundle = Bundle { - partials: vec![PartialTx::from_witness(ptx_unbalanced)], + partials: vec![ptx_unbalanced.commit()], }; assert!(!bundle.is_balanced(bundle_witness.balance_blinding)); assert_eq!( - bundle.balance(), - crate::balance::balance( - 4840, - hash_to_curve(b"CRV"), - crv_4840_out.note.balance.blinding - ) - (crate::balance::balance( - 10, - hash_to_curve(b"NMO"), - nmo_10_in.note.balance.blinding - ) + crate::balance::balance( - 23, - hash_to_curve(b"ETH"), - eth_23_in.note.balance.blinding - )) + bundle.balance().0, + crv_4840_out.commit().balance.0 + - (nmo_10_in.commit().balance.0 + eth_23_in.commit().balance.0) ); - let crv_4840_in = - InputWitness::random(NoteWitness::new(4840, "CRV", [0u8; 32], &mut rng), &mut rng); + let crv_4840_in = InputWitness::random(crv_4840_out, nf_c, &mut rng); let nmo_10_out = OutputWitness::random( - NoteWitness::new(10, "NMO", [0u8; 32], &mut rng), + NoteWitness::basic(10, "NMO"), NullifierSecret::random(&mut rng).commit(), // transferring to a random owner &mut rng, ); let eth_23_out = OutputWitness::random( - NoteWitness::new(23, "ETH", [0u8; 32], &mut rng), + NoteWitness::basic(23, "ETH"), NullifierSecret::random(&mut rng).commit(), // transferring to a random owner &mut rng, ); - bundle - .partials - .push(PartialTx::from_witness(PartialTxWitness { - inputs: vec![crv_4840_in.clone()], - outputs: vec![nmo_10_out.clone(), eth_23_out.clone()], - })); + bundle.partials.push( + PartialTxWitness { + inputs: vec![crv_4840_in], + outputs: vec![nmo_10_out, eth_23_out], + } + .commit(), + ); let witness = BundleWitness { - balance_blinding: -nmo_10_in.note.balance.blinding - eth_23_in.note.balance.blinding - + crv_4840_out.note.balance.blinding - - crv_4840_in.note.balance.blinding - + nmo_10_out.note.balance.blinding - + eth_23_out.note.balance.blinding, + balance_blinding: BalanceWitness::new( + -nmo_10_in.balance_blinding.0 - eth_23_in.balance_blinding.0 + + crv_4840_out.balance_blinding.0 + - crv_4840_in.balance_blinding.0 + + nmo_10_out.balance_blinding.0 + + eth_23_out.balance_blinding.0, + ), }; - assert_eq!( - bundle.balance(), - crate::balance::balance( - 0, - curve25519_dalek::constants::RISTRETTO_BASEPOINT_POINT, - witness.balance_blinding - ) - ); - assert!(bundle.is_balanced(witness.balance_blinding)); } } diff --git a/goas/cl/cl/src/input.rs b/goas/cl/cl/src/input.rs index dde6f36..8a9ff2f 100644 --- a/goas/cl/cl/src/input.rs +++ b/goas/cl/cl/src/input.rs @@ -6,8 +6,9 @@ use crate::{ balance::Balance, note::{DeathCommitment, NoteWitness}, nullifier::{Nullifier, NullifierNonce, NullifierSecret}, + BalanceWitness, }; -use rand_core::RngCore; +use rand_core::CryptoRngCore; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] @@ -20,33 +21,40 @@ pub struct Input { #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct InputWitness { pub note: NoteWitness, + pub balance_blinding: BalanceWitness, pub nf_sk: NullifierSecret, pub nonce: NullifierNonce, } impl InputWitness { - pub fn random(note: NoteWitness, mut rng: impl RngCore) -> Self { + pub fn random( + output: crate::OutputWitness, + nf_sk: NullifierSecret, + mut rng: impl CryptoRngCore, + ) -> Self { + assert_eq!(nf_sk.commit(), output.nf_pk); Self { - note, - nf_sk: NullifierSecret::random(&mut rng), - nonce: NullifierNonce::random(&mut rng), + note: output.note, + balance_blinding: BalanceWitness::random(&mut rng), + nf_sk, + nonce: output.nonce, } } + pub fn nullifier(&self) -> Nullifier { + Nullifier::new(self.nf_sk, self.nonce) + } + pub fn commit(&self) -> Input { Input { - nullifier: Nullifier::new(self.nf_sk, self.nonce), - balance: self.note.balance(), + nullifier: self.nullifier(), + balance: self.balance_blinding.commit(&self.note), death_cm: self.note.death_commitment(), } } - pub fn to_output_witness(&self) -> crate::OutputWitness { - crate::OutputWitness { - note: self.note.clone(), - nf_pk: self.nf_sk.commit(), - nonce: self.nonce, - } + pub fn note_commitment(&self) -> crate::NoteCommitment { + self.note.commit(self.nf_sk.commit(), self.nonce) } } diff --git a/goas/cl/cl/src/note.rs b/goas/cl/cl/src/note.rs index 0beed54..10a773e 100644 --- a/goas/cl/cl/src/note.rs +++ b/goas/cl/cl/src/note.rs @@ -1,11 +1,8 @@ -use rand_core::CryptoRngCore; +use curve25519_dalek::RistrettoPoint; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; -use crate::{ - balance::{Balance, BalanceWitness}, - nullifier::{NullifierCommitment, NullifierNonce}, -}; +use crate::nullifier::{NullifierCommitment, NullifierNonce}; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub struct DeathCommitment(pub [u8; 32]); @@ -19,6 +16,10 @@ pub fn death_commitment(death_constraint: &[u8]) -> DeathCommitment { DeathCommitment(death_cm) } +pub fn unit_point(unit: &str) -> RistrettoPoint { + crate::crypto::hash_to_curve(unit.as_bytes()) +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub struct NoteCommitment([u8; 32]); @@ -32,7 +33,8 @@ impl NoteCommitment { #[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] pub struct NoteWitness { - pub balance: BalanceWitness, + pub value: u64, + pub unit: RistrettoPoint, pub death_constraint: [u8; 32], // death constraint verification key pub state: [u8; 32], } @@ -41,23 +43,32 @@ impl NoteWitness { pub fn new( value: u64, unit: impl Into, + death_constraint: [u8; 32], state: [u8; 32], - rng: impl CryptoRngCore, ) -> Self { Self { - balance: BalanceWitness::random(value, unit, rng), - death_constraint: [0u8; 32], + value, + unit: unit_point(&unit.into()), + death_constraint, state, } } + pub fn basic(value: u64, unit: impl Into) -> Self { + Self::new(value, unit, [0u8; 32], [0u8; 32]) + } + + pub fn stateless(value: u64, unit: impl Into, death_constraint: [u8; 32]) -> Self { + Self::new(value, unit, death_constraint, [0u8; 32]) + } + pub fn commit(&self, nf_pk: NullifierCommitment, nonce: NullifierNonce) -> NoteCommitment { let mut hasher = Sha256::new(); hasher.update(b"NOMOS_CL_NOTE_COMMIT"); // COMMIT TO BALANCE - hasher.update(self.balance.value.to_le_bytes()); - hasher.update(self.balance.unit.compress().to_bytes()); + hasher.update(self.value.to_le_bytes()); + hasher.update(self.unit.compress().to_bytes()); // Important! we don't commit to the balance blinding factor as that may make the notes linkable. // COMMIT TO STATE @@ -74,10 +85,6 @@ impl NoteWitness { NoteCommitment(commit_bytes) } - pub fn balance(&self) -> Balance { - self.balance.commit() - } - pub fn death_commitment(&self) -> DeathCommitment { death_commitment(&self.death_constraint) } @@ -90,18 +97,57 @@ mod test { use super::*; #[test] - fn test_note_commitments_dont_commit_to_balance_blinding() { + fn test_note_commit_permutations() { let mut rng = rand::thread_rng(); - let n1 = NoteWitness::new(12, "NMO", [0u8; 32], &mut rng); - let n2 = NoteWitness::new(12, "NMO", [0u8; 32], &mut rng); let nf_pk = NullifierSecret::random(&mut rng).commit(); - let nonce = NullifierNonce::random(&mut rng); + let nf_nonce = NullifierNonce::random(&mut rng); - // Balance blinding factors are different. - assert_ne!(n1.balance.blinding, n2.balance.blinding); + let reference_note = NoteWitness::basic(32, "NMO"); - // But their commitments are the same. - assert_eq!(n1.commit(nf_pk, nonce), n2.commit(nf_pk, nonce)); + // different notes under same nullifier produce different commitments + let mutation_tests = [ + NoteWitness { + value: 12, + ..reference_note + }, + NoteWitness { + unit: unit_point("ETH"), + ..reference_note + }, + NoteWitness { + death_constraint: [1u8; 32], + ..reference_note + }, + NoteWitness { + state: [1u8; 32], + ..reference_note + }, + ]; + + for n in mutation_tests { + assert_ne!( + n.commit(nf_pk, nf_nonce), + reference_note.commit(nf_pk, nf_nonce) + ); + } + + // commitment to same note with different nullifiers produce different commitments + + let other_nf_pk = NullifierSecret::random(&mut rng).commit(); + let other_nf_nonce = NullifierNonce::random(&mut rng); + + assert_ne!( + reference_note.commit(nf_pk, nf_nonce), + reference_note.commit(other_nf_pk, nf_nonce) + ); + assert_ne!( + reference_note.commit(nf_pk, nf_nonce), + reference_note.commit(nf_pk, other_nf_nonce) + ); + assert_ne!( + reference_note.commit(nf_pk, nf_nonce), + reference_note.commit(other_nf_pk, other_nf_nonce) + ); } } diff --git a/goas/cl/cl/src/nullifier.rs b/goas/cl/cl/src/nullifier.rs index 9d55378..b378093 100644 --- a/goas/cl/cl/src/nullifier.rs +++ b/goas/cl/cl/src/nullifier.rs @@ -5,9 +5,9 @@ // notes to allow users to hold fewer secrets. A note // nonce is used to disambiguate when the same nullifier // secret is used for multiple notes. -use blake2::{Blake2s256, Digest}; use rand_core::RngCore; use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; // TODO: create a nullifier witness and use it throughout. // struct NullifierWitness { @@ -44,7 +44,7 @@ impl NullifierSecret { } pub fn commit(&self) -> NullifierCommitment { - let mut hasher = Blake2s256::new(); + let mut hasher = Sha256::new(); hasher.update(b"NOMOS_CL_NULL_COMMIT"); hasher.update(self.0); @@ -85,7 +85,7 @@ impl NullifierNonce { impl Nullifier { pub fn new(sk: NullifierSecret, nonce: NullifierNonce) -> Self { - let mut hasher = Blake2s256::new(); + let mut hasher = Sha256::new(); hasher.update(b"NOMOS_CL_NULLIFIER"); hasher.update(sk.0); hasher.update(nonce.0); @@ -103,6 +103,7 @@ impl Nullifier { mod test { use super::*; + #[ignore = "nullifier test vectors not stable yet"] #[test] fn test_nullifier_commitment_vectors() { assert_eq!( diff --git a/goas/cl/cl/src/output.rs b/goas/cl/cl/src/output.rs index f7e2391..6758fcd 100644 --- a/goas/cl/cl/src/output.rs +++ b/goas/cl/cl/src/output.rs @@ -1,4 +1,4 @@ -use rand_core::RngCore; +use rand_core::CryptoRngCore; use serde::{Deserialize, Serialize}; use crate::{ @@ -6,6 +6,7 @@ use crate::{ error::Error, note::{NoteCommitment, NoteWitness}, nullifier::{NullifierCommitment, NullifierNonce}, + BalanceWitness, }; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -14,17 +15,23 @@ pub struct Output { pub balance: Balance, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct OutputWitness { pub note: NoteWitness, + pub balance_blinding: BalanceWitness, pub nf_pk: NullifierCommitment, pub nonce: NullifierNonce, } impl OutputWitness { - pub fn random(note: NoteWitness, owner: NullifierCommitment, mut rng: impl RngCore) -> Self { + pub fn random( + note: NoteWitness, + owner: NullifierCommitment, + mut rng: impl CryptoRngCore, + ) -> Self { Self { note, + balance_blinding: BalanceWitness::random(&mut rng), nf_pk: owner, nonce: NullifierNonce::random(&mut rng), } @@ -34,10 +41,14 @@ impl OutputWitness { self.note.commit(self.nf_pk, self.nonce) } + pub fn commit_balance(&self) -> Balance { + self.balance_blinding.commit(&self.note) + } + pub fn commit(&self) -> Output { Output { note_comm: self.commit_note(), - balance: self.note.balance(), + balance: self.commit_balance(), } } } @@ -49,7 +60,7 @@ pub struct OutputProof(OutputWitness); impl Output { pub fn prove(&self, w: &OutputWitness) -> Result { if &w.commit() == self { - Ok(OutputProof(w.clone())) + Ok(OutputProof(*w)) } else { Err(Error::ProofFailed) } @@ -61,8 +72,7 @@ impl Output { // - 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() + self.note_comm == witness.commit_note() && self.balance == witness.commit_balance() } pub fn to_bytes(&self) -> [u8; 64] { @@ -82,11 +92,12 @@ mod test { fn test_output_proof() { let mut rng = rand::thread_rng(); - let note = NoteWitness::new(10, "NMO", [0u8; 32], &mut rng); - let nf_pk = NullifierSecret::random(&mut rng).commit(); - let nonce = NullifierNonce::random(&mut rng); - - let witness = OutputWitness { note, nf_pk, nonce }; + let witness = OutputWitness { + note: NoteWitness::basic(10, "NMO"), + balance_blinding: BalanceWitness::random(&mut rng), + nf_pk: NullifierSecret::random(&mut rng).commit(), + nonce: NullifierNonce::random(&mut rng), + }; let output = witness.commit(); let proof = output.prove(&witness).unwrap(); @@ -95,20 +106,24 @@ mod test { let wrong_witnesses = [ OutputWitness { - note: NoteWitness::new(11, "NMO", [0u8; 32], &mut rng), - ..witness.clone() + note: NoteWitness::basic(11, "NMO"), + ..witness }, OutputWitness { - note: NoteWitness::new(10, "ETH", [0u8; 32], &mut rng), - ..witness.clone() + note: NoteWitness::basic(10, "ETH"), + ..witness + }, + OutputWitness { + balance_blinding: BalanceWitness::random(&mut rng), + ..witness }, OutputWitness { nf_pk: NullifierSecret::random(&mut rng).commit(), - ..witness.clone() + ..witness }, OutputWitness { nonce: NullifierNonce::random(&mut rng), - ..witness.clone() + ..witness }, ]; diff --git a/goas/cl/cl/src/partial_tx.rs b/goas/cl/cl/src/partial_tx.rs index 814a70f..afe9785 100644 --- a/goas/cl/cl/src/partial_tx.rs +++ b/goas/cl/cl/src/partial_tx.rs @@ -1,8 +1,9 @@ -use rand_core::RngCore; -// use risc0_groth16::ProofJson; use curve25519_dalek::ristretto::RistrettoPoint; +use curve25519_dalek::Scalar; +use rand_core::RngCore; use serde::{Deserialize, Serialize}; +use crate::balance::{Balance, BalanceWitness}; use crate::input::{Input, InputWitness}; use crate::merkle; use crate::output::{Output, OutputWitness}; @@ -45,14 +46,23 @@ pub struct PartialTxWitness { pub outputs: Vec, } -impl PartialTx { - pub fn from_witness(w: PartialTxWitness) -> Self { - Self { - inputs: Vec::from_iter(w.inputs.iter().map(InputWitness::commit)), - outputs: Vec::from_iter(w.outputs.iter().map(OutputWitness::commit)), +impl PartialTxWitness { + pub fn commit(&self) -> PartialTx { + PartialTx { + inputs: Vec::from_iter(self.inputs.iter().map(InputWitness::commit)), + outputs: Vec::from_iter(self.outputs.iter().map(OutputWitness::commit)), } } + pub fn balance_blinding(&self) -> BalanceWitness { + let in_sum: Scalar = self.inputs.iter().map(|i| i.balance_blinding.0).sum(); + let out_sum: Scalar = self.outputs.iter().map(|o| o.balance_blinding.0).sum(); + + BalanceWitness(out_sum - in_sum) + } +} + +impl PartialTx { pub fn input_root(&self) -> [u8; 32] { let input_bytes = Vec::from_iter(self.inputs.iter().map(Input::to_bytes).map(Vec::from_iter)); @@ -96,18 +106,18 @@ impl PartialTx { PtxRoot(root) } - pub fn balance(&self) -> RistrettoPoint { + pub fn balance(&self) -> Balance { let in_sum: RistrettoPoint = self.inputs.iter().map(|i| i.balance.0).sum(); let out_sum: RistrettoPoint = self.outputs.iter().map(|o| o.balance.0).sum(); - out_sum - in_sum + Balance(out_sum - in_sum) } } #[cfg(test)] mod test { - use crate::{crypto::hash_to_curve, note::NoteWitness, nullifier::NullifierSecret}; + use crate::{note::NoteWitness, nullifier::NullifierSecret}; use super::*; @@ -115,35 +125,31 @@ mod test { fn test_partial_tx_balance() { let mut rng = rand::thread_rng(); - let nmo_10 = - InputWitness::random(NoteWitness::new(10, "NMO", [0u8; 32], &mut rng), &mut rng); - let eth_23 = - InputWitness::random(NoteWitness::new(23, "ETH", [0u8; 32], &mut rng), &mut rng); - let crv_4840 = OutputWitness::random( - NoteWitness::new(4840, "CRV", [0u8; 32], &mut rng), - NullifierSecret::random(&mut rng).commit(), // transferring to a random owner - &mut rng, - ); + let nf_a = NullifierSecret::random(&mut rng); + let nf_b = NullifierSecret::random(&mut rng); + let nf_c = NullifierSecret::random(&mut rng); + + let nmo_10_utxo = + OutputWitness::random(NoteWitness::basic(10, "NMO"), nf_a.commit(), &mut rng); + let nmo_10 = InputWitness::random(nmo_10_utxo, nf_a, &mut rng); + + let eth_23_utxo = + OutputWitness::random(NoteWitness::basic(23, "ETH"), nf_b.commit(), &mut rng); + let eth_23 = InputWitness::random(eth_23_utxo, nf_b, &mut rng); + + let crv_4840 = + OutputWitness::random(NoteWitness::basic(4840, "CRV"), nf_c.commit(), &mut rng); let ptx_witness = PartialTxWitness { - inputs: vec![nmo_10.clone(), eth_23.clone()], - outputs: vec![crv_4840.clone()], + inputs: vec![nmo_10, eth_23], + outputs: vec![crv_4840], }; - let ptx = PartialTx::from_witness(ptx_witness.clone()); + let ptx = ptx_witness.commit(); assert_eq!( - ptx.balance(), - crate::balance::balance(4840, hash_to_curve(b"CRV"), crv_4840.note.balance.blinding) - - (crate::balance::balance( - 10, - hash_to_curve(b"NMO"), - nmo_10.note.balance.blinding - ) + crate::balance::balance( - 23, - hash_to_curve(b"ETH"), - eth_23.note.balance.blinding - )) + ptx.balance().0, + crv_4840.commit().balance.0 - (nmo_10.commit().balance.0 + eth_23.commit().balance.0) ); } } diff --git a/goas/cl/cl/tests/simple_transfer.rs b/goas/cl/cl/tests/simple_transfer.rs new file mode 100644 index 0000000..f008b6f --- /dev/null +++ b/goas/cl/cl/tests/simple_transfer.rs @@ -0,0 +1,41 @@ +use rand_core::CryptoRngCore; + +fn receive_utxo( + note: cl::NoteWitness, + nf_pk: cl::NullifierCommitment, + rng: impl CryptoRngCore, +) -> cl::OutputWitness { + cl::OutputWitness::random(note, nf_pk, rng) +} + +#[test] +fn test_simple_transfer() { + let mut rng = rand::thread_rng(); + + let sender_nf_sk = cl::NullifierSecret::random(&mut rng); + let sender_nf_pk = sender_nf_sk.commit(); + + let recipient_nf_pk = cl::NullifierSecret::random(&mut rng).commit(); + + // Assume the sender has received an unspent output from somewhere + let utxo = receive_utxo(cl::NoteWitness::basic(10, "NMO"), sender_nf_pk, &mut rng); + + // and wants to send 8 NMO to some recipient and return 2 NMO to itself. + let recipient_output = + cl::OutputWitness::random(cl::NoteWitness::basic(8, "NMO"), recipient_nf_pk, &mut rng); + let change_output = + cl::OutputWitness::random(cl::NoteWitness::basic(2, "NMO"), sender_nf_pk, &mut rng); + + let ptx_witness = cl::PartialTxWitness { + inputs: vec![cl::InputWitness::random(utxo, sender_nf_sk, &mut rng)], + outputs: vec![recipient_output, change_output], + }; + + let ptx = ptx_witness.commit(); + + let bundle = cl::Bundle { + partials: vec![ptx], + }; + + assert!(bundle.is_balanced(ptx_witness.balance_blinding())) +} diff --git a/goas/cl/ledger/Cargo.toml b/goas/cl/ledger/Cargo.toml index a0767f2..aea2bf7 100644 --- a/goas/cl/ledger/Cargo.toml +++ b/goas/cl/ledger/Cargo.toml @@ -10,4 +10,5 @@ nomos_cl_risc0_proofs = { path = "../risc0_proofs" } risc0-zkvm = { version = "1.0", features = ["prove", "metal"] } risc0-groth16 = { version = "1.0" } rand = "0.8.5" +rand_core = "0.6.0" thiserror = "1.0.62" diff --git a/goas/cl/ledger/src/bundle.rs b/goas/cl/ledger/src/bundle.rs new file mode 100644 index 0000000..7517be3 --- /dev/null +++ b/goas/cl/ledger/src/bundle.rs @@ -0,0 +1,57 @@ +use crate::error::Result; + +pub struct ProvedBundle { + pub bundle: cl::Bundle, + pub risc0_receipt: risc0_zkvm::Receipt, +} + +impl ProvedBundle { + pub fn prove(bundle: &cl::Bundle, bundle_witness: &cl::BundleWitness) -> Self { + // need to show that bundle is balanced. + // i.e. the sum of ptx balances is 0 + + let env = risc0_zkvm::ExecutorEnv::builder() + .write(&bundle_witness) + .unwrap() + .build() + .unwrap(); + + let prover = risc0_zkvm::default_prover(); + + let start_t = std::time::Instant::now(); + + let opts = risc0_zkvm::ProverOpts::succinct(); + let prove_info = prover + .prove_with_opts(env, nomos_cl_risc0_proofs::BUNDLE_ELF, &opts) + .unwrap(); + + println!( + "STARK 'bundle' prover time: {:.2?}, total_cycles: {}", + start_t.elapsed(), + prove_info.stats.total_cycles + ); + + let receipt = prove_info.receipt; + + Self { + bundle: bundle.clone(), + risc0_receipt: receipt, + } + } + + pub fn public(&self) -> Result { + Ok(self.risc0_receipt.journal.decode()?) + } + + pub fn verify(&self) -> bool { + let Ok(zero_commitment) = self.public() else { + return false; + }; + + self.bundle.balance() == zero_commitment + && self + .risc0_receipt + .verify(nomos_cl_risc0_proofs::BUNDLE_ID) + .is_ok() + } +} diff --git a/goas/cl/ledger/src/input.rs b/goas/cl/ledger/src/input.rs index 3e8cf66..0c934b5 100644 --- a/goas/cl/ledger/src/input.rs +++ b/goas/cl/ledger/src/input.rs @@ -4,67 +4,79 @@ use crate::error::Result; const MAX_NOTE_COMMS: usize = 2usize.pow(8); -#[derive(Debug, Clone)] -pub struct InputProof { - receipt: risc0_zkvm::Receipt, +pub struct ProvedInput { + pub input: InputPublic, + pub risc0_receipt: risc0_zkvm::Receipt, } -impl InputProof { - pub fn public(&self) -> Result { - Ok(self.receipt.journal.decode()?) +impl ProvedInput { + pub fn prove(input: &cl::InputWitness, note_commitments: &[cl::NoteCommitment]) -> Self { + let output_cm = input.note_commitment(); + + let cm_leaves = note_commitment_leaves(note_commitments); + let cm_idx = note_commitments + .iter() + .position(|c| c == &output_cm) + .unwrap(); + let cm_path = cl::merkle::path(cm_leaves, cm_idx); + + let secrets = InputPrivate { + input: *input, + cm_path, + }; + + let env = risc0_zkvm::ExecutorEnv::builder() + .write(&secrets) + .unwrap() + .build() + .unwrap(); + + // Obtain the default prover. + let prover = risc0_zkvm::default_prover(); + + let start_t = std::time::Instant::now(); + + // Proof information by proving the specified ELF binary. + // This struct contains the receipt along with statistics about execution of the guest + let opts = risc0_zkvm::ProverOpts::succinct(); + let prove_info = prover + .prove_with_opts(env, nomos_cl_risc0_proofs::INPUT_ELF, &opts) + .unwrap(); + + println!( + "STARK prover time: {:.2?}, total_cycles: {}", + start_t.elapsed(), + prove_info.stats.total_cycles + ); + // extract the receipt. + let receipt = prove_info.receipt; + + Self { + input: InputPublic { + cm_root: cl::merkle::root(cm_leaves), + input: input.commit(), + }, + risc0_receipt: receipt, + } } - pub fn verify(&self, expected_public_inputs: &InputPublic) -> bool { - let Ok(public_inputs) = self.public() else { + pub fn public(&self) -> Result { + Ok(self.risc0_receipt.journal.decode()?) + } + + pub fn verify(&self) -> bool { + let Ok(proved_public_inputs) = self.public() else { return false; }; - &public_inputs == expected_public_inputs - && self.receipt.verify(nomos_cl_risc0_proofs::INPUT_ID).is_ok() + self.input == proved_public_inputs + && self + .risc0_receipt + .verify(nomos_cl_risc0_proofs::INPUT_ID) + .is_ok() } } -pub fn prove_input(input: cl::InputWitness, note_commitments: &[cl::NoteCommitment]) -> InputProof { - let output_cm = input.to_output_witness().commit_note(); - - let cm_leaves = note_commitment_leaves(note_commitments); - let cm_idx = note_commitments - .iter() - .position(|c| c == &output_cm) - .unwrap(); - let cm_path = cl::merkle::path(cm_leaves, cm_idx); - - let secrets = InputPrivate { input, cm_path }; - - let env = risc0_zkvm::ExecutorEnv::builder() - .write(&secrets) - .unwrap() - .build() - .unwrap(); - - // Obtain the default prover. - let prover = risc0_zkvm::default_prover(); - - use std::time::Instant; - let start_t = Instant::now(); - - // Proof information by proving the specified ELF binary. - // This struct contains the receipt along with statistics about execution of the guest - let opts = risc0_zkvm::ProverOpts::succinct(); - let prove_info = prover - .prove_with_opts(env, nomos_cl_risc0_proofs::INPUT_ELF, &opts) - .unwrap(); - - println!( - "STARK prover time: {:.2?}, total_cycles: {}", - start_t.elapsed(), - prove_info.stats.total_cycles - ); - // extract the receipt. - let receipt = prove_info.receipt; - InputProof { receipt } -} - fn note_commitment_leaves(note_commitments: &[cl::NoteCommitment]) -> [[u8; 32]; MAX_NOTE_COMMS] { let note_comm_bytes = Vec::from_iter(note_commitments.iter().map(|c| c.as_bytes().to_vec())); let cm_leaves = cl::merkle::padded_leaves::(¬e_comm_bytes); @@ -78,28 +90,27 @@ mod test { use super::*; #[test] - fn test_input_nullifier_prover() { + fn test_input_prover() { let mut rng = thread_rng(); + let input = cl::InputWitness { - note: cl::NoteWitness { - balance: cl::BalanceWitness::random(32, "NMO", &mut rng), - death_constraint: [0u8; 32], - state: [0u8; 32], - }, + note: cl::NoteWitness::basic(32, "NMO"), + balance_blinding: cl::BalanceWitness::random(&mut rng), nf_sk: cl::NullifierSecret::random(&mut rng), nonce: cl::NullifierNonce::random(&mut rng), }; - let notes = vec![input.to_output_witness().commit_note()]; + let notes = vec![input.note_commitment()]; - let proof = prove_input(input, ¬es); + let mut proved_input = ProvedInput::prove(&input, ¬es); let expected_public_inputs = InputPublic { cm_root: cl::merkle::root(note_commitment_leaves(¬es)), input: input.commit(), }; - assert!(proof.verify(&expected_public_inputs)); + assert_eq!(proved_input.input, expected_public_inputs); + assert!(proved_input.verify()); let wrong_public_inputs = [ InputPublic { @@ -125,7 +136,8 @@ mod test { }, InputPublic { input: cl::Input { - balance: cl::BalanceWitness::random(32, "NMO", &mut rng).commit(), + balance: cl::BalanceWitness::random(&mut rng) + .commit(&cl::NoteWitness::basic(32, "NMO")), ..expected_public_inputs.input }, ..expected_public_inputs @@ -133,57 +145,12 @@ mod test { ]; for wrong_input in wrong_public_inputs { - assert!(!proof.verify(&wrong_input)); + proved_input.input = wrong_input; + assert!(!proved_input.verify()); } } // ----- The following tests still need to be built. ----- - // - // #[test] - // fn test_input_proof() { - // let mut rng = rand::thread_rng(); - - // let ptx_root = cl::PtxRoot::default(); - - // let note = cl::NoteWitness::new(10, "NMO", [0u8; 32], &mut rng); - // let nf_sk = cl::NullifierSecret::random(&mut rng); - // let nonce = cl::NullifierNonce::random(&mut rng); - - // let input_witness = cl::InputWitness { note, nf_sk, nonce }; - - // let input = input_witness.commit(); - // let proof = input.prove(&input_witness, ptx_root, vec![]).unwrap(); - - // assert!(input.verify(ptx_root, &proof)); - - // let wrong_witnesses = [ - // cl::InputWitness { - // note: cl::NoteWitness::new(11, "NMO", [0u8; 32], &mut rng), - // ..input_witness.clone() - // }, - // cl::InputWitness { - // note: cl::NoteWitness::new(10, "ETH", [0u8; 32], &mut rng), - // ..input_witness.clone() - // }, - // cl::InputWitness { - // nf_sk: cl::NullifierSecret::random(&mut rng), - // ..input_witness.clone() - // }, - // cl::InputWitness { - // nonce: cl::NullifierNonce::random(&mut rng), - // ..input_witness.clone() - // }, - // ]; - - // for wrong_witness in wrong_witnesses { - // assert!(input.prove(&wrong_witness, ptx_root, vec![]).is_err()); - - // let wrong_input = wrong_witness.commit(); - // let wrong_proof = wrong_input.prove(&wrong_witness, ptx_root, vec![]).unwrap(); - // assert!(!input.verify(ptx_root, &wrong_proof)); - // } - // } - // #[test] // fn test_input_ptx_coupling() { // let mut rng = rand::thread_rng(); diff --git a/goas/cl/ledger/src/lib.rs b/goas/cl/ledger/src/lib.rs index 2150f1b..bac9e97 100644 --- a/goas/cl/ledger/src/lib.rs +++ b/goas/cl/ledger/src/lib.rs @@ -1,2 +1,6 @@ +// pub mod death_constraint; +pub mod bundle; pub mod error; pub mod input; +pub mod output; +pub mod partial_tx; diff --git a/goas/cl/ledger/src/output.rs b/goas/cl/ledger/src/output.rs new file mode 100644 index 0000000..0ad6ee3 --- /dev/null +++ b/goas/cl/ledger/src/output.rs @@ -0,0 +1,103 @@ +use crate::error::Result; + +pub struct ProvedOutput { + pub output: cl::Output, + pub risc0_receipt: risc0_zkvm::Receipt, +} + +impl ProvedOutput { + pub fn prove(witness: &cl::OutputWitness) -> Self { + let env = risc0_zkvm::ExecutorEnv::builder() + .write(&witness) + .unwrap() + .build() + .unwrap(); + + let prover = risc0_zkvm::default_prover(); + + let start_t = std::time::Instant::now(); + + let opts = risc0_zkvm::ProverOpts::succinct(); + let prove_info = prover + .prove_with_opts(env, nomos_cl_risc0_proofs::OUTPUT_ELF, &opts) + .unwrap(); + + println!( + "STARK 'output' prover time: {:.2?}, total_cycles: {}", + start_t.elapsed(), + prove_info.stats.total_cycles + ); + + let receipt = prove_info.receipt; + + Self { + output: witness.commit(), + risc0_receipt: receipt, + } + } + + pub fn public(&self) -> Result { + Ok(self.risc0_receipt.journal.decode()?) + } + + pub fn verify(&self) -> bool { + let Ok(output_commitments) = self.public() else { + return false; + }; + + self.output == output_commitments + && self + .risc0_receipt + .verify(nomos_cl_risc0_proofs::OUTPUT_ID) + .is_ok() + } +} + +#[cfg(test)] +mod test { + use rand::thread_rng; + + use super::*; + + #[test] + fn test_output_prover() { + let mut rng = thread_rng(); + + let output = cl::OutputWitness { + note: cl::NoteWitness::basic(32, "NMO"), + balance_blinding: cl::BalanceWitness::random(&mut rng), + nf_pk: cl::NullifierSecret::random(&mut rng).commit(), + nonce: cl::NullifierNonce::random(&mut rng), + }; + + let mut proved_output = ProvedOutput::prove(&output); + + let expected_output_cm = output.commit(); + + assert_eq!(proved_output.output, expected_output_cm); + assert!(proved_output.verify()); + + let wrong_output_cms = [ + cl::Output { + note_comm: cl::NoteWitness::basic(100, "NMO").commit( + cl::NullifierSecret::random(&mut rng).commit(), + cl::NullifierNonce::random(&mut rng), + ), + ..expected_output_cm + }, + cl::Output { + note_comm: cl::NoteWitness::basic(100, "NMO").commit( + cl::NullifierSecret::random(&mut rng).commit(), + cl::NullifierNonce::random(&mut rng), + ), + balance: cl::BalanceWitness::random(&mut rng) + .commit(&cl::NoteWitness::basic(100, "NMO")), + }, + ]; + + for wrong_output_cm in wrong_output_cms { + proved_output.output = wrong_output_cm; + assert!(!proved_output.verify()); + } + } +} diff --git a/goas/cl/ledger/src/partial_tx.rs b/goas/cl/ledger/src/partial_tx.rs new file mode 100644 index 0000000..bd155f4 --- /dev/null +++ b/goas/cl/ledger/src/partial_tx.rs @@ -0,0 +1,26 @@ +use crate::{input::ProvedInput, output::ProvedOutput}; + +pub struct ProvedPartialTx { + pub inputs: Vec, + pub outputs: Vec, +} + +impl ProvedPartialTx { + pub fn prove( + ptx: &cl::PartialTxWitness, + note_commitments: &[cl::NoteCommitment], + ) -> ProvedPartialTx { + Self { + inputs: Vec::from_iter( + ptx.inputs + .iter() + .map(|i| ProvedInput::prove(i, note_commitments)), + ), + outputs: Vec::from_iter(ptx.outputs.iter().map(ProvedOutput::prove)), + } + } + + pub fn verify(&self) -> bool { + self.inputs.iter().all(ProvedInput::verify) && self.outputs.iter().all(ProvedOutput::verify) + } +} diff --git a/goas/cl/ledger/tests/simple_transfer.rs b/goas/cl/ledger/tests/simple_transfer.rs new file mode 100644 index 0000000..ecccc91 --- /dev/null +++ b/goas/cl/ledger/tests/simple_transfer.rs @@ -0,0 +1,71 @@ +use ledger::{bundle::ProvedBundle, partial_tx::ProvedPartialTx}; +use rand_core::CryptoRngCore; + +struct User(cl::NullifierSecret); + +impl User { + fn random(mut rng: impl CryptoRngCore) -> Self { + Self(cl::NullifierSecret::random(&mut rng)) + } + + fn pk(&self) -> cl::NullifierCommitment { + self.0.commit() + } + + fn sk(&self) -> cl::NullifierSecret { + self.0 + } +} + +fn receive_utxo( + note: cl::NoteWitness, + nf_pk: cl::NullifierCommitment, + rng: impl CryptoRngCore, +) -> cl::OutputWitness { + cl::OutputWitness::random(note, nf_pk, rng) +} + +#[test] +fn test_simple_transfer() { + let mut rng = rand::thread_rng(); + + // alice is sending 8 NMO to bob. + + let alice = User::random(&mut rng); + let bob = User::random(&mut rng); + + // Alice has an unspent note worth 10 NMO + let utxo = receive_utxo(cl::NoteWitness::basic(10, "NMO"), alice.pk(), &mut rng); + let alices_input = cl::InputWitness::random(utxo, alice.sk(), &mut rng); + + // Alice wants to send 8 NMO to bob + let bobs_output = + cl::OutputWitness::random(cl::NoteWitness::basic(8, "NMO"), bob.pk(), &mut rng); + + // .. and return the 2 NMO in change to herself. + let change_output = + cl::OutputWitness::random(cl::NoteWitness::basic(2, "NMO"), alice.pk(), &mut rng); + + // Construct the ptx consuming Alices inputs and producing the two outputs. + let ptx_witness = cl::PartialTxWitness { + inputs: vec![alices_input], + outputs: vec![bobs_output, change_output], + }; + + // assume we only have one note commitment on chain for now ... + let note_commitments = vec![utxo.commit_note()]; + let proved_ptx = ProvedPartialTx::prove(&ptx_witness, ¬e_commitments); + + assert!(proved_ptx.verify()); // It's a valid ptx. + + let bundle = cl::Bundle { + partials: vec![ptx_witness.commit()], + }; + + let bundle_witness = cl::BundleWitness { + balance_blinding: ptx_witness.balance_blinding(), + }; + + let proved_bundle = ProvedBundle::prove(&bundle, &bundle_witness); + assert!(proved_bundle.verify()); // The bundle is balanced. +} diff --git a/goas/cl/proof_statements/src/death_constraint.rs b/goas/cl/proof_statements/src/death_constraint.rs index efcac9e..019ecdb 100644 --- a/goas/cl/proof_statements/src/death_constraint.rs +++ b/goas/cl/proof_statements/src/death_constraint.rs @@ -3,7 +3,6 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct DeathConstraintPublic { - pub cm_root: [u8; 32], pub nf: Nullifier, pub ptx_root: PtxRoot, } diff --git a/goas/cl/proof_statements/src/ptx.rs b/goas/cl/proof_statements/src/ptx.rs index 7c7f9d7..9701646 100644 --- a/goas/cl/proof_statements/src/ptx.rs +++ b/goas/cl/proof_statements/src/ptx.rs @@ -1,5 +1,6 @@ use cl::{merkle, InputWitness, OutputWitness, PtxRoot}; use serde::{Deserialize, Serialize}; + /// An input to a partial transaction #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct PartialTxInputPrivate { @@ -15,7 +16,7 @@ impl PartialTxInputPrivate { } pub fn cm_root(&self) -> [u8; 32] { - let leaf = merkle::leaf(self.input.to_output_witness().commit_note().as_bytes()); + let leaf = merkle::leaf(self.input.note_commitment().as_bytes()); merkle::path_root(leaf, &self.cm_path) } } diff --git a/goas/cl/risc0_proofs/Cargo.toml b/goas/cl/risc0_proofs/Cargo.toml index 198a3c0..7a77cd5 100644 --- a/goas/cl/risc0_proofs/Cargo.toml +++ b/goas/cl/risc0_proofs/Cargo.toml @@ -7,5 +7,5 @@ edition = "2021" risc0-build = { version = "1.0" } [package.metadata.risc0] -methods = ["input"] +methods = ["input", "output", "bundle", "death_constraint_nop"] diff --git a/goas/cl/risc0_proofs/bundle/Cargo.toml b/goas/cl/risc0_proofs/bundle/Cargo.toml new file mode 100644 index 0000000..a2a0124 --- /dev/null +++ b/goas/cl/risc0_proofs/bundle/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "bundle" +version = "0.1.0" +edition = "2021" + +[workspace] + +[dependencies] +risc0-zkvm = { version = "1.0", default-features = false, features = ['std'] } +serde = { version = "1.0", features = ["derive"] } +cl = { path = "../../cl" } +proof_statements = { path = "../../proof_statements" } + + +[patch.crates-io] +# add RISC Zero accelerator support for all downstream usages of the following crates. +sha2 = { git = "https://github.com/risc0/RustCrypto-hashes", tag = "sha2-v0.10.8-risczero.0" } +crypto-bigint = { git = "https://github.com/risc0/RustCrypto-crypto-bigint", tag = "v0.5.5-risczero.0" } +curve25519-dalek = { git = "https://github.com/risc0/curve25519-dalek", tag = "curve25519-4.1.2-risczero.0" } diff --git a/goas/cl/risc0_proofs/bundle/src/main.rs b/goas/cl/risc0_proofs/bundle/src/main.rs new file mode 100644 index 0000000..d55efec --- /dev/null +++ b/goas/cl/risc0_proofs/bundle/src/main.rs @@ -0,0 +1,17 @@ +/// Bundle Proof +/// +/// The bundle proof demonstrates that the set of partial transactions +/// balance to zero. i.e. \sum inputs = \sum outputs. +/// +/// This is done by proving knowledge of some blinding factor `r` s.t. +/// \sum outputs - \sum input = 0*G + r*H +/// +/// To avoid doing costly ECC in stark, we compute only the RHS in stark. +/// The sums and equality is checked outside of stark during proof verification. +use risc0_zkvm::guest::env; + +fn main() { + let bundle_witness: cl::BundleWitness = env::read(); + let zero_balance = cl::Balance::zero(bundle_witness.balance_blinding); + env::commit(&zero_balance); +} diff --git a/goas/cl/risc0_proofs/death_constraint_nop/Cargo.toml b/goas/cl/risc0_proofs/death_constraint_nop/Cargo.toml new file mode 100644 index 0000000..46cab6a --- /dev/null +++ b/goas/cl/risc0_proofs/death_constraint_nop/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "death_constraint_nop" +version = "0.1.0" +edition = "2021" + +[workspace] + +[dependencies] +risc0-zkvm = { version = "1.0", default-features = false, features = ['std'] } +serde = { version = "1.0", features = ["derive"] } +cl = { path = "../../cl" } +proof_statements = { path = "../../proof_statements" } + + +[patch.crates-io] +# add RISC Zero accelerator support for all downstream usages of the following crates. +sha2 = { git = "https://github.com/risc0/RustCrypto-hashes", tag = "sha2-v0.10.8-risczero.0" } +crypto-bigint = { git = "https://github.com/risc0/RustCrypto-crypto-bigint", tag = "v0.5.5-risczero.0" } +curve25519-dalek = { git = "https://github.com/risc0/curve25519-dalek", tag = "curve25519-4.1.2-risczero.0" } diff --git a/goas/cl/risc0_proofs/death_constraint_nop/src/main.rs b/goas/cl/risc0_proofs/death_constraint_nop/src/main.rs new file mode 100644 index 0000000..09920ad --- /dev/null +++ b/goas/cl/risc0_proofs/death_constraint_nop/src/main.rs @@ -0,0 +1,8 @@ +/// Death Constraint No-op Proof +use proof_statements::death_constraint::DeathConstraintPublic; +use risc0_zkvm::guest::env; + +fn main() { + let public: DeathConstraintPublic = env::read(); + env::commit(&public); +} diff --git a/goas/cl/risc0_proofs/input/src/main.rs b/goas/cl/risc0_proofs/input/src/main.rs index b84517f..baa0737 100644 --- a/goas/cl/risc0_proofs/input/src/main.rs +++ b/goas/cl/risc0_proofs/input/src/main.rs @@ -6,7 +6,7 @@ use risc0_zkvm::guest::env; fn main() { let secret: InputPrivate = env::read(); - let out_cm = secret.input.to_output_witness().commit_note(); + let out_cm = secret.input.note_commitment(); let cm_leaf = merkle::leaf(out_cm.as_bytes()); let cm_root = merkle::path_root(cm_leaf, &secret.cm_path); diff --git a/goas/cl/risc0_proofs/output/Cargo.toml b/goas/cl/risc0_proofs/output/Cargo.toml new file mode 100644 index 0000000..6b4f0f9 --- /dev/null +++ b/goas/cl/risc0_proofs/output/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "output" +version = "0.1.0" +edition = "2021" + +[workspace] + +[dependencies] +risc0-zkvm = { version = "1.0", default-features = false, features = ['std'] } +serde = { version = "1.0", features = ["derive"] } +cl = { path = "../../cl" } +proof_statements = { path = "../../proof_statements" } + + +[patch.crates-io] +# add RISC Zero accelerator support for all downstream usages of the following crates. +sha2 = { git = "https://github.com/risc0/RustCrypto-hashes", tag = "sha2-v0.10.8-risczero.0" } +crypto-bigint = { git = "https://github.com/risc0/RustCrypto-crypto-bigint", tag = "v0.5.5-risczero.0" } +curve25519-dalek = { git = "https://github.com/risc0/curve25519-dalek", tag = "curve25519-4.1.2-risczero.0" } diff --git a/goas/cl/risc0_proofs/output/src/main.rs b/goas/cl/risc0_proofs/output/src/main.rs new file mode 100644 index 0000000..33b5ee0 --- /dev/null +++ b/goas/cl/risc0_proofs/output/src/main.rs @@ -0,0 +1,12 @@ +/// Output Proof +/// +/// given randomness `r` and `note=(value, unit, ...)` prove that +/// - balance = balance_commit(value, unit, r) +/// - note_cm = note_commit(note) +use risc0_zkvm::guest::env; + +fn main() { + let output: cl::OutputWitness = env::read(); + let output_cm = output.commit(); + env::commit(&output_cm); +} diff --git a/goas/zone/risc0_proofs/spend_zone_funds/src/main.rs b/goas/zone/risc0_proofs/spend_zone_funds/src/main.rs index 18a00ea..7510e03 100644 --- a/goas/zone/risc0_proofs/spend_zone_funds/src/main.rs +++ b/goas/zone/risc0_proofs/spend_zone_funds/src/main.rs @@ -18,7 +18,6 @@ fn main() { spend_event_state_path, } = env::read(); - let cm_root = in_zone_funds.cm_root(); let ptx_root = in_zone_funds.ptx_root(); let nf = Nullifier::new(in_zone_funds.input.nf_sk, in_zone_funds.input.nonce); // check the zone funds note is the one in the spend event @@ -39,19 +38,18 @@ fn main() { let change = in_zone_funds .input .note - .balance .value .checked_sub(spend_event.amount) .unwrap(); - assert_eq!(out_zone_funds.output.note.balance.value, change); + assert_eq!(out_zone_funds.output.note.value, change); // zone funds output should have the same death constraints as the zone funds input assert_eq!( out_zone_funds.output.note.death_constraint, in_zone_funds.input.note.death_constraint ); assert_eq!( - out_zone_funds.output.note.balance.unit, - in_zone_funds.input.note.balance.unit + out_zone_funds.output.note.unit, + in_zone_funds.input.note.unit ); // zone funds nullifier, nonce and value blinding should be public so that everybody can spend it assert_eq!( @@ -59,8 +57,8 @@ fn main() { NullifierSecret::from_bytes([0; 16]).commit() ); assert_eq!( - out_zone_funds.output.note.balance.blinding, - in_zone_funds.input.note.balance.blinding + out_zone_funds.output.balance_blinding, + in_zone_funds.input.balance_blinding ); let mut evolved_nonce = [0; 16]; evolved_nonce[..16] @@ -73,16 +71,12 @@ fn main() { assert_eq!(ptx_root, spent_note.ptx_root()); // check the correct amount of funds is being spent - assert_eq!(spent_note.output.note.balance.value, spend_event.amount); - assert_eq!( - spent_note.output.note.balance.unit, - in_zone_funds.input.note.balance.unit - ); + assert_eq!(spent_note.output.note.value, spend_event.amount); + assert_eq!(spent_note.output.note.unit, in_zone_funds.input.note.unit); // check the correct recipient is being paid assert_eq!(spent_note.output.nf_pk, spend_event.to); env::commit(&DeathConstraintPublic { - cm_root, ptx_root, nf, });