cl: partial transactions can now be built and verified
This commit is contained in:
parent
7db1420194
commit
5ce7b253cf
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue