cl/integrate risc0 (#103)

* cl: restructure cl into a workspace and integrate nullifier proof

* cl: get cl tests building again

* cl: cleanup commented out code

* cl: comment out the failing ptx tests for now.
This commit is contained in:
davidrusu 2024-07-15 16:51:21 +04:00 committed by GitHub
parent 3d2459052c
commit 69c50316d6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 457 additions and 552 deletions

View File

@ -1,21 +1,11 @@
[package]
name = "cl"
version = "0.1.0"
edition = "2021"
[workspace]
resolver = "2"
members = [ "cl", "ledger", "proof_statements", "risc0_proofs"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[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_core = "0.6.0"
rand_chacha = "0.3.1"
lazy_static = "1.4.0"
hex = "0.4.3"
curve25519-dalek = {version = "4.1", features = ["serde", "digest", "rand_core"]}
sha2 = "0.10"
# Always optimize; building and running the risc0_proofs takes much longer without optimization.
[profile.dev]
opt-level = 3
[profile.release]
debug = 1
lto = true

20
cl/cl/Cargo.toml Normal file
View File

@ -0,0 +1,20 @@
[package]
name = "cl"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[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"
lazy_static = "1.4.0"
hex = "0.4.3"
curve25519-dalek = {version = "4.1", features = ["serde", "digest", "rand_core"]}
sha2 = "0.10"

View File

@ -149,8 +149,6 @@ pub fn balance(value: u64, unit: RistrettoPoint, blinding: Scalar) -> RistrettoP
mod test {
use super::*;
use crate::test_util::seed_rng;
use k256::elliptic_curve::group::prime::PrimeCurveAffine;
#[test]
fn test_pederson_blinding_point_pre_compute() {
@ -166,8 +164,8 @@ mod test {
#[test]
fn test_balance_zero_unitless() {
// Zero is the same across all units
let rng = seed_rng(0);
let r = Scalar::random(rng);
let mut rng = rand::thread_rng();
let r = Scalar::random(&mut rng);
assert_eq!(
BalanceWitness::new(0, "NMO", r).commit(),
BalanceWitness::new(0, "ETH", r).commit(),
@ -184,10 +182,7 @@ mod test {
let a = a_w.commit();
let b = b_w.commit();
assert_ne!(a, b);
assert_eq!(
a.0.to_curve() - b.0.to_curve(),
BalanceWitness::new(0, "NMO", r1 - r2).commit().0.to_curve()
);
assert_eq!(a.0 - b.0, BalanceWitness::new(0, "NMO", r1 - r2).commit().0);
}
#[test]
@ -201,7 +196,7 @@ mod test {
#[test]
fn test_balance_homomorphism() {
let mut rng = seed_rng(0);
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());
@ -209,16 +204,13 @@ mod test {
let two = BalanceWitness::new(2, "NMO", 0u32.into());
// Values of same unit are homomorphic
assert_eq!(
ten.commit().0.to_curve() - eight.commit().0.to_curve(),
two.commit().0.to_curve()
);
assert_eq!(ten.commit().0 - eight.commit().0, two.commit().0);
// Blinding factors are also homomorphic.
assert_eq!(
BalanceWitness::new(10, "NMO", r1).commit().0.to_curve()
- BalanceWitness::new(10, "NMO", r2).commit().0.to_curve(),
BalanceWitness::new(0, "NMO", r1 - r2).commit().0.to_curve()
BalanceWitness::new(10, "NMO", r1).commit().0
- BalanceWitness::new(10, "NMO", r2).commit().0,
BalanceWitness::new(0, "NMO", r1 - r2).commit().0
);
}
}

View File

@ -1,14 +1,8 @@
use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use curve25519_dalek::{constants::RISTRETTO_BASEPOINT_POINT, ristretto::RistrettoPoint, Scalar};
use crate::{
error::Error,
note::NoteCommitment,
partial_tx::{PartialTx, PartialTxProof},
};
use crate::partial_tx::PartialTx;
/// The transaction bundle is a collection of partial transactions.
/// The goal in bundling transactions is to produce a set of partial transactions
@ -24,12 +18,6 @@ pub struct BundleWitness {
pub balance_blinding: Scalar,
}
#[derive(Debug)]
pub struct BundleProof {
pub partials: Vec<PartialTxProof>,
pub balance_blinding: Scalar,
}
impl Bundle {
pub fn balance(&self) -> RistrettoPoint {
self.partials.iter().map(|ptx| ptx.balance()).sum()
@ -39,68 +27,20 @@ impl Bundle {
self.balance()
== crate::balance::balance(0, RISTRETTO_BASEPOINT_POINT, balance_blinding_witness)
}
pub fn prove(
&self,
w: BundleWitness,
ptx_proofs: Vec<PartialTxProof>,
) -> Result<BundleProof, Error> {
if ptx_proofs.len() == self.partials.len() {
return Err(Error::ProofFailed);
}
let input_notes: Vec<NoteCommitment> = self
.partials
.iter()
.flat_map(|ptx| ptx.inputs.iter().map(|i| i.note_comm))
.collect();
if input_notes.len() != BTreeSet::from_iter(input_notes.iter()).len() {
return Err(Error::ProofFailed);
}
let output_notes: Vec<NoteCommitment> = self
.partials
.iter()
.flat_map(|ptx| ptx.outputs.iter().map(|o| o.note_comm))
.collect();
if output_notes.len() != BTreeSet::from_iter(output_notes.iter()).len() {
return Err(Error::ProofFailed);
}
if self.balance()
!= crate::balance::balance(0, RISTRETTO_BASEPOINT_POINT, w.balance_blinding)
{
return Err(Error::ProofFailed);
}
Ok(BundleProof {
partials: ptx_proofs,
balance_blinding: w.balance_blinding,
})
}
pub fn verify(&self, proof: BundleProof) -> bool {
proof.partials.len() == self.partials.len()
&& self.is_balanced(proof.balance_blinding)
&& self
.partials
.iter()
.zip(&proof.partials)
.all(|(p, p_proof)| p.verify(p_proof))
}
}
#[cfg(test)]
mod test {
use crate::{
crypto::hash_to_curve, input::InputWitness, note::NoteWitness, nullifier::NullifierSecret,
output::OutputWitness, partial_tx::PartialTxWitness, test_util::seed_rng,
output::OutputWitness, partial_tx::PartialTxWitness,
};
use super::*;
#[test]
fn test_bundle_balance() {
let mut rng = seed_rng(0);
let mut rng = rand::thread_rng();
let nmo_10_in =
InputWitness::random(NoteWitness::new(10, "NMO", [0u8; 32], &mut rng), &mut rng);
@ -175,7 +115,11 @@ mod test {
assert_eq!(
bundle.balance(),
crate::balance::balance(0, RistrettoPoint::GENERATOR, witness.balance_blinding)
crate::balance::balance(
0,
curve25519_dalek::constants::RISTRETTO_BASEPOINT_POINT,
witness.balance_blinding
)
);
assert!(bundle.is_balanced(witness.balance_blinding));

62
cl/cl/src/input.rs Normal file
View File

@ -0,0 +1,62 @@
/// This module defines the partial transaction structure.
///
/// Partial transactions, as the name suggests, are transactions
/// which on their own may not balance (i.e. \sum inputs != \sum outputs)
use crate::{
balance::Balance,
note::{NoteCommitment, NoteWitness},
nullifier::{Nullifier, NullifierNonce, NullifierSecret},
};
use rand_core::RngCore;
// use risc0_groth16::{PublicInputsJson, Verifier};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Input {
pub note_comm: NoteCommitment,
pub nullifier: Nullifier,
pub balance: Balance,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct InputWitness {
pub note: NoteWitness,
pub nf_sk: NullifierSecret,
pub nonce: NullifierNonce,
}
impl InputWitness {
pub fn random(note: NoteWitness, mut rng: impl RngCore) -> Self {
Self {
note,
nf_sk: NullifierSecret::random(&mut rng),
nonce: NullifierNonce::random(&mut rng),
}
}
pub fn commit(&self) -> Input {
Input {
note_comm: self.note.commit(self.nf_sk.commit(), self.nonce),
nullifier: Nullifier::new(self.nf_sk, self.nonce),
balance: self.note.balance(),
}
}
pub fn to_output_witness(&self) -> crate::OutputWitness {
crate::OutputWitness {
note: self.note.clone(),
nf_pk: self.nf_sk.commit(),
nonce: self.nonce,
}
}
}
impl Input {
pub fn to_bytes(&self) -> [u8; 96] {
let mut bytes = [0u8; 96];
bytes[..32].copy_from_slice(self.note_comm.as_bytes());
bytes[32..64].copy_from_slice(self.nullifier.as_bytes());
bytes[64..96].copy_from_slice(&self.balance.to_bytes());
bytes
}
}

View File

@ -16,6 +16,3 @@ pub use note::{NoteCommitment, NoteWitness};
pub use nullifier::{Nullifier, NullifierCommitment, NullifierNonce, NullifierSecret};
pub use output::{Output, OutputWitness};
pub use partial_tx::{PartialTx, PartialTxWitness, PtxRoot};
#[cfg(test)]
mod test_util;

View File

@ -49,7 +49,7 @@ pub enum PathNode {
Right([u8; 32]),
}
pub fn verify_path(leaf: [u8; 32], path: &[PathNode], root: [u8; 32]) -> bool {
pub fn path_root(leaf: [u8; 32], path: &[PathNode]) -> [u8; 32] {
let mut computed_hash = leaf;
for path_node in path {
@ -63,7 +63,7 @@ pub fn verify_path(leaf: [u8; 32], path: &[PathNode], root: [u8; 32]) -> bool {
}
}
computed_hash == root
computed_hash
}
pub fn path<const N: usize>(leaves: [[u8; 32]; N], idx: usize) -> Vec<PathNode> {
@ -163,7 +163,7 @@ mod test {
let p = path::<1>(leaves, 0);
let expected = vec![];
assert_eq!(p, expected);
assert!(verify_path(leaf(b"desert"), &p, r));
assert_eq!(path_root(leaf(b"desert"), &p), r);
}
#[test]
@ -176,14 +176,14 @@ mod test {
let p0 = path(leaves, 0);
let expected0 = vec![PathNode::Right(leaf(b"sand"))];
assert_eq!(p0, expected0);
assert!(verify_path(leaf(b"desert"), &p0, r));
assert_eq!(path_root(leaf(b"desert"), &p0), r);
// --- proof for element at idx 1
let p1 = path(leaves, 1);
let expected1 = vec![PathNode::Left(leaf(b"desert"))];
assert_eq!(p1, expected1);
assert!(verify_path(leaf(b"sand"), &p1, r));
assert_eq!(path_root(leaf(b"sand"), &p1), r);
}
#[test]
@ -204,7 +204,7 @@ mod test {
PathNode::Right(node(leaf(b"feels"), leaf(b"warm"))),
];
assert_eq!(p0, expected0);
assert!(verify_path(leaf(b"desert"), &p0, r));
assert_eq!(path_root(leaf(b"desert"), &p0), r);
// --- proof for element at idx 1
@ -214,7 +214,7 @@ mod test {
PathNode::Right(node(leaf(b"feels"), leaf(b"warm"))),
];
assert_eq!(p1, expected1);
assert!(verify_path(leaf(b"sand"), &p1, r));
assert_eq!(path_root(leaf(b"sand"), &p1), r);
// --- proof for element at idx 2
@ -224,7 +224,7 @@ mod test {
PathNode::Left(node(leaf(b"desert"), leaf(b"sand"))),
];
assert_eq!(p2, expected2);
assert!(verify_path(leaf(b"feels"), &p2, r));
assert_eq!(path_root(leaf(b"feels"), &p2), r);
// --- proof for element at idx 3
@ -234,6 +234,6 @@ mod test {
PathNode::Left(node(leaf(b"desert"), leaf(b"sand"))),
];
assert_eq!(p3, expected3);
assert!(verify_path(leaf(b"warm"), &p3, r));
assert_eq!(path_root(leaf(b"warm"), &p3), r);
}
}

View File

@ -1,6 +1,6 @@
use sha2::{Digest, Sha256};
use rand_core::CryptoRngCore;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::{
balance::{Balance, BalanceWitness},
@ -69,13 +69,13 @@ impl NoteWitness {
#[cfg(test)]
mod test {
use crate::{nullifier::NullifierSecret, test_util::seed_rng};
use crate::nullifier::NullifierSecret;
use super::*;
#[test]
fn test_note_commitments_dont_commit_to_balance_blinding() {
let mut rng = seed_rng(0);
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);

View File

@ -94,7 +94,6 @@ impl Nullifier {
#[cfg(test)]
mod test {
use super::*;
use crate::test_util::seed_rng;
#[test]
fn test_nullifier_commitment_vectors() {
@ -114,9 +113,10 @@ mod test {
#[test]
fn test_nullifier_same_sk_different_nonce() {
let sk = NullifierSecret::random(seed_rng(0));
let nonce_1 = NullifierNonce::random(seed_rng(1));
let nonce_2 = NullifierNonce::random(seed_rng(2));
let mut rng = rand::thread_rng();
let sk = NullifierSecret::random(&mut rng);
let nonce_1 = NullifierNonce::random(&mut rng);
let nonce_2 = NullifierNonce::random(&mut rng);
let nf_1 = Nullifier::new(sk, nonce_1);
let nf_2 = Nullifier::new(sk, nonce_2);

View File

@ -14,7 +14,7 @@ pub struct Output {
pub balance: Balance,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OutputWitness {
pub note: NoteWitness,
pub nf_pk: NullifierCommitment,
@ -76,11 +76,11 @@ impl Output {
#[cfg(test)]
mod test {
use super::*;
use crate::{nullifier::NullifierSecret, test_util::seed_rng};
use crate::nullifier::NullifierSecret;
#[test]
fn test_output_proof() {
let mut rng = seed_rng(0);
let mut rng = rand::thread_rng();
let note = NoteWitness::new(10, "NMO", [0u8; 32], &mut rng);
let nf_pk = NullifierSecret::random(&mut rng).commit();

View File

@ -1,14 +1,11 @@
use std::collections::BTreeSet;
use rand_core::RngCore;
// use risc0_groth16::ProofJson;
use curve25519_dalek::ristretto::RistrettoPoint;
use serde::{Deserialize, Serialize};
use crate::error::Error;
use crate::input::{Input, InputProof, InputWitness};
use crate::input::{Input, InputWitness};
use crate::merkle;
use crate::output::{Output, OutputProof, OutputWitness};
use crate::output::{Output, OutputWitness};
const MAX_INPUTS: usize = 8;
const MAX_OUTPUTS: usize = 8;
@ -42,12 +39,6 @@ pub struct PartialTxWitness {
pub outputs: Vec<OutputWitness>,
}
#[derive(Debug)]
pub struct PartialTxProof {
pub inputs: Vec<InputProof>,
pub outputs: Vec<OutputProof>,
}
impl PartialTx {
pub fn from_witness(w: PartialTxWitness) -> Self {
Self {
@ -99,64 +90,6 @@ impl PartialTx {
PtxRoot(root)
}
pub fn prove(
&self,
w: PartialTxWitness,
death_proofs: Vec<Vec<u8>>,
) -> Result<PartialTxProof, Error> {
if bincode::serialize(&Self::from_witness(w.clone())).unwrap()
!= bincode::serialize(&self).unwrap()
{
return Err(Error::ProofFailed);
}
let input_note_comms = BTreeSet::from_iter(self.inputs.iter().map(|i| i.note_comm));
let output_note_comms = BTreeSet::from_iter(self.outputs.iter().map(|o| o.note_comm));
if input_note_comms.len() != self.inputs.len()
|| output_note_comms.len() != self.outputs.len()
{
return Err(Error::ProofFailed);
}
let ptx_root = self.root();
let input_proofs: Vec<InputProof> = Result::from_iter(
self.inputs
.iter()
.zip(&w.inputs)
.zip(death_proofs.into_iter())
.map(|((i, i_w), death_p)| i.prove(i_w, ptx_root, death_p)),
)?;
let output_proofs: Vec<OutputProof> = Result::from_iter(
self.outputs
.iter()
.zip(&w.outputs)
.map(|(o, o_w)| o.prove(o_w)),
)?;
Ok(PartialTxProof {
inputs: input_proofs,
outputs: output_proofs,
})
}
pub fn verify(&self, proof: &PartialTxProof) -> bool {
let ptx_root = self.root();
self.inputs.len() == proof.inputs.len()
&& self.outputs.len() == proof.outputs.len()
&& self
.inputs
.iter()
.zip(&proof.inputs)
.all(|(i, p)| i.verify(ptx_root, p))
&& self
.outputs
.iter()
.zip(&proof.outputs)
.all(|(o, p)| o.verify(p))
}
pub fn balance(&self) -> RistrettoPoint {
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();
@ -168,41 +101,13 @@ impl PartialTx {
#[cfg(test)]
mod test {
use crate::{
crypto::hash_to_curve, note::NoteWitness, nullifier::NullifierSecret, test_util::seed_rng,
};
use crate::{crypto::hash_to_curve, note::NoteWitness, nullifier::NullifierSecret};
use super::*;
#[test]
fn test_partial_tx_proof() {
let mut rng = seed_rng(0);
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 ptx_witness = PartialTxWitness {
inputs: vec![nmo_10, eth_23],
outputs: vec![crv_4840],
};
let ptx = PartialTx::from_witness(ptx_witness.clone());
let ptx_proof = ptx.prove(ptx_witness, vec![vec![], vec![]]).unwrap();
assert!(ptx.verify(&ptx_proof));
}
#[test]
fn test_partial_tx_balance() {
let mut rng = seed_rng(0);
let mut rng = rand::thread_rng();
let nmo_10 =
InputWitness::random(NoteWitness::new(10, "NMO", [0u8; 32], &mut rng), &mut rng);

13
cl/ledger/Cargo.toml Normal file
View File

@ -0,0 +1,13 @@
[package]
name = "ledger"
version = "0.1.0"
edition = "2021"
[dependencies]
cl = { path = "../cl" }
proof_statements = { path = "../proof_statements" }
nomos_cl_risc0_proofs = { path = "../risc0_proofs" }
risc0-zkvm = { version = "1.0", features = ["prove", "metal"] }
risc0-groth16 = { version = "1.0" }
rand = "0.8.5"
thiserror = "1.0.62"

9
cl/ledger/src/error.rs Normal file
View File

@ -0,0 +1,9 @@
use thiserror::Error;
pub type Result<T> = core::result::Result<T, Error>;
#[derive(Error, Debug)]
pub enum Error {
#[error("risc0 failed to serde")]
Risc0Serde(#[from] risc0_zkvm::serde::Error),
}

194
cl/ledger/src/input.rs Normal file
View File

@ -0,0 +1,194 @@
use proof_statements::nullifier::{NullifierPrivate, NullifierPublic};
use crate::error::Result;
const MAX_NOTE_COMMS: usize = 2usize.pow(8);
#[derive(Debug, Clone)]
pub struct InputNullifierProof {
receipt: risc0_zkvm::Receipt,
}
impl InputNullifierProof {
pub fn public(&self) -> Result<NullifierPublic> {
Ok(self.receipt.journal.decode()?)
}
pub fn verify(&self, expected_public_inputs: NullifierPublic) -> bool {
let Ok(public_inputs) = self.public() else {
return false;
};
public_inputs == expected_public_inputs
&& self
.receipt
.verify(nomos_cl_risc0_proofs::NULLIFIER_ID)
.is_ok()
}
}
pub fn prove_input_nullifier(
input: &cl::InputWitness,
note_commitments: &[cl::NoteCommitment],
) -> InputNullifierProof {
let output = input.to_output_witness();
let cm_leaves = note_commitment_leaves(note_commitments);
let output_cm = output.commit_note();
let cm_idx = note_commitments
.iter()
.position(|c| c == &output_cm)
.unwrap();
let cm_path = cl::merkle::path(cm_leaves, cm_idx);
let secrets = NullifierPrivate {
nf_sk: input.nf_sk,
output,
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::NULLIFIER_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;
InputNullifierProof { 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::<MAX_NOTE_COMMS>(&note_comm_bytes);
cm_leaves
}
#[cfg(test)]
mod test {
use proof_statements::nullifier::NullifierPublic;
use rand::thread_rng;
use super::{note_commitment_leaves, prove_input_nullifier};
#[test]
fn test_input_nullifier_prover() {
let mut rng = thread_rng();
let input = cl::InputWitness {
note: cl::NoteWitness {
balance: cl::BalanceWitness::random(32, "NMO", &mut rng),
death_constraint: vec![],
state: [0u8; 32],
},
nf_sk: cl::NullifierSecret::random(&mut rng),
nonce: cl::NullifierNonce::random(&mut rng),
};
let notes = vec![input.to_output_witness().commit_note()];
let proof = prove_input_nullifier(&input, &notes);
let expected_public_inputs = NullifierPublic {
cm_root: cl::merkle::root(note_commitment_leaves(&notes)),
nf: input.commit().nullifier,
};
assert!(proof.verify(expected_public_inputs));
let wrong_public_inputs = NullifierPublic {
cm_root: cl::merkle::root(note_commitment_leaves(&notes)),
nf: cl::Nullifier::new(
cl::NullifierSecret::random(&mut rng),
cl::NullifierNonce::random(&mut rng),
),
};
assert!(!proof.verify(wrong_public_inputs));
}
// ----- 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();
// 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 witness = cl::InputWitness { note, nf_sk, nonce };
// let input = witness.commit();
// let ptx_root = cl::PtxRoot::random(&mut rng);
// let proof = input.prove(&witness, ptx_root, vec![]).unwrap();
// assert!(input.verify(ptx_root, &proof));
// // The same input proof can not be used in another partial transaction.
// let another_ptx_root = cl::PtxRoot::random(&mut rng);
// assert!(!input.verify(another_ptx_root, &proof));
// }
}

2
cl/ledger/src/lib.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod error;
pub mod input;

View File

@ -0,0 +1,8 @@
[package]
name = "proof_statements"
version = "0.1.0"
edition = "2021"
[dependencies]
cl = { path = "../cl" }
serde = { version = "1.0", features = ["derive"] }

View File

@ -0,0 +1 @@
pub mod nullifier;

View File

@ -0,0 +1,22 @@
use serde::{Deserialize, Serialize};
/// for public input `nf` (nullifier) and `root_cm` (root of merkle tree over commitment set).
/// the prover has knowledge of `output = (note, nf_pk, nonce)`, `nf` and `path` s.t. that the following constraints hold
/// 0. nf_pk = hash(nf_sk)
/// 1. nf = hash(nonce||nf_sk)
/// 2. note_cm = output_commitment(output)
/// 3. verify_merkle_path(note_cm, root, path)
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NullifierPublic {
pub cm_root: [u8; 32],
pub nf: cl::Nullifier,
// TODO: we need a way to link this statement to a particular input. i.e. prove that the nullifier is actually derived from the input note.
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NullifierPrivate {
pub nf_sk: cl::NullifierSecret,
pub output: cl::OutputWitness,
pub cm_path: Vec<cl::merkle::PathNode>,
}

View File

@ -0,0 +1,11 @@
[package]
name = "nomos_cl_risc0_proofs"
version = "0.1.0"
edition = "2021"
[build-dependencies]
risc0-build = { version = "1.0" }
[package.metadata.risc0]
methods = ["nullifier"]

3
cl/risc0_proofs/build.rs Normal file
View File

@ -0,0 +1,3 @@
fn main() {
risc0_build::embed_methods();
}

View File

@ -8,13 +8,12 @@ edition = "2021"
[dependencies]
risc0-zkvm = { version = "1.0", default-features = false, features = ['std'] }
serde = { version = "1.0", features = ["derive"] }
bincode = "1"
cl = { path = "../../../cl" }
cl = { path = "../../cl" }
proof_statements = { path = "../../proof_statements" }
[patch.crates-io]
# Placing these patch statement in the workspace Cargo.toml will add RISC Zero SHA-256 and bigint
# multiplication accelerator support for all downstream usages of the following crates.
# 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" }
# k256 = { git = "https://github.com/risc0/RustCrypto-elliptic-curves", tag = "k256/v0.13.3-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" }

View File

@ -0,0 +1,28 @@
/// Nullifier Proof
///
/// Our goal: prove the nullifier nf was derived from a note that had previously been committed to.
///
/// More formally, nullifier statement says:
/// for public input `nf` (nullifier) and `root_cm` (root of merkle tree over commitment set).
/// the prover has knowledge of `output = (note, nf_pk, nonce)`, `nf` and `path` s.t. that the following constraints hold
/// 0. nf_pk = hash(nf_sk)
/// 1. nf = hash(nonce||nf_sk)
/// 2. note_cm = output_commitment(output)
/// 3. verify_merkle_path(note_cm, root, path)
use cl::merkle;
use cl::nullifier::Nullifier;
use proof_statements::nullifier::{NullifierPrivate, NullifierPublic};
use risc0_zkvm::guest::env;
fn main() {
let secret: NullifierPrivate = env::read();
assert_eq!(secret.output.nf_pk, secret.nf_sk.commit());
let cm_out = secret.output.commit_note();
let cm_leaf = merkle::leaf(cm_out.as_bytes());
let cm_root = merkle::path_root(cm_leaf, &secret.cm_path);
let nf = Nullifier::new(secret.nf_sk, secret.output.nonce);
env::commit(&NullifierPublic { cm_root, nf });
}

View File

@ -0,0 +1 @@
include!(concat!(env!("OUT_DIR"), "/methods.rs"));

View File

@ -1,188 +0,0 @@
/// This module defines the partial transaction structure.
///
/// Partial transactions, as the name suggests, are transactions
/// which on their own may not balance (i.e. \sum inputs != \sum outputs)
use crate::{
balance::Balance,
error::Error,
note::{NoteCommitment, NoteWitness},
nullifier::{Nullifier, NullifierNonce, NullifierSecret},
partial_tx::PtxRoot,
};
use rand_core::RngCore;
// use risc0_groth16::{PublicInputsJson, Verifier};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Input {
pub note_comm: NoteCommitment,
pub nullifier: Nullifier,
pub balance: Balance,
}
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
pub struct InputWitness {
pub note: NoteWitness,
pub nf_sk: NullifierSecret,
pub nonce: NullifierNonce,
}
impl InputWitness {
pub fn random(note: NoteWitness, mut rng: impl RngCore) -> Self {
Self {
note,
nf_sk: NullifierSecret::random(&mut rng),
nonce: NullifierNonce::random(&mut rng),
}
}
pub fn commit(&self) -> Input {
Input {
note_comm: self.note.commit(self.nf_sk.commit(), self.nonce),
nullifier: Nullifier::new(self.nf_sk, self.nonce),
balance: self.note.balance(),
}
}
}
// as we don't have SNARKS hooked up yet, the witness will be our proof
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct InputProof {
input: InputWitness,
ptx_root: PtxRoot,
death_proof: Vec<u8>,
}
impl Input {
pub fn prove(
&self,
w: &InputWitness,
ptx_root: PtxRoot,
death_proof: Vec<u8>,
) -> Result<InputProof, Error> {
if bincode::serialize(&w.commit()).unwrap() != bincode::serialize(&self).unwrap() {
Err(Error::ProofFailed)
} else {
Ok(InputProof {
input: w.clone(),
ptx_root,
death_proof,
})
}
}
pub fn verify(&self, ptx_root: PtxRoot, proof: &InputProof) -> bool {
// verification checks the relation
// - nf_pk == hash(nf_sk)
// - note_comm == commit(note || nf_pk)
// - nullifier == hash(nf_sk || nonce)
// - balance == v * hash_to_curve(Unit) + blinding * H
// - ptx_root is the same one that was used in proving.
let witness = &proof.input;
let nf_pk = witness.nf_sk.commit();
// let death_constraint_was_committed_to =
// witness.note.death_constraint == bincode::serialize(&death_constraint).unwrap();
// let death_constraint_is_satisfied: bool = Verifier::from_json(
// bincode::deserialize(&proof.death_proof).unwrap(),
// PublicInputsJson {
// values: vec![ptx_root.hex()],
// },
// bincode::deserialize(&witness.note.death_constraint).unwrap(),
// )
// .unwrap()
// .verify()
// .is_ok();
let death_constraint_is_satisfied = true;
self.note_comm == witness.note.commit(nf_pk, witness.nonce)
&& self.nullifier == Nullifier::new(witness.nf_sk, witness.nonce)
&& self.balance == witness.note.balance()
&& ptx_root == proof.ptx_root
// && death_constraint_was_committed_to
&& death_constraint_is_satisfied
}
pub fn to_bytes(&self) -> [u8; 96] {
let mut bytes = [0u8; 96];
bytes[..32].copy_from_slice(self.note_comm.as_bytes());
bytes[32..64].copy_from_slice(self.nullifier.as_bytes());
bytes[64..96].copy_from_slice(&self.balance.to_bytes());
bytes
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::{nullifier::NullifierNonce, test_util::seed_rng};
#[test]
fn test_input_proof() {
let mut rng = seed_rng(0);
let ptx_root = PtxRoot::default();
let note = NoteWitness::new(10, "NMO", [0u8; 32], &mut rng);
let nf_sk = NullifierSecret::random(&mut rng);
let nonce = NullifierNonce::random(&mut rng);
let input_witness = 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 = [
InputWitness {
note: NoteWitness::new(11, "NMO", [0u8; 32], &mut rng),
..input_witness.clone()
},
InputWitness {
note: NoteWitness::new(10, "ETH", [0u8; 32], &mut rng),
..input_witness.clone()
},
InputWitness {
nf_sk: NullifierSecret::random(&mut rng),
..input_witness.clone()
},
InputWitness {
nonce: 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 = seed_rng(0);
let note = NoteWitness::new(10, "NMO", [0u8; 32], &mut rng);
let nf_sk = NullifierSecret::random(&mut rng);
let nonce = NullifierNonce::random(&mut rng);
let witness = InputWitness { note, nf_sk, nonce };
let input = witness.commit();
let ptx_root = PtxRoot::random(&mut rng);
let proof = input.prove(&witness, ptx_root, vec![]).unwrap();
assert!(input.verify(ptx_root, &proof));
// The same input proof can not be used in another partial transaction.
let another_ptx_root = PtxRoot::random(&mut rng);
assert!(!input.verify(another_ptx_root, &proof));
}
}

View File

@ -1,7 +0,0 @@
use rand_core::SeedableRng;
pub fn seed_rng(seed: u64) -> impl rand_core::RngCore {
let mut bytes = [0u8; 32];
bytes[..8].copy_from_slice(&seed.to_le_bytes());
rand_chacha::ChaCha12Rng::from_seed(bytes)
}

View File

@ -6,8 +6,8 @@ default-run = "host"
[dependencies]
methods = { path = "../methods" }
risc0-zkvm = { version = "1.0.1", features = ["prove"] }
risc0-groth16 = { version = "1.0.1" }
risc0-zkvm = { version = "1.0", features = ["prove", "metal"] }
risc0-groth16 = { version = "1.0" }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
serde = "1.0"
blake2 = "0.10"
@ -16,4 +16,4 @@ common = { path = "../common" }
tempfile = "3"
clap = { version = "4", features = ["derive"] }
rand = "0.8.5"
cl = { path = "../../cl" }
cl = { path = "../../cl/cl" }

View File

@ -10,7 +10,6 @@ use clap::Parser;
#[command(version, about, long_about = None)]
enum Action {
Stf,
Nullifier,
}
fn stf_prove_stark() {
@ -101,65 +100,6 @@ fn stf_prove_stark() {
receipt.verify(methods::METHOD_ID).unwrap();
}
fn nf_prove_stark() {
let mut rng = rand::thread_rng();
let nf_sk = cl::NullifierSecret::random(&mut rng);
let output = cl::OutputWitness {
note: cl::NoteWitness {
balance: cl::BalanceWitness::random(10, "NMO", &mut rng),
death_constraint: vec![],
state: [0u8; 32],
},
nf_pk: nf_sk.commit(),
nonce: cl::NullifierNonce::random(&mut rng),
};
let output_cm = output.commit_note().as_bytes().to_vec();
let cm_set = cl::merkle::padded_leaves::<64>(&[output_cm]);
let cm_root = cl::merkle::root(cm_set);
let cm_path = cl::merkle::path(cm_set, 0);
let nf = cl::Nullifier::new(nf_sk, output.nonce);
let env = ExecutorEnv::builder()
.write(&cm_root)
.unwrap()
.write(&nf)
.unwrap()
.write(&nf_sk)
.unwrap()
.write(&output)
.unwrap()
.write(&cm_path)
.unwrap()
.build()
.unwrap();
// Obtain the default prover.
let prover = 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, methods::NULLIFIER_ELF, &opts)
.unwrap();
println!("STARK prover time: {:.2?}", start_t.elapsed());
// extract the receipt.
let receipt = prove_info.receipt;
// TODO: Implement code for retrieving receipt journal here.
std::fs::write("proof.stark", bincode::serialize(&receipt).unwrap()).unwrap();
// The receipt was verified at the end of proving, but the below code is an
// example of how someone else could verify this receipt.
receipt.verify(methods::NULLIFIER_ID).unwrap();
}
fn main() {
// Initialize tracing. In order to view logs, run `RUST_LOG=info cargo run`
tracing_subscriber::fmt()
@ -170,7 +110,6 @@ fn main() {
match action {
Action::Stf => stf_prove_stark(),
Action::Nullifier => nf_prove_stark(),
}
}

View File

@ -7,4 +7,4 @@ edition = "2021"
risc0-build = { version = "1.0" }
[package.metadata.risc0]
methods = ["guest", "nullifier"]
methods = ["guest"]

View File

@ -13,7 +13,7 @@ blake2 = "0.10"
serde = { version = "1.0", features = ["derive"] }
bincode = "1"
common = { path = "../../common" }
cl = { path = "../../../cl" }
cl = { path = "../../../cl/cl" }
[patch.crates-io]
# Placing these patch statement in the workspace Cargo.toml will add RISC Zero SHA-256 and bigint

View File

@ -1,9 +1,9 @@
use blake2::{Blake2s256, Digest};
use risc0_zkvm::guest::env;
use common::*;
use cl::merkle;
use cl::input::InputWitness;
use cl::merkle;
use cl::output::OutputWitness;
use common::*;
use risc0_zkvm::guest::env;
/// Public Inputs:
/// * ptx_root: the root of the partial tx merkle tree of inputs/outputs
@ -31,8 +31,11 @@ fn execute(
// a transfer is included as part of the same transaction in the cl
let in_comm = in_note.commit().to_bytes();
eprintln!("input comm: {}", env::cycle_count());
assert!(merkle::verify_path(merkle::leaf(&in_comm), &in_ptx_path, input_root));
assert_eq!(
merkle::path_root(merkle::leaf(&in_comm), &in_ptx_path),
input_root
);
eprintln!("input merkle path: {}", env::cycle_count());
// check the commitments match the actual data
@ -61,8 +64,11 @@ fn execute(
// (this is done in the death condition to disallow burning)
let out_comm = out_note.commit().to_bytes();
eprintln!("output comm: {}", env::cycle_count());
assert!(merkle::verify_path(merkle::leaf(&out_comm), &out_ptx_path, output_root));
assert_eq!(
merkle::path_root(merkle::leaf(&out_comm), &out_ptx_path),
output_root
);
eprintln!("out merkle proof: {}", env::cycle_count());
}
@ -81,8 +87,19 @@ fn main() {
let state: State = env::read();
let journal: Journal = env::read();
eprintln!("parse input: {}", env::cycle_count());
execute(ptx_root, input_root, output_root, in_ptx_path, out_ptx_path, in_note, out_note, input, state, journal);
eprintln!("parse input: {}", env::cycle_count());
execute(
ptx_root,
input_root,
output_root,
in_ptx_path,
out_ptx_path,
in_note,
out_note,
input,
state,
journal,
);
}
fn calculate_state_hash(state: &State) -> [u8; 32] {

View File

@ -1,57 +0,0 @@
/// Nullifier Proof
///
/// Our goal: prove the nullifier nf was derived from a note that had previously been committed to.
///
/// More formally, nullifier statement says:
/// for public input `nf` (nullifier) and `root_cm` (root of merkle tree over commitment set).
/// the prover has knowledge of `output = (note, nf_pk, nonce)`, `nf` and `path` s.t. that the following constraints hold
/// 0. nf_pk = hash(nf_sk)
/// 1. nf = hash(nonce||nf_sk)
/// 2. note_cm = output_commitment(output)
/// 3. verify_merkle_path(note_cm, root, path)
use cl::merkle;
use cl::nullifier::{Nullifier, NullifierSecret};
use cl::output::OutputWitness;
use risc0_zkvm::guest::env;
fn execute(
// public
cm_root: [u8; 32],
nf: Nullifier,
// private
nf_sk: NullifierSecret,
output: OutputWitness,
cm_path: Vec<merkle::PathNode>,
) {
eprintln!("start exec: {}", env::cycle_count());
assert_eq!(output.nf_pk, nf_sk.commit());
eprintln!("output nullifier: {}", env::cycle_count());
assert_eq!(nf, Nullifier::new(nf_sk, output.nonce));
eprintln!("nullifier: {}", env::cycle_count());
let cm_out = output.commit_note();
eprintln!("out_cm: {}", env::cycle_count());
assert!(merkle::verify_path(
merkle::leaf(cm_out.as_bytes()),
&cm_path,
cm_root
));
eprintln!("nullifier merkle path: {}", env::cycle_count());
}
fn main() {
// public input
let cm_root: [u8; 32] = env::read();
let nf: Nullifier = env::read();
// private input
let nf_sk: NullifierSecret = env::read();
let output: OutputWitness = env::read();
let cm_path: Vec<merkle::PathNode> = env::read();
eprintln!("parse input: {}", env::cycle_count());
execute(cm_root, nf, nf_sk, output, cm_path);
}