Merge pull request #11 from logos-co/drusu/cl/add-death-constraints

cl: add death constraints to simple transfer scenario
This commit is contained in:
davidrusu 2024-07-29 15:37:43 +04:00 committed by GitHub
commit 94bef73ba4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 161 additions and 23 deletions

View File

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

View File

@ -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,

View File

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

View File

@ -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<DeathConstraintPublic> {
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 'death-nop' 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,
}
}
}

View File

@ -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,
@ -44,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
);

View File

@ -1,5 +1,5 @@
// pub mod death_constraint;
pub mod bundle;
pub mod death_constraint;
pub mod error;
pub mod input;
pub mod output;

View File

@ -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 valid
&& self.death.verify(DeathConstraintPublic { nf, ptx_root }) // verify the death constraint was satisfied
}
}
pub struct ProvedPartialTx {
pub inputs: Vec<ProvedInput>,
pub inputs: Vec<PartialTxInput>,
pub outputs: Vec<ProvedOutput>,
}
impl ProvedPartialTx {
pub fn prove(
ptx: &cl::PartialTxWitness,
mut death_proofs: BTreeMap<cl::Nullifier, DeathProof>,
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()
}
}

View File

@ -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, &note_commitments);
let proved_ptx = ProvedPartialTx::prove(&ptx_witness, death_proofs, &note_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);

View File

@ -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,