cl: partial_tx; input; output

This commit is contained in:
David Rusu 2024-06-13 12:12:08 -04:00
parent 26f6fe54f6
commit cc8c6e31cb
8 changed files with 327 additions and 82 deletions

4
cl/src/error.rs Normal file
View File

@ -0,0 +1,4 @@
#[derive(Debug)]
pub enum Error {
ProofFailed,
}

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

@ -0,0 +1,123 @@
/// 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::{
error::Error,
note::{Note, NoteCommitment},
nullifier::{Nullifier, NullifierNonce, NullifierSecret},
};
use jubjub::{ExtendedPoint, Scalar};
#[derive(Debug, PartialEq, Eq)]
pub struct Input {
pub note_comm: NoteCommitment,
pub nullifier: Nullifier,
pub balance: ExtendedPoint,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InputWitness {
note: Note,
nf_sk: NullifierSecret,
nonce: NullifierNonce,
balance_blinding: Scalar,
}
// as we don't have SNARKS hooked up yet, the witness will be our proof
pub struct InputProof(InputWitness);
impl Input {
pub fn from_witness(w: InputWitness) -> Self {
Self {
note_comm: w.note.commit(w.nf_sk.commit(), w.nonce),
nullifier: Nullifier::new(w.nf_sk, w.nonce),
balance: w.note.balance(w.balance_blinding),
}
}
pub fn prove(&self, w: &InputWitness) -> Result<InputProof, Error> {
if &Input::from_witness(w.clone()) != self {
Err(Error::ProofFailed)
} else {
Ok(InputProof(w.clone()))
}
}
pub fn verify(&self, 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
let witness = &proof.0;
let nf_pk = witness.nf_sk.commit();
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)
}
}
#[cfg(test)]
mod test {
use group::ff::Field;
use super::*;
use crate::{nullifier::NullifierNonce, test_util::seed_rng};
#[test]
fn test_input_proof() {
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 proof = input.prove(&witness).unwrap();
assert!(input.verify(&proof));
let wrong_witnesses = [
InputWitness {
note: Note::new(11, "NMO"),
..witness.clone()
},
InputWitness {
note: Note::new(10, "ETH"),
..witness.clone()
},
InputWitness {
nf_sk: NullifierSecret::random(&mut rng),
..witness.clone()
},
InputWitness {
nonce: NullifierNonce::random(&mut rng),
..witness.clone()
},
InputWitness {
balance_blinding: Scalar::random(&mut rng),
..witness.clone()
},
];
for wrong_witness in wrong_witnesses {
assert!(input.prove(&wrong_witness).is_err());
let wrong_note = Input::from_witness(wrong_witness.clone());
let wrong_proof = wrong_note.prove(&wrong_witness).unwrap();
assert!(!input.verify(&wrong_proof));
}
}
}

View File

