cl: partial transactions can now be built and verified

This commit is contained in:
David Rusu 2024-06-13 17:24:02 -04:00
parent 7db1420194
commit 5ce7b253cf
6 changed files with 235 additions and 36 deletions

View File

@ -6,10 +6,13 @@ use crate::{
error::Error,
note::{Note, NoteCommitment},
nullifier::{Nullifier, NullifierNonce, NullifierSecret},
partial_tx::PtxCommitment,
};
use group::{ff::Field, GroupEncoding};
use jubjub::{ExtendedPoint, Scalar};
use rand_core::RngCore;
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Input {
pub note_comm: NoteCommitment,
pub nullifier: Nullifier,
@ -18,14 +21,26 @@ pub struct Input {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InputWitness {
note: Note,
nf_sk: NullifierSecret,
nonce: NullifierNonce,
balance_blinding: Scalar,
pub note: Note,
pub nf_sk: NullifierSecret,
pub nonce: NullifierNonce,
pub balance_blinding: Scalar,
}
impl InputWitness {
pub fn random(note: Note, mut rng: impl RngCore) -> Self {
Self {
note,
nf_sk: NullifierSecret::random(&mut rng),
nonce: NullifierNonce::random(&mut rng),
balance_blinding: Scalar::random(&mut rng),
}
}
}
// as we don't have SNARKS hooked up yet, the witness will be our proof
pub struct InputProof(InputWitness);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InputProof(InputWitness, PtxCommitment);
impl Input {
pub fn from_witness(w: InputWitness) -> Self {
@ -36,20 +51,21 @@ impl Input {
}
}
pub fn prove(&self, w: &InputWitness) -> Result<InputProof, Error> {
pub fn prove(&self, w: &InputWitness, ptx_comm: PtxCommitment) -> Result<InputProof, Error> {
if &Input::from_witness(w.clone()) != self {
Err(Error::ProofFailed)
} else {
Ok(InputProof(w.clone()))
Ok(InputProof(w.clone(), ptx_comm))
}
}
pub fn verify(&self, proof: &InputProof) -> bool {
pub fn verify(&self, ptx_comm: PtxCommitment, 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_comm is the same one that was used in proving.
let witness = &proof.0;
@ -57,6 +73,15 @@ impl Input {
self.note_comm == witness.note.commit(nf_pk, witness.nonce)
&& self.nullifier == Nullifier::new(witness.nf_sk, witness.nonce)
&& self.balance == witness.note.balance(witness.balance_blinding)
&& ptx_comm == proof.1
}
pub(crate) 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
}
}
@ -71,6 +96,8 @@ mod test {
fn test_input_proof() {
let mut rng = seed_rng(0);
let ptx_comm = PtxCommitment::default();
let note = Note::new(10, "NMO");
let nf_sk = NullifierSecret::random(&mut rng);
let nonce = NullifierNonce::random(&mut rng);
@ -84,9 +111,9 @@ mod test {
};
let input = Input::from_witness(witness.clone());
let proof = input.prove(&witness).unwrap();
let proof = input.prove(&witness, ptx_comm).unwrap();
assert!(input.verify(&proof));
assert!(input.verify(ptx_comm, &proof));
let wrong_witnesses = [
InputWitness {
@ -112,11 +139,39 @@ mod test {
];
for wrong_witness in wrong_witnesses {
assert!(input.prove(&wrong_witness).is_err());
assert!(input.prove(&wrong_witness, ptx_comm).is_err());
let wrong_input = Input::from_witness(wrong_witness.clone());
let wrong_proof = wrong_input.prove(&wrong_witness).unwrap();
assert!(!input.verify(&wrong_proof));
let wrong_proof = wrong_input.prove(&wrong_witness, ptx_comm).unwrap();
assert!(!input.verify(ptx_comm, &wrong_proof));
}
}
#[test]
fn test_input_ptx_coupling() {
let mut rng = seed_rng(0);
let note = Note::new(10, "NMO");
let nf_sk = NullifierSecret::random(&mut rng);
let nonce = NullifierNonce::random(&mut rng);
let balance_blinding = Scalar::random(&mut rng);
let witness = InputWitness {
note,
nf_sk,
nonce,
balance_blinding,
};
let input = Input::from_witness(witness.clone());
let ptx_comm = PtxCommitment::random(&mut rng);
let proof = input.prove(&witness, ptx_comm).unwrap();
assert!(input.verify(ptx_comm, &proof));
// The same input proof can not be used in another partial transaction.
let another_ptx_comm = PtxCommitment::random(&mut rng);
assert!(!input.verify(another_ptx_comm, &proof));
}
}

View File

@ -16,6 +16,12 @@ lazy_static! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NoteCommitment([u8; 32]);
impl NoteCommitment {
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Note {
pub value: u64,

View File

@ -78,6 +78,10 @@ impl Nullifier {
let nf_bytes: [u8; 32] = hasher.finalize().into();
Self(nf_bytes)
}
pub(crate) fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
}
#[cfg(test)]

View File

@ -1,4 +1,6 @@
use group::{ff::Field, GroupEncoding};
use jubjub::{ExtendedPoint, Scalar};
use rand_core::RngCore;
use crate::{
error::Error,
@ -14,10 +16,21 @@ pub struct Output {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OutputWitness {
note: Note,
nf_pk: NullifierCommitment,
nonce: NullifierNonce,
balance_blinding: Scalar,
pub note: Note,
pub nf_pk: NullifierCommitment,
pub nonce: NullifierNonce,
pub balance_blinding: Scalar,
}
impl OutputWitness {
pub fn random(note: Note, owner: NullifierCommitment, mut rng: impl RngCore) -> Self {
Self {
note,
nf_pk: owner,
nonce: NullifierNonce::random(&mut rng),
balance_blinding: Scalar::random(&mut rng),
}
}
}
// as we don't have SNARKS hooked up yet, the witness will be our proof
@ -49,6 +62,13 @@ impl Output {
self.note_comm == witness.note.commit(witness.nf_pk, witness.nonce)
&& self.balance == witness.note.balance(witness.balance_blinding)
}
pub(crate) fn to_bytes(&self) -> [u8; 64] {
let mut bytes = [0u8; 64];
bytes[..32].copy_from_slice(self.note_comm.as_bytes());
bytes[32..64].copy_from_slice(&self.balance.to_bytes());
bytes
}
}
#[cfg(test)]

View File

@ -1,31 +1,145 @@
use blake2::{Blake2s256, Digest};
use jubjub::ExtendedPoint;
use rand_core::RngCore;
use crate::input::{Input, InputProof};
use crate::output::{Output, OutputProof};
use crate::error::Error;
use crate::input::{Input, InputProof, InputWitness};
use crate::output::{Output, OutputProof, OutputWitness};
/// The partial transaction commitment couples an input to a partial transaction.
/// Prevents partial tx unbundling.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct PtxCommitment([u8; 32]);
impl PtxCommitment {
pub fn random(mut rng: impl RngCore) -> Self {
let mut sk = [0u8; 32];
rng.fill_bytes(&mut sk);
Self(sk)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PartialTx {
inputs: Vec<(Input, InputProof)>,
outputs: Vec<(Output, OutputProof)>,
inputs: Vec<Input>,
outputs: Vec<Output>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PartialTxWitness {
inputs: Vec<InputWitness>,
outputs: Vec<OutputWitness>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PartialTxProof {
inputs: Vec<InputProof>,
outputs: Vec<OutputProof>,
}
impl PartialTx {
pub fn verify(&self) -> bool {
self.inputs.iter().all(|(i, p)| i.verify(p))
&& self.outputs.iter().all(|(o, p)| o.verify(p))
pub fn from_witness(w: PartialTxWitness) -> Self {
Self {
inputs: Vec::from_iter(w.inputs.into_iter().map(Input::from_witness)),
outputs: Vec::from_iter(w.outputs.into_iter().map(Output::from_witness)),
}
}
pub fn commitment(&self) -> PtxCommitment {
let mut hasher = Blake2s256::new();
hasher.update(b"NOMOS_CL_PTX_COMMIT");
hasher.update(b"INPUTS");
for input in self.inputs.iter() {
hasher.update(input.to_bytes());
}
hasher.update(b"OUTPUTS");
for outputs in self.outputs.iter() {
hasher.update(outputs.to_bytes());
}
let commit_bytes: [u8; 32] = hasher.finalize().into();
PtxCommitment(commit_bytes)
}
pub fn prove(&self, w: PartialTxWitness) -> Result<PartialTxProof, Error> {
if &Self::from_witness(w.clone()) != self {
return Err(Error::ProofFailed);
}
let ptx_comm = self.commitment();
let input_proofs: Vec<InputProof> = Result::from_iter(
self.inputs
.iter()
.zip(&w.inputs)
.map(|(i, i_w)| i.prove(i_w, ptx_comm)),
)?;
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_comm = self.commitment();
self.inputs.len() == proof.inputs.len()
&& self.outputs.len() == proof.outputs.len()
&& self
.inputs
.iter()
.zip(&proof.inputs)
.all(|(i, p)| i.verify(ptx_comm, p))
&& self
.outputs
.iter()
.zip(&proof.outputs)
.all(|(o, p)| o.verify(p))
}
pub fn balance(&self) -> ExtendedPoint {
let in_sum = self
.inputs
.iter()
.map(|(i, _)| i.balance)
.sum::<ExtendedPoint>();
let out_sum = self
.outputs
.iter()
.map(|(o, _)| o.balance)
.sum::<ExtendedPoint>();
let in_sum: ExtendedPoint = self.inputs.iter().map(|i| i.balance).sum();
let out_sum: ExtendedPoint = self.outputs.iter().map(|o| o.balance).sum();
in_sum - out_sum
}
}
#[cfg(test)]
mod test {
use crate::{note::Note, nullifier::NullifierSecret, test_util::seed_rng};
use super::*;
#[test]
fn test_partial_tx_proof() {
let mut rng = seed_rng(0);
let nmo_10 = InputWitness::random(Note::new(10, "NMO"), &mut rng);
let eth_23 = InputWitness::random(Note::new(23, "ETH"), &mut rng);
let crv_4840 = OutputWitness::random(
Note::new(4840, "CRV"),
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).unwrap();
assert!(ptx.verify(&ptx_proof));
}
}

View File

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