Merge pull request #7 from logos-co/drusu/cl/simple-transfer

CL PoC: simple transfer
This commit is contained in:
davidrusu 2024-07-24 13:30:38 +04:00 committed by GitHub
commit bb3840ab14
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 545 additions and 189 deletions

View File

@ -17,6 +17,16 @@ pub struct Balance(pub RistrettoPoint);
pub struct BalanceWitness(pub Scalar); pub struct BalanceWitness(pub Scalar);
impl Balance { 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] { pub fn to_bytes(&self) -> [u8; 32] {
self.0.compress().to_bytes() self.0.compress().to_bytes()
} }

View File

@ -1,8 +1,6 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use curve25519_dalek::{constants::RISTRETTO_BASEPOINT_POINT, ristretto::RistrettoPoint}; use crate::{partial_tx::PartialTx, Balance, BalanceWitness};
use crate::{partial_tx::PartialTx, BalanceWitness};
/// The transaction bundle is a collection of partial transactions. /// The transaction bundle is a collection of partial transactions.
/// The goal in bundling transactions is to produce a set 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<PartialTx>, pub partials: Vec<PartialTx>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct BundleWitness { pub struct BundleWitness {
pub balance_blinding: BalanceWitness, pub balance_blinding: BalanceWitness,
} }
impl Bundle { impl Bundle {
pub fn balance(&self) -> RistrettoPoint { pub fn balance(&self) -> Balance {
self.partials.iter().map(|ptx| ptx.balance()).sum() Balance(self.partials.iter().map(|ptx| ptx.balance().0).sum())
} }
pub fn is_balanced(&self, witness: BalanceWitness) -> bool { 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)] #[cfg(test)]
mod test { mod test {
use crate::{ use crate::{
crypto::hash_to_curve, input::InputWitness, note::NoteWitness, nullifier::NullifierSecret, input::InputWitness, note::NoteWitness, nullifier::NullifierSecret, output::OutputWitness,
output::OutputWitness, partial_tx::PartialTxWitness, partial_tx::PartialTxWitness,
}; };
use super::*; use super::*;
@ -57,8 +55,8 @@ mod test {
OutputWitness::random(NoteWitness::basic(4840, "CRV"), nf_c.commit(), &mut rng); OutputWitness::random(NoteWitness::basic(4840, "CRV"), nf_c.commit(), &mut rng);
let ptx_unbalanced = PartialTxWitness { let ptx_unbalanced = PartialTxWitness {
inputs: vec![nmo_10_in.clone(), eth_23_in.clone()], inputs: vec![nmo_10_in, eth_23_in],
outputs: vec![crv_4840_out.clone()], outputs: vec![crv_4840_out],
}; };
let bundle_witness = BundleWitness { let bundle_witness = BundleWitness {
@ -70,22 +68,14 @@ mod test {
}; };
let mut bundle = Bundle { 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!(!bundle.is_balanced(bundle_witness.balance_blinding));
assert_eq!( assert_eq!(
bundle.balance(), bundle.balance().0,
crate::balance::balance(4840, hash_to_curve(b"CRV"), crv_4840_out.balance_blinding.0) crv_4840_out.commit().balance.0
- (crate::balance::balance( - (nmo_10_in.commit().balance.0 + eth_23_in.commit().balance.0)
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
))
); );
let crv_4840_in = InputWitness::random(crv_4840_out, nf_c, &mut rng); let crv_4840_in = InputWitness::random(crv_4840_out, nf_c, &mut rng);
@ -100,12 +90,13 @@ mod test {
&mut rng, &mut rng,
); );
bundle bundle.partials.push(
.partials PartialTxWitness {
.push(PartialTx::from_witness(PartialTxWitness { inputs: vec![crv_4840_in],
inputs: vec![crv_4840_in.clone()], outputs: vec![nmo_10_out, eth_23_out],
outputs: vec![nmo_10_out.clone(), eth_23_out.clone()], }
})); .commit(),
);
let witness = BundleWitness { let witness = BundleWitness {
balance_blinding: BalanceWitness::new( 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)); assert!(bundle.is_balanced(witness.balance_blinding));
} }
} }

View File

@ -21,7 +21,6 @@ pub struct Input {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct InputWitness { pub struct InputWitness {
pub note: NoteWitness, pub note: NoteWitness,
pub utxo_balance_blinding: BalanceWitness,
pub balance_blinding: BalanceWitness, pub balance_blinding: BalanceWitness,
pub nf_sk: NullifierSecret, pub nf_sk: NullifierSecret,
pub nonce: NullifierNonce, pub nonce: NullifierNonce,
@ -36,7 +35,6 @@ impl InputWitness {
assert_eq!(nf_sk.commit(), output.nf_pk); assert_eq!(nf_sk.commit(), output.nf_pk);
Self { Self {
note: output.note, note: output.note,
utxo_balance_blinding: output.balance_blinding,
balance_blinding: BalanceWitness::random(&mut rng), balance_blinding: BalanceWitness::random(&mut rng),
nf_sk, nf_sk,
nonce: output.nonce, nonce: output.nonce,
@ -55,13 +53,8 @@ impl InputWitness {
} }
} }
pub fn to_output(&self) -> crate::OutputWitness { pub fn note_commitment(&self) -> crate::NoteCommitment {
crate::OutputWitness { self.note.commit(self.nf_sk.commit(), self.nonce)
note: self.note,
balance_blinding: self.utxo_balance_blinding,
nf_pk: self.nf_sk.commit(),
nonce: self.nonce,
}
} }
} }

View File

@ -40,22 +40,26 @@ pub struct NoteWitness {
} }
impl NoteWitness { impl NoteWitness {
pub fn new(value: u64, unit: impl Into<String>, state: [u8; 32]) -> Self { pub fn new(
value: u64,
unit: impl Into<String>,
death_constraint: [u8; 32],
state: [u8; 32],
) -> Self {
Self { Self {
value, value,
unit: unit_point(&unit.into()), unit: unit_point(&unit.into()),
death_constraint: [0u8; 32], death_constraint,
state, state,
} }
} }
pub fn basic(value: u64, unit: impl Into<String>) -> Self { pub fn basic(value: u64, unit: impl Into<String>) -> Self {
Self { Self::new(value, unit, [0u8; 32], [0u8; 32])
value, }
unit: unit_point(&unit.into()),
death_constraint: [0u8; 32], pub fn stateless(value: u64, unit: impl Into<String>, death_constraint: [u8; 32]) -> Self {
state: [0u8; 32], Self::new(value, unit, death_constraint, [0u8; 32])
}
} }
pub fn commit(&self, nf_pk: NullifierCommitment, nonce: NullifierNonce) -> NoteCommitment { 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_pk = NullifierSecret::random(&mut rng).commit();
let nf_nonce = NullifierNonce::random(&mut rng); 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 // different notes under same nullifier produce different commitments
let mutation_tests = [ let mutation_tests = [

View File

@ -107,23 +107,23 @@ mod test {
let wrong_witnesses = [ let wrong_witnesses = [
OutputWitness { OutputWitness {
note: NoteWitness::basic(11, "NMO"), note: NoteWitness::basic(11, "NMO"),
..witness.clone() ..witness
}, },
OutputWitness { OutputWitness {
note: NoteWitness::basic(10, "ETH"), note: NoteWitness::basic(10, "ETH"),
..witness.clone() ..witness
}, },
OutputWitness { OutputWitness {
balance_blinding: BalanceWitness::random(&mut rng), balance_blinding: BalanceWitness::random(&mut rng),
..witness.clone() ..witness
}, },
OutputWitness { OutputWitness {
nf_pk: NullifierSecret::random(&mut rng).commit(), nf_pk: NullifierSecret::random(&mut rng).commit(),
..witness.clone() ..witness
}, },
OutputWitness { OutputWitness {
nonce: NullifierNonce::random(&mut rng), nonce: NullifierNonce::random(&mut rng),
..witness.clone() ..witness
}, },
]; ];

View File

@ -1,7 +1,9 @@
use curve25519_dalek::ristretto::RistrettoPoint; use curve25519_dalek::ristretto::RistrettoPoint;
use curve25519_dalek::Scalar;
use rand_core::RngCore; use rand_core::RngCore;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::balance::{Balance, BalanceWitness};
use crate::input::{Input, InputWitness}; use crate::input::{Input, InputWitness};
use crate::merkle; use crate::merkle;
use crate::output::{Output, OutputWitness}; use crate::output::{Output, OutputWitness};
@ -44,14 +46,23 @@ pub struct PartialTxWitness {
pub outputs: Vec<OutputWitness>, pub outputs: Vec<OutputWitness>,
} }
impl PartialTx { impl PartialTxWitness {
pub fn from_witness(w: PartialTxWitness) -> Self { pub fn commit(&self) -> PartialTx {
Self { PartialTx {
inputs: Vec::from_iter(w.inputs.iter().map(InputWitness::commit)), inputs: Vec::from_iter(self.inputs.iter().map(InputWitness::commit)),
outputs: Vec::from_iter(w.outputs.iter().map(OutputWitness::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] { pub fn input_root(&self) -> [u8; 32] {
let input_bytes = let input_bytes =
Vec::from_iter(self.inputs.iter().map(Input::to_bytes).map(Vec::from_iter)); Vec::from_iter(self.inputs.iter().map(Input::to_bytes).map(Vec::from_iter));
@ -95,18 +106,18 @@ impl PartialTx {
PtxRoot(root) 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 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(); let out_sum: RistrettoPoint = self.outputs.iter().map(|o| o.balance.0).sum();
out_sum - in_sum Balance(out_sum - in_sum)
} }
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::{crypto::hash_to_curve, note::NoteWitness, nullifier::NullifierSecret}; use crate::{note::NoteWitness, nullifier::NullifierSecret};
use super::*; use super::*;
@ -130,21 +141,15 @@ mod test {
OutputWitness::random(NoteWitness::basic(4840, "CRV"), nf_c.commit(), &mut rng); OutputWitness::random(NoteWitness::basic(4840, "CRV"), nf_c.commit(), &mut rng);
let ptx_witness = PartialTxWitness { let ptx_witness = PartialTxWitness {
inputs: vec![nmo_10.clone(), eth_23.clone()], inputs: vec![nmo_10, eth_23],
outputs: vec![crv_4840.clone()], outputs: vec![crv_4840],
}; };
let ptx = PartialTx::from_witness(ptx_witness.clone()); let ptx = ptx_witness.commit();
assert_eq!( assert_eq!(
ptx.balance(), ptx.balance().0,
crate::balance::balance(4840, hash_to_curve(b"CRV"), crv_4840.balance_blinding.0) crv_4840.commit().balance.0 - (nmo_10.commit().balance.0 + eth_23.commit().balance.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
))
); );
} }
} }

View File

@ -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()))
}

View File

@ -10,4 +10,5 @@ nomos_cl_risc0_proofs = { path = "../risc0_proofs" }
risc0-zkvm = { version = "1.0", features = ["prove", "metal"] } risc0-zkvm = { version = "1.0", features = ["prove", "metal"] }
risc0-groth16 = { version = "1.0" } risc0-groth16 = { version = "1.0" }
rand = "0.8.5" rand = "0.8.5"
rand_core = "0.6.0"
thiserror = "1.0.62" thiserror = "1.0.62"

View File

@ -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<cl::Balance> {
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()
}
}

View File

@ -4,67 +4,79 @@ use crate::error::Result;
const MAX_NOTE_COMMS: usize = 2usize.pow(8); const MAX_NOTE_COMMS: usize = 2usize.pow(8);
#[derive(Debug, Clone)] pub struct ProvedInput {
pub struct InputProof { pub input: InputPublic,
receipt: risc0_zkvm::Receipt, pub risc0_receipt: risc0_zkvm::Receipt,
} }
impl InputProof { impl ProvedInput {
pub fn public(&self) -> Result<InputPublic> { pub fn prove(input: &cl::InputWitness, note_commitments: &[cl::NoteCommitment]) -> Self {
Ok(self.receipt.journal.decode()?) 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 { pub fn public(&self) -> Result<InputPublic> {
let Ok(public_inputs) = self.public() else { Ok(self.risc0_receipt.journal.decode()?)
}
pub fn verify(&self) -> bool {
let Ok(proved_public_inputs) = self.public() else {
return false; return false;
}; };
&public_inputs == expected_public_inputs self.input == proved_public_inputs
&& self.receipt.verify(nomos_cl_risc0_proofs::INPUT_ID).is_ok() && 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] { 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 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); let cm_leaves = cl::merkle::padded_leaves::<MAX_NOTE_COMMS>(&note_comm_bytes);
@ -78,27 +90,27 @@ mod test {
use super::*; use super::*;
#[test] #[test]
fn test_input_nullifier_prover() { fn test_input_prover() {
let mut rng = thread_rng(); let mut rng = thread_rng();
let input = cl::InputWitness { let input = cl::InputWitness {
note: cl::NoteWitness::basic(32, "NMO"), note: cl::NoteWitness::basic(32, "NMO"),
utxo_balance_blinding: cl::BalanceWitness::random(&mut rng),
balance_blinding: cl::BalanceWitness::random(&mut rng), balance_blinding: cl::BalanceWitness::random(&mut rng),
nf_sk: cl::NullifierSecret::random(&mut rng), nf_sk: cl::NullifierSecret::random(&mut rng),
nonce: cl::NullifierNonce::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, &notes); let mut proved_input = ProvedInput::prove(&input, &notes);
let expected_public_inputs = InputPublic { let expected_public_inputs = InputPublic {
cm_root: cl::merkle::root(note_commitment_leaves(&notes)), cm_root: cl::merkle::root(note_commitment_leaves(&notes)),
input: input.commit(), 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 = [ let wrong_public_inputs = [
InputPublic { InputPublic {
@ -133,57 +145,12 @@ mod test {
]; ];
for wrong_input in wrong_public_inputs { 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. ----- // ----- 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] // #[test]
// fn test_input_ptx_coupling() { // fn test_input_ptx_coupling() {
// let mut rng = rand::thread_rng(); // let mut rng = rand::thread_rng();

View File

@ -1,2 +1,6 @@
// pub mod death_constraint;
pub mod bundle;
pub mod error; pub mod error;
pub mod input; pub mod input;
pub mod output;
pub mod partial_tx;

View File

@ -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<cl::Output> {
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());
}
}
}

View File

@ -0,0 +1,26 @@
use crate::{input::ProvedInput, output::ProvedOutput};
pub struct ProvedPartialTx {
pub inputs: Vec<ProvedInput>,
pub outputs: Vec<ProvedOutput>,
}
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)
}
}

View File

@ -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, &note_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.
}

View File

@ -3,7 +3,6 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeathConstraintPublic { pub struct DeathConstraintPublic {
pub cm_root: [u8; 32],
pub nf: Nullifier, pub nf: Nullifier,
pub ptx_root: PtxRoot, pub ptx_root: PtxRoot,
} }

View File

@ -1,5 +1,6 @@
use cl::{merkle, InputWitness, OutputWitness, PtxRoot}; use cl::{merkle, InputWitness, OutputWitness, PtxRoot};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// An input to a partial transaction /// An input to a partial transaction
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PartialTxInputPrivate { pub struct PartialTxInputPrivate {
@ -15,7 +16,7 @@ impl PartialTxInputPrivate {
} }
pub fn cm_root(&self) -> [u8; 32] { 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) merkle::path_root(leaf, &self.cm_path)
} }
} }

View File

@ -7,5 +7,5 @@ edition = "2021"
risc0-build = { version = "1.0" } risc0-build = { version = "1.0" }
[package.metadata.risc0] [package.metadata.risc0]
methods = ["input"] methods = ["input", "output", "bundle", "death_constraint_nop"]

View File

@ -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" }

View File

@ -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);
}

View File

@ -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" }

View File

@ -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);
}

View File

@ -6,7 +6,7 @@ use risc0_zkvm::guest::env;
fn main() { fn main() {
let secret: InputPrivate = env::read(); 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_leaf = merkle::leaf(out_cm.as_bytes());
let cm_root = merkle::path_root(cm_leaf, &secret.cm_path); let cm_root = merkle::path_root(cm_leaf, &secret.cm_path);

View File

@ -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" }

View File

@ -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);
}

View File

@ -18,7 +18,6 @@ fn main() {
spend_event_state_path, spend_event_state_path,
} = env::read(); } = env::read();
let cm_root = in_zone_funds.cm_root();
let ptx_root = in_zone_funds.ptx_root(); let ptx_root = in_zone_funds.ptx_root();
let nf = Nullifier::new(in_zone_funds.input.nf_sk, in_zone_funds.input.nonce); 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 // 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); assert_eq!(spent_note.output.nf_pk, spend_event.to);
env::commit(&DeathConstraintPublic { env::commit(&DeathConstraintPublic {
cm_root,
ptx_root, ptx_root,
nf, nf,
}); });