@ -1,6 +1,13 @@
mod crypto;
mod note;
mod nullifier;
pub mod crypto;
pub mod error;
pub mod input;
pub mod note;
pub mod nullifier;
pub mod output;
pub mod partial_tx;
#[cfg(test)]
mod test_util;
fn main() {
println!("Hello, world!");

View File

@ -1,13 +1,22 @@
use blake2::{Blake2s256, Digest};
use group::GroupEncoding;
use jubjub::{ExtendedPoint, Scalar};
use lazy_static::lazy_static;
use crate::crypto;
use crate::{
crypto,
nullifier::{NullifierCommitment, NullifierNonce},
};
lazy_static! {
static ref PEDERSON_COMMITMENT_BLINDING_POINT: ExtendedPoint =
crypto::hash_to_curve(b"NOMOS_CL_PEDERSON_COMMITMENT_BLINDING");
}
#[derive(Debug, PartialEq, Eq)]
pub struct NoteCommitment([u8; 32]);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Note {
pub value: u64,
pub unit: String,
@ -21,53 +30,72 @@ impl Note {
}
}
pub fn unit_point(&self) -> ExtendedPoint {
crypto::hash_to_curve(self.unit.as_bytes())
}
pub fn balance(&self, blinding: Scalar) -> ExtendedPoint {
let value_scalar = Scalar::from(self.value);
let unit_point = crypto::hash_to_curve(self.unit.as_bytes());
self.unit_point() * value_scalar + *PEDERSON_COMMITMENT_BLINDING_POINT * blinding
}
unit_point * value_scalar + *PEDERSON_COMMITMENT_BLINDING_POINT * blinding
pub fn commit(&self, nf_pk: NullifierCommitment, nonce: NullifierNonce) -> NoteCommitment {
let mut hasher = Blake2s256::new();
hasher.update(b"NOMOS_CL_NOTE_COMMIT");
hasher.update(self.value.to_le_bytes());
hasher.update(self.unit_point().to_bytes());
hasher.update(nf_pk.as_bytes());
hasher.update(nonce.as_bytes());
let commit_bytes: [u8; 32] = hasher.finalize().into();
NoteCommitment(commit_bytes)
}
}
#[test]
fn test_balance_zero_unitless() {
// Zero is the same across all units
let r = Scalar::from(32);
assert_eq!(
Note::new(0, "NMO").balance(r),
Note::new(0, "ETH").balance(r)
);
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_balance_blinding() {
// balances are blinded
let r1 = Scalar::from(12);
let r2 = Scalar::from(8);
let a = Note::new(10, "NMO");
assert_ne!(a.balance(r1), a.balance(r2));
assert_eq!(a.balance(r1), a.balance(r1));
}
#[test]
fn test_balance_zero_unitless() {
// Zero is the same across all units
let r = Scalar::from(32);
assert_eq!(
Note::new(0, "NMO").balance(r),
Note::new(0, "ETH").balance(r)
);
}
#[test]
fn test_balance_units() {
// Unit's differentiate between values.
let nmo = Note::new(10, "NMO");
let eth = Note::new(10, "ETH");
let r = Scalar::from(1337);
assert_ne!(nmo.balance(r), eth.balance(r));
}
#[test]
fn test_balance_blinding() {
// balances are blinded
let r1 = Scalar::from(12);
let r2 = Scalar::from(8);
let a = Note::new(10, "NMO");
assert_ne!(a.balance(r1), a.balance(r2));
assert_eq!(a.balance(r1), a.balance(r1));
}
#[test]
fn test_balance_homomorphism() {
let r = Scalar::from(32);
let ten = Note::new(10, "NMO");
let eight = Note::new(8, "NMO");
let two = Note::new(2, "NMO");
assert_eq!(ten.balance(r) - eight.balance(r), two.balance(0.into()));
#[test]
fn test_balance_units() {
// Unit's differentiate between values.
let nmo = Note::new(10, "NMO");
let eth = Note::new(10, "ETH");
let r = Scalar::from(1337);
assert_ne!(nmo.balance(r), eth.balance(r));
}
assert_eq!(
ten.balance(54.into()) - ten.balance(48.into()),
Note::new(0, "NMO").balance(6.into())
);
#[test]
fn test_balance_homomorphism() {
let r = Scalar::from(32);
let ten = Note::new(10, "NMO");
let eight = Note::new(8, "NMO");
let two = Note::new(2, "NMO");
assert_eq!(ten.balance(r) - eight.balance(r), two.balance(0.into()));
assert_eq!(
ten.balance(54.into()) - ten.balance(48.into()),
Note::new(0, "NMO").balance(6.into())
);
}
}

View File

@ -6,8 +6,7 @@
// nonce is used to disambiguate when the same nullifier
// secret is used for multiple notes.
use blake2::{Blake2s256, Digest};
use hex;
use rand_core::{RngCore, SeedableRng};
use rand_core::RngCore;
// Maintained privately by note holder
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -31,16 +30,16 @@ pub struct NullifierNonce([u8; 16]);
pub struct Nullifier([u8; 32]);
impl NullifierSecret {
fn random(mut rng: impl RngCore) -> Self {
pub fn random(mut rng: impl RngCore) -> Self {
let mut sk = [0u8; 16];
rng.fill_bytes(&mut sk);
Self(sk)
}
fn commit(&self) -> NullifierCommitment {
pub fn commit(&self) -> NullifierCommitment {
let mut hasher = Blake2s256::new();
hasher.update(b"NOMOS_CL_NULL_COMMIT");
hasher.update(&self.0);
hasher.update(self.0);
let commit_bytes: [u8; 32] = hasher.finalize().into();
NullifierCommitment(commit_bytes)
@ -48,60 +47,68 @@ impl NullifierSecret {
}
impl NullifierCommitment {
pub fn to_hex(&self) -> String {
hex::encode(&self.0)
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
pub fn hex(&self) -> String {
hex::encode(self.0)
}
}
impl NullifierNonce {
fn random(mut rng: impl RngCore) -> Self {
pub fn random(mut rng: impl RngCore) -> Self {
let mut nonce = [0u8; 16];
rng.fill_bytes(&mut nonce);
Self(nonce)
}
pub fn as_bytes(&self) -> &[u8; 16] {
&self.0
}
}
impl Nullifier {
fn new(sk: NullifierSecret, nonce: NullifierNonce) -> Self {
pub fn new(sk: NullifierSecret, nonce: NullifierNonce) -> Self {
let mut hasher = Blake2s256::new();
hasher.update(b"NOMOS_CL_NULLIFIER");
hasher.update(&sk.0);
hasher.update(&nonce.0);
hasher.update(sk.0);
hasher.update(nonce.0);
let nf_bytes: [u8; 32] = hasher.finalize().into();
Self(nf_bytes)
}
}
fn seed_rng(seed: u64) -> impl rand_core::RngCore {
let mut bytes = [0u8; 32];
(&mut bytes[..8]).copy_from_slice(&seed.to_le_bytes());
rand_chacha::ChaCha12Rng::from_seed(bytes)
}
#[cfg(test)]
mod test {
use super::*;
use crate::test_util::seed_rng;
#[test]
fn test_nullifier_commitment_vectors() {
assert_eq!(
NullifierSecret([0u8; 16]).commit().to_hex(),
"384318f9864fe57647bac344e2afdc500a672dedb29d2dc63b004e940e4b382a"
);
assert_eq!(
NullifierSecret([1u8; 16]).commit().to_hex(),
"0fd667e6bb39fbdc35d6265726154b839638ea90bcf4e736953ccf27ca5f870b"
);
assert_eq!(
NullifierSecret([u8::MAX; 16]).commit().to_hex(),
"1cb78e487eb0b3116389311fdde84cd3f619a4d7f487b29bf5a002eed3784d75"
);
}
#[test]
fn test_nullifier_commitment_vectors() {
assert_eq!(
NullifierSecret([0u8; 16]).commit().hex(),
"384318f9864fe57647bac344e2afdc500a672dedb29d2dc63b004e940e4b382a"
);
assert_eq!(
NullifierSecret([1u8; 16]).commit().hex(),
"0fd667e6bb39fbdc35d6265726154b839638ea90bcf4e736953ccf27ca5f870b"
);
assert_eq!(
NullifierSecret([u8::MAX; 16]).commit().hex(),
"1cb78e487eb0b3116389311fdde84cd3f619a4d7f487b29bf5a002eed3784d75"
);
}
#[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 nf_1 = Nullifier::new(sk, nonce_1);
let nf_2 = Nullifier::new(sk, nonce_2);
#[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 nf_1 = Nullifier::new(sk, nonce_1);
let nf_2 = Nullifier::new(sk, nonce_2);
assert_ne!(nf_1, nf_2);
assert_ne!(nf_1, nf_2);
}
}

38
cl/src/output.rs Normal file
View File

@ -0,0 +1,38 @@
use jubjub::{ExtendedPoint, Scalar};
use crate::{
note::{Note, NoteCommitment},
nullifier::{NullifierCommitment, NullifierNonce},
};
pub struct Output {
pub note_comm: NoteCommitment,
pub balance: ExtendedPoint,
}
pub struct OutputWitness {
note: Note,
nf_pk: NullifierCommitment,
nonce: NullifierNonce,
balance_blinding: Scalar,
}
// as we don't have SNARKS hooked up yet, the witness will be our proof
pub struct OutputProof(OutputWitness);
impl Output {
pub fn prove(&self, w: OutputWitness) -> OutputProof {
OutputProof(w)
}
pub fn verify(&self, proof: &OutputProof) -> bool {
// verification checks the relation
// - note_comm == commit(note || nf_pk)
// - balance == v * hash_to_curve(Unit) + blinding * H
let witness = &proof.0;
self.note_comm == witness.note.commit(witness.nf_pk, witness.nonce)
&& self.balance == witness.note.balance(witness.balance_blinding)
}
}

31
cl/src/partial_tx.rs Normal file
View File

@ -0,0 +1,31 @@
use jubjub::ExtendedPoint;
use crate::input::{Input, InputProof};
use crate::output::{Output, OutputProof};
pub struct PartialTx {
inputs: Vec<(Input, InputProof)>,
outputs: Vec<(Output, 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 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>();
in_sum - out_sum
}
}

7
cl/src/test_util.rs Normal file
View File

@ -0,0 +1,7 @@
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());
rand_chacha::ChaCha12Rng::from_seed(bytes)
}