diff --git a/goas/cl/cl/src/balance.rs b/goas/cl/cl/src/balance.rs index 9f9a05e..b49f18b 100644 --- a/goas/cl/cl/src/balance.rs +++ b/goas/cl/cl/src/balance.rs @@ -17,6 +17,16 @@ pub struct Balance(pub RistrettoPoint); 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() } diff --git a/goas/cl/cl/src/bundle.rs b/goas/cl/cl/src/bundle.rs index a08da62..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}; - -use crate::{partial_tx::PartialTx, BalanceWitness}; +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,26 +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: 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, witness: BalanceWitness) -> bool { - self.balance() == crate::balance::balance(0, RISTRETTO_BASEPOINT_POINT, witness.0) + 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::*; @@ -57,8 +55,8 @@ mod test { 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 { @@ -70,22 +68,14 @@ mod test { }; 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.balance_blinding.0) - - (crate::balance::balance( - 10, - hash_to_curve(b"NMO"), - nmo_10_in.balance_blinding.0 - ) + crate::balance::balance( - 23, - hash_to_curve(b"ETH"), - eth_23_in.balance_blinding.0 - )) + 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(crv_4840_out, nf_c, &mut rng); @@ -100,12 +90,13 @@ mod test { &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: BalanceWitness::new( @@ -117,15 +108,6 @@ mod test { ), }; - assert_eq!( - bundle.balance(), - crate::balance::balance( - 0, - curve25519_dalek::constants::RISTRETTO_BASEPOINT_POINT, - witness.balance_blinding.0 - ) - ); - assert!(bundle.is_balanced(witness.balance_blinding)); } } diff --git a/goas/cl/cl/src/input.rs b/goas/cl/cl/src/input.rs index 246952d..8a9ff2f 100644 --- a/goas/cl/cl/src/input.rs +++ b/goas/cl/cl/src/input.rs @@ -21,7 +21,6 @@ pub struct Input { #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct InputWitness { pub note: NoteWitness, - pub utxo_balance_blinding: BalanceWitness, pub balance_blinding: BalanceWitness, pub nf_sk: NullifierSecret, pub nonce: NullifierNonce, @@ -36,7 +35,6 @@ impl InputWitness { assert_eq!(nf_sk.commit(), output.nf_pk); Self { note: output.note, - utxo_balance_blinding: output.balance_blinding, balance_blinding: BalanceWitness::random(&mut rng), nf_sk, nonce: output.nonce, @@ -55,13 +53,8 @@ impl InputWitness { } } - pub fn to_output(&self) -> crate::OutputWitness { - crate::OutputWitness { - note: self.note, - balance_blinding: self.utxo_balance_blinding, - 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 44ef951..10a773e 100644 --- a/goas/cl/cl/src/note.rs +++ b/goas/cl/cl/src/note.rs @@ -40,22 +40,26 @@ pub struct NoteWitness { } impl NoteWitness { - pub fn new(value: u64, unit: impl Into, state: [u8; 32]) -> Self { + pub fn new( + value: u64, + unit: impl Into, + death_constraint: [u8; 32], + state: [u8; 32], + ) -> Self { Self { value, unit: unit_point(&unit.into()), - death_constraint: [0u8; 32], + death_constraint, state, } } pub fn basic(value: u64, unit: impl Into) -> Self { - Self { - value, - unit: unit_point(&unit.into()), - death_constraint: [0u8; 32], - state: [0u8; 32], - } + 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 { @@ -99,7 +103,7 @@ mod test { let nf_pk = NullifierSecret::random(&mut rng).commit(); let nf_nonce = NullifierNonce::random(&mut rng); - let reference_note = NoteWitness::new(32, "NMO", [0u8; 32]); + let reference_note = NoteWitness::basic(32, "NMO"); // different notes under same nullifier produce different commitments let mutation_tests = [ diff --git a/goas/cl/cl/src/output.rs b/goas/cl/cl/src/output.rs index ec71c6a..6758fcd 100644 --- a/goas/cl/cl/src/output.rs +++ b/goas/cl/cl/src/output.rs @@ -107,23 +107,23 @@ mod test { let wrong_witnesses = [ OutputWitness { note: NoteWitness::basic(11, "NMO"), - ..witness.clone() + ..witness }, OutputWitness { note: NoteWitness::basic(10, "ETH"), - ..witness.clone() + ..witness }, OutputWitness { balance_blinding: BalanceWitness::random(&mut rng), - ..witness.clone() + ..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 6be8d77..afe9785 100644 --- a/goas/cl/cl/src/partial_tx.rs +++ b/goas/cl/cl/src/partial_tx.rs @@ -1,7 +1,9 @@ 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}; @@ -44,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)); @@ -95,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::*; @@ -130,21 +141,15 @@ mod test { 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.balance_blinding.0) - - (crate::balance::balance(10, hash_to_curve(b"NMO"), nmo_10.balance_blinding.0) - + crate::balance::balance( - 23, - hash_to_curve(b"ETH"), - eth_23.balance_blinding.0 - )) + 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 ace8e0f..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().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,27 +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::basic(32, "NMO"), - utxo_balance_blinding: cl::BalanceWitness::random(&mut rng), 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().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 { @@ -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 2428e09..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().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 fa63ab3..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().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 97beecd..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 @@ -78,7 +77,6 @@ fn main() { assert_eq!(spent_note.output.nf_pk, spend_event.to); env::commit(&DeathConstraintPublic { - cm_root, ptx_root, nf, });