cl: integrate groth16 death constraint validation

This commit is contained in:
David Rusu 2024-06-19 18:49:21 +02:00
parent 993ecf13b5
commit 96482b219a
8 changed files with 161 additions and 46 deletions

View File

@ -6,6 +6,9 @@ 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"

View File

@ -2,14 +2,15 @@ use group::{ff::Field, GroupEncoding};
use jubjub::{Scalar, SubgroupPoint};
use lazy_static::lazy_static;
use rand_core::RngCore;
use serde::{Deserialize, Serialize};
lazy_static! {
static ref PEDERSON_COMMITMENT_BLINDING_POINT: SubgroupPoint =
crate::crypto::hash_to_curve(b"NOMOS_CL_PEDERSON_COMMITMENT_BLINDING");
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Balance(pub SubgroupPoint);
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
pub struct Balance(#[serde(with = "serde_point")] pub SubgroupPoint);
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct BalanceWitness {
@ -55,6 +56,51 @@ pub fn balance(value: u64, unit: &str, blinding: Scalar) -> SubgroupPoint {
unit_point(unit) * value_scalar + *PEDERSON_COMMITMENT_BLINDING_POINT * blinding
}
mod serde_point {
use super::SubgroupPoint;
use group::GroupEncoding;
use serde::de::{self, Visitor};
use serde::{Deserializer, Serializer};
use std::fmt;
// Serialize a SubgroupPoint by converting it to bytes.
pub fn serialize<S>(point: &SubgroupPoint, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let bytes = point.to_bytes();
serializer.serialize_bytes(&bytes)
}
// Deserialize a SubgroupPoint by converting it from bytes.
pub fn deserialize<'de, D>(deserializer: D) -> Result<SubgroupPoint, D::Error>
where
D: Deserializer<'de>,
{
struct BytesVisitor;
impl<'de> Visitor<'de> for BytesVisitor {
type Value = SubgroupPoint;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a valid SubgroupPoint in byte representation")
}
fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
where
E: de::Error,
{
let mut bytes = <jubjub::SubgroupPoint as group::GroupEncoding>::Repr::default();
assert_eq!(bytes.len(), v.len());
bytes.copy_from_slice(v);
Ok(SubgroupPoint::from_bytes(&bytes).unwrap())
}
}
deserializer.deserialize_bytes(BytesVisitor)
}
}
#[cfg(test)]
mod test {

View File

@ -1,39 +1,35 @@
use std::collections::BTreeSet;
use jubjub::{Scalar, SubgroupPoint};
use serde::{Deserialize, Serialize};
use crate::{
error::Error,
note::NoteCommitment,
partial_tx::{PartialTx, PartialTxProof, PartialTxWitness},
partial_tx::{PartialTx, PartialTxProof},
};
/// The transaction bundle is a collection of partial transactions.
/// The goal in bundling transactions is to produce a set of partial transactions
/// that balance each other.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Bundle {
pub partials: Vec<PartialTx>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone)]
pub struct BundleWitness {
pub partials: Vec<PartialTxWitness>,
pub balance_blinding: Scalar,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug)]
pub struct BundleProof {
pub partials: Vec<PartialTxProof>,
pub balance_blinding: Scalar,
}
impl Bundle {
pub fn from_witness(w: BundleWitness) -> Self {
Self {
partials: Vec::from_iter(w.partials.into_iter().map(PartialTx::from_witness)),
}
}
pub fn balance(&self) -> SubgroupPoint {
self.partials.iter().map(|ptx| ptx.balance()).sum()
}
@ -42,11 +38,12 @@ impl Bundle {
self.balance() == crate::balance::balance(0, "", balance_blinding_witness)
}
pub fn prove(&self, w: BundleWitness) -> Result<BundleProof, Error> {
if &Self::from_witness(w.clone()) != self {
return Err(Error::ProofFailed);
}
if w.partials.len() == self.partials.len() {
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
@ -67,15 +64,13 @@ impl Bundle {
return Err(Error::ProofFailed);
}
let ptx_proofs = self
.partials
.iter()
.zip(w.partials)
.map(|(ptx, p_w)| ptx.prove(p_w))
.collect::<Result<Vec<PartialTxProof>, _>>()?;
if self.balance() != crate::balance::balance(0, "", w.balance_blinding) {
return Err(Error::ProofFailed);
}
Ok(BundleProof {
partials: ptx_proofs,
balance_blinding: w.balance_blinding,
})
}

View File

@ -10,15 +10,17 @@ use crate::{
partial_tx::PtxCommitment,
};
use rand_core::RngCore;
use risc0_groth16::{ProofJson, PublicInputsJson, Verifier};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Input {
pub note_comm: NoteCommitment,
pub nullifier: Nullifier,
pub balance: Balance,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone)]
pub struct InputWitness {
pub note: Note,
pub nf_sk: NullifierSecret,
@ -36,8 +38,19 @@ impl InputWitness {
}
// as we don't have SNARKS hooked up yet, the witness will be our proof
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InputProof(InputWitness, PtxCommitment);
#[derive(Debug)]
pub struct InputProof {
input: InputWitness,
ptx_comm: PtxCommitment,
death_proof: ProofJson,
}
impl InputProof {
fn clone_death_proof(&self) -> ProofJson {
let bytes = bincode::serialize(&self.death_proof).unwrap();
bincode::deserialize(&bytes).unwrap()
}
}
impl Input {
pub fn from_witness(w: InputWitness) -> Self {
@ -48,11 +61,22 @@ impl Input {
}
}
pub fn prove(&self, w: &InputWitness, ptx_comm: PtxCommitment) -> Result<InputProof, Error> {
if &Input::from_witness(w.clone()) != self {
pub fn prove(
&self,
w: &InputWitness,
ptx_comm: PtxCommitment,
death_proof: ProofJson,
) -> Result<InputProof, Error> {
if bincode::serialize(&Input::from_witness(w.clone())).unwrap()
!= bincode::serialize(&self).unwrap()
{
Err(Error::ProofFailed)
} else {
Ok(InputProof(w.clone(), ptx_comm))
Ok(InputProof {
input: w.clone(),
ptx_comm,
death_proof,
})
}
}
@ -64,13 +88,29 @@ impl Input {
// - balance == v * hash_to_curve(Unit) + blinding * H
// - ptx_comm is the same one that was used in proving.
let witness = &proof.0;
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(
proof.clone_death_proof(),
PublicInputsJson {
values: vec![ptx_comm.hex()],
},
bincode::deserialize(&witness.note.death_constraint).unwrap(),
)
.unwrap()
.verify()
.is_ok();
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_comm == proof.1
&& ptx_comm == proof.ptx_comm
// && death_constraint_was_committed_to
&& death_constraint_is_satisfied
}
pub(crate) fn to_bytes(&self) -> [u8; 96] {

View File

@ -1,13 +1,15 @@
use blake2::{Blake2s256, Digest};
use group::GroupEncoding;
use rand_core::RngCore;
use risc0_groth16::VerifyingKeyJson;
use serde::{Deserialize, Serialize};
use crate::{
balance::{Balance, BalanceWitness},
nullifier::{NullifierCommitment, NullifierNonce},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct NoteCommitment([u8; 32]);
impl NoteCommitment {
@ -18,24 +20,38 @@ impl NoteCommitment {
// TODO: Rename Note to NoteWitness and NoteCommitment to Note
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone)]
pub struct Note {
pub balance: BalanceWitness,
pub death_constraint: Vec<u8>, // serialized death_constraint
}
impl Note {
pub fn random(value: u64, unit: impl Into<String>, rng: impl RngCore) -> Self {
pub fn random(
value: u64,
unit: impl Into<String>,
death_constraint: &VerifyingKeyJson,
rng: impl RngCore,
) -> Self {
Self {
balance: BalanceWitness::random(value, unit, rng),
death_constraint: bincode::serialize(death_constraint).unwrap(),
}
}
pub fn commit(&self, nf_pk: NullifierCommitment, nonce: NullifierNonce) -> NoteCommitment {
let mut hasher = Blake2s256::new();
hasher.update(b"NOMOS_CL_NOTE_COMMIT");
// COMMIT TO BALANCE
hasher.update(self.balance.value.to_le_bytes());
hasher.update(self.balance.unit_point().to_bytes());
// Important! we don't commit to the balance blinding factor as that may make the notes linkable.
// COMMIT TO DEATH CONSTRAINT
hasher.update(&self.death_constraint);
// COMMIT TO NULLIFIER
hasher.update(nf_pk.as_bytes());
hasher.update(nonce.as_bytes());

View File

@ -7,6 +7,7 @@
// secret is used for multiple notes.
use blake2::{Blake2s256, Digest};
use rand_core::RngCore;
use serde::{Deserialize, Serialize};
// Maintained privately by note holder
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -26,7 +27,7 @@ pub struct NullifierNonce([u8; 16]);
// The nullifier attached to input notes to prove an input has not
// already been spent.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Nullifier([u8; 32]);
impl NullifierSecret {

View File

@ -1,4 +1,5 @@
use rand_core::RngCore;
use serde::{Deserialize, Serialize};
use crate::{
balance::Balance,
@ -7,13 +8,13 @@ use crate::{
nullifier::{NullifierCommitment, NullifierNonce},
};
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Output {
pub note_comm: NoteCommitment,
pub balance: Balance,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone)]
pub struct OutputWitness {
pub note: Note,
pub nf_pk: NullifierCommitment,
@ -31,7 +32,7 @@ impl OutputWitness {
}
// as we don't have SNARKS hooked up yet, the witness will be our proof
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone)]
pub struct OutputProof(OutputWitness);
impl Output {

View File

@ -3,6 +3,8 @@ use std::collections::BTreeSet;
use blake2::{Blake2s256, Digest};
use jubjub::SubgroupPoint;
use rand_core::RngCore;
use risc0_groth16::ProofJson;
use serde::{Deserialize, Serialize};
use crate::error::Error;
use crate::input::{Input, InputProof, InputWitness};
@ -19,21 +21,25 @@ impl PtxCommitment {
rng.fill_bytes(&mut sk);
Self(sk)
}
pub fn hex(&self) -> String {
hex::encode(self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PartialTx {
pub inputs: Vec<Input>,
pub outputs: Vec<Output>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone)]
pub struct PartialTxWitness {
pub inputs: Vec<InputWitness>,
pub outputs: Vec<OutputWitness>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug)]
pub struct PartialTxProof {
pub inputs: Vec<InputProof>,
pub outputs: Vec<OutputProof>,
@ -63,8 +69,14 @@ impl PartialTx {
PtxCommitment(commit_bytes)
}
pub fn prove(&self, w: PartialTxWitness) -> Result<PartialTxProof, Error> {
if &Self::from_witness(w.clone()) != self {
pub fn prove(
&self,
w: PartialTxWitness,
death_proofs: Vec<ProofJson>,
) -> 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));
@ -82,7 +94,8 @@ impl PartialTx {
self.inputs
.iter()
.zip(&w.inputs)
.map(|(i, i_w)| i.prove(i_w, ptx_comm)),
.zip(death_proofs.into_iter())
.map(|((i, i_w), death_p)| i.prove(i_w, ptx_comm, death_p)),
)?;
let output_proofs: Vec<OutputProof> = Result::from_iter(