From 1d16f40a4c396bd483d10874fded3a57689d1b65 Mon Sep 17 00:00:00 2001 From: David Rusu Date: Sat, 27 Jul 2024 18:55:43 +0400 Subject: [PATCH 1/2] cl: add death constraints to simple transfer scenario --- goas/cl/cl/src/nullifier.rs | 14 ++-- goas/cl/cl/src/output.rs | 2 +- goas/cl/ledger/Cargo.toml | 1 + goas/cl/ledger/src/death_constraint.rs | 83 +++++++++++++++++++ goas/cl/ledger/src/input.rs | 1 + goas/cl/ledger/src/lib.rs | 2 +- goas/cl/ledger/src/partial_tx.rs | 55 ++++++++++-- goas/cl/ledger/tests/simple_transfer.rs | 22 ++++- .../proof_statements/src/death_constraint.rs | 2 +- 9 files changed, 160 insertions(+), 22 deletions(-) create mode 100644 goas/cl/ledger/src/death_constraint.rs diff --git a/goas/cl/cl/src/nullifier.rs b/goas/cl/cl/src/nullifier.rs index f0cf311..84189a1 100644 --- a/goas/cl/cl/src/nullifier.rs +++ b/goas/cl/cl/src/nullifier.rs @@ -33,7 +33,7 @@ pub struct NullifierNonce([u8; 32]); // The nullifier attached to input notes to prove an input has not // already been spent. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] pub struct Nullifier([u8; 32]); impl NullifierSecret { @@ -83,13 +83,13 @@ impl NullifierNonce { } pub fn evolve(&self, nf_sk: &NullifierSecret) -> Self { - let mut hasher = Sha256::new(); - hasher.update(b"NOMOS_COIN_EVOLVE"); - hasher.update(&self.0); - hasher.update(nf_sk.0); + let mut hasher = Sha256::new(); + hasher.update(b"NOMOS_COIN_EVOLVE"); + hasher.update(&self.0); + hasher.update(nf_sk.0); - let nonce_bytes: [u8; 32] = hasher.finalize().into(); - Self(nonce_bytes) + let nonce_bytes: [u8; 32] = hasher.finalize().into(); + Self(nonce_bytes) } } diff --git a/goas/cl/cl/src/output.rs b/goas/cl/cl/src/output.rs index 6758fcd..1335c23 100644 --- a/goas/cl/cl/src/output.rs +++ b/goas/cl/cl/src/output.rs @@ -9,7 +9,7 @@ use crate::{ BalanceWitness, }; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct Output { pub note_comm: NoteCommitment, pub balance: Balance, diff --git a/goas/cl/ledger/Cargo.toml b/goas/cl/ledger/Cargo.toml index aea2bf7..0dc7269 100644 --- a/goas/cl/ledger/Cargo.toml +++ b/goas/cl/ledger/Cargo.toml @@ -12,3 +12,4 @@ risc0-groth16 = { version = "1.0" } rand = "0.8.5" rand_core = "0.6.0" thiserror = "1.0.62" +sha2 = "0.10" diff --git a/goas/cl/ledger/src/death_constraint.rs b/goas/cl/ledger/src/death_constraint.rs new file mode 100644 index 0000000..066e9a1 --- /dev/null +++ b/goas/cl/ledger/src/death_constraint.rs @@ -0,0 +1,83 @@ +use proof_statements::death_constraint::DeathConstraintPublic; +use sha2::{Digest, Sha256}; + +use crate::error::Result; + +pub type Risc0DeathConstraintId = [u32; 8]; + +#[derive(Debug, Clone)] +pub struct DeathProof { + constraint: Risc0DeathConstraintId, + risc0_receipt: risc0_zkvm::Receipt, +} + +fn risc0_id_to_cl_death_constraint(risc0_id: Risc0DeathConstraintId) -> [u8; 32] { + // RISC0 proof ids have the format: [u32; 8], and cl death constraint ids have the format [u8; 32]. + // CL death constraints are meaningless beyond being binding, therefore we merely need a collision + // resisitant mapping of RISC0 ids to cl death constraints. + + let mut hasher = Sha256::new(); + hasher.update(b"NOMOS_RISC0_ID_TO_CL_DEATH_CONSTRAINT"); + for word in risc0_id { + hasher.update(u32::to_ne_bytes(word)); + } + let death_constraint: [u8; 32] = hasher.finalize().into(); + death_constraint +} + +impl DeathProof { + pub fn death_commitment(&self) -> cl::DeathCommitment { + cl::note::death_commitment(&risc0_id_to_cl_death_constraint(self.constraint)) + } + + pub fn public(&self) -> Result { + Ok(self.risc0_receipt.journal.decode()?) + } + + pub fn verify(&self, expected_public: DeathConstraintPublic) -> bool { + let Ok(public) = self.public() else { + return false; + }; + + expected_public == public && self.risc0_receipt.verify(self.constraint).is_ok() + } + + pub fn nop_constraint() -> [u8; 32] { + risc0_id_to_cl_death_constraint(nomos_cl_risc0_proofs::DEATH_CONSTRAINT_NOP_ID) + } + + pub fn prove_nop(nf: cl::Nullifier, ptx_root: cl::PtxRoot) -> Self { + let death_public = DeathConstraintPublic { nf, ptx_root }; + let env = risc0_zkvm::ExecutorEnv::builder() + .write(&death_public) + .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::DEATH_CONSTRAINT_NOP_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 { + constraint: nomos_cl_risc0_proofs::DEATH_CONSTRAINT_NOP_ID, + risc0_receipt: receipt, + } + } +} diff --git a/goas/cl/ledger/src/input.rs b/goas/cl/ledger/src/input.rs index 0c934b5..5aedf19 100644 --- a/goas/cl/ledger/src/input.rs +++ b/goas/cl/ledger/src/input.rs @@ -4,6 +4,7 @@ use crate::error::Result; const MAX_NOTE_COMMS: usize = 2usize.pow(8); +#[derive(Debug, Clone)] pub struct ProvedInput { pub input: InputPublic, pub risc0_receipt: risc0_zkvm::Receipt, diff --git a/goas/cl/ledger/src/lib.rs b/goas/cl/ledger/src/lib.rs index bac9e97..115f111 100644 --- a/goas/cl/ledger/src/lib.rs +++ b/goas/cl/ledger/src/lib.rs @@ -1,5 +1,5 @@ -// pub mod death_constraint; pub mod bundle; +pub mod death_constraint; pub mod error; pub mod input; pub mod output; diff --git a/goas/cl/ledger/src/partial_tx.rs b/goas/cl/ledger/src/partial_tx.rs index bd155f4..300f302 100644 --- a/goas/cl/ledger/src/partial_tx.rs +++ b/goas/cl/ledger/src/partial_tx.rs @@ -1,26 +1,65 @@ -use crate::{input::ProvedInput, output::ProvedOutput}; +use std::collections::BTreeMap; + +use proof_statements::death_constraint::DeathConstraintPublic; + +use crate::{death_constraint::DeathProof, input::ProvedInput, output::ProvedOutput}; + +#[derive(Debug, Clone)] +pub struct PartialTxInput { + pub input: ProvedInput, + pub death: DeathProof, +} + +impl PartialTxInput { + fn verify(&self, ptx_root: cl::PtxRoot) -> bool { + let nf = self.input.input.input.nullifier; + self.input.input.input.death_cm == self.death.death_commitment() // ensure the death proof is actually for this input + && self.input.verify() // ensure the input proof is valie + && self.death.verify(DeathConstraintPublic { nf, ptx_root }) // verify the death constraint was satisfied + } +} pub struct ProvedPartialTx { - pub inputs: Vec, + pub inputs: Vec, pub outputs: Vec, } impl ProvedPartialTx { pub fn prove( ptx: &cl::PartialTxWitness, + mut death_proofs: BTreeMap, note_commitments: &[cl::NoteCommitment], ) -> ProvedPartialTx { Self { - inputs: Vec::from_iter( - ptx.inputs - .iter() - .map(|i| ProvedInput::prove(i, note_commitments)), - ), + inputs: Vec::from_iter(ptx.inputs.iter().map(|i| { + PartialTxInput { + input: ProvedInput::prove(i, note_commitments), + death: death_proofs + .remove(&i.nullifier()) + .expect("Input missing death proof"), + } + })), outputs: Vec::from_iter(ptx.outputs.iter().map(ProvedOutput::prove)), } } + pub fn ptx(&self) -> cl::PartialTx { + cl::PartialTx { + inputs: Vec::from_iter(self.inputs.iter().map(|i| i.input.input.input)), + outputs: Vec::from_iter(self.outputs.iter().map(|o| o.output)), + } + } + + pub fn verify_inputs(&self) -> bool { + let ptx_root = self.ptx().root(); + self.inputs.iter().all(|i| i.verify(ptx_root)) + } + + pub fn verify_outputs(&self) -> bool { + self.outputs.iter().all(|o| o.verify()) + } + pub fn verify(&self) -> bool { - self.inputs.iter().all(ProvedInput::verify) && self.outputs.iter().all(ProvedOutput::verify) + self.verify_inputs() && self.verify_outputs() } } diff --git a/goas/cl/ledger/tests/simple_transfer.rs b/goas/cl/ledger/tests/simple_transfer.rs index ecccc91..81ca545 100644 --- a/goas/cl/ledger/tests/simple_transfer.rs +++ b/goas/cl/ledger/tests/simple_transfer.rs @@ -1,4 +1,6 @@ -use ledger::{bundle::ProvedBundle, partial_tx::ProvedPartialTx}; +use std::collections::BTreeMap; + +use ledger::{bundle::ProvedBundle, death_constraint::DeathProof, partial_tx::ProvedPartialTx}; use rand_core::CryptoRngCore; struct User(cl::NullifierSecret); @@ -35,7 +37,11 @@ fn test_simple_transfer() { 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 utxo = receive_utxo( + cl::NoteWitness::stateless(10, "NMO", DeathProof::nop_constraint()), + alice.pk(), + &mut rng, + ); let alices_input = cl::InputWitness::random(utxo, alice.sk(), &mut rng); // Alice wants to send 8 NMO to bob @@ -52,9 +58,17 @@ fn test_simple_transfer() { outputs: vec![bobs_output, change_output], }; + // Prove the death constraints for alices input (she uses the no-op death constraint) + let death_proofs = BTreeMap::from_iter(ptx_witness.inputs.iter().map(|i| { + ( + i.nullifier(), + DeathProof::prove_nop(i.nullifier(), ptx_witness.commit().root()), + ) + })); + // 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); + let proved_ptx = ProvedPartialTx::prove(&ptx_witness, death_proofs, ¬e_commitments); assert!(proved_ptx.verify()); // It's a valid ptx. @@ -63,7 +77,7 @@ fn test_simple_transfer() { }; let bundle_witness = cl::BundleWitness { - balance_blinding: ptx_witness.balance_blinding(), + balance_blinding: ptx_witness.balance_blinding(), }; let proved_bundle = ProvedBundle::prove(&bundle, &bundle_witness); diff --git a/goas/cl/proof_statements/src/death_constraint.rs b/goas/cl/proof_statements/src/death_constraint.rs index 019ecdb..92e64f6 100644 --- a/goas/cl/proof_statements/src/death_constraint.rs +++ b/goas/cl/proof_statements/src/death_constraint.rs @@ -1,7 +1,7 @@ use cl::{Nullifier, PtxRoot}; use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct DeathConstraintPublic { pub nf: Nullifier, pub ptx_root: PtxRoot, From 762fdcc9803a151796272f409cea201e9536f9ca Mon Sep 17 00:00:00 2001 From: David Rusu Date: Mon, 29 Jul 2024 14:52:45 +0400 Subject: [PATCH 2/2] cl: typo --- goas/cl/ledger/src/death_constraint.rs | 2 +- goas/cl/ledger/src/input.rs | 2 +- goas/cl/ledger/src/partial_tx.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/goas/cl/ledger/src/death_constraint.rs b/goas/cl/ledger/src/death_constraint.rs index 066e9a1..e00972c 100644 --- a/goas/cl/ledger/src/death_constraint.rs +++ b/goas/cl/ledger/src/death_constraint.rs @@ -67,7 +67,7 @@ impl DeathProof { .unwrap(); println!( - "STARK prover time: {:.2?}, total_cycles: {}", + "STARK 'death-nop' prover time: {:.2?}, total_cycles: {}", start_t.elapsed(), prove_info.stats.total_cycles ); diff --git a/goas/cl/ledger/src/input.rs b/goas/cl/ledger/src/input.rs index 5aedf19..49ad59f 100644 --- a/goas/cl/ledger/src/input.rs +++ b/goas/cl/ledger/src/input.rs @@ -45,7 +45,7 @@ impl ProvedInput { .unwrap(); println!( - "STARK prover time: {:.2?}, total_cycles: {}", + "STARK 'input' prover time: {:.2?}, total_cycles: {}", start_t.elapsed(), prove_info.stats.total_cycles ); diff --git a/goas/cl/ledger/src/partial_tx.rs b/goas/cl/ledger/src/partial_tx.rs index 300f302..d647eba 100644 --- a/goas/cl/ledger/src/partial_tx.rs +++ b/goas/cl/ledger/src/partial_tx.rs @@ -14,7 +14,7 @@ impl PartialTxInput { fn verify(&self, ptx_root: cl::PtxRoot) -> bool { let nf = self.input.input.input.nullifier; self.input.input.input.death_cm == self.death.death_commitment() // ensure the death proof is actually for this input - && self.input.verify() // ensure the input proof is valie + && self.input.verify() // ensure the input proof is valid && self.death.verify(DeathConstraintPublic { nf, ptx_root }) // verify the death constraint was satisfied } }