Sigma pass (#22801)

* zk-token-sdk: add zeroize and reference arithmetic to zero-balance proof

* zk-token-sdk: add zeroize and reference arithmetic to equality proof

* zk-token-sdk: add zeroize and reference arithmetic to validity proof

* zk-token-sdk: add aggregated validity proof

* zk-token-sdk: use subtle choice for fee

* zk-token-sdk: add test for fee proof

* zk-token-sdk: add documentation for sigma protocols

* zk-token-sdk: add edge case tests for equality proof

* zk-token-sdk: add edge case tests for zero-balance proof

* zk-token-sdk: add edge case tests for validity proof

* zk-token-sdk: add some docs for fee sigma proof

* zk-token-sdk: clippy
This commit is contained in:
samkim-crypto 2022-01-27 20:53:15 -04:00 committed by GitHub
parent 89c42ebcbe
commit 5cef4c0a4c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 1094 additions and 392 deletions

View File

@ -15,7 +15,7 @@ use {
errors::ProofError,
instruction::{Role, Verifiable},
range_proof::RangeProof,
sigma_proofs::{equality_proof::EqualityProof, validity_proof::ValidityProof},
sigma_proofs::{equality_proof::EqualityProof, validity_proof::AggregatedValidityProof},
transcript::TranscriptProtocol,
},
curve25519_dalek::scalar::Scalar,
@ -248,7 +248,7 @@ pub struct TransferProof {
pub equality_proof: pod::EqualityProof,
/// Associated ciphertext validity proof
pub validity_proof: pod::ValidityProof,
pub validity_proof: pod::AggregatedValidityProof,
// Associated range proof
pub range_proof: pod::RangeProof128,
@ -292,9 +292,6 @@ impl TransferProof {
transcript.append_point(b"D_EG", &D_EG.compress());
transcript.append_point(b"C_Ped", &C_Ped.compress());
// let c = transcript.challenge_scalar(b"c");
// println!("{:?}", c);
// generate equality_proof
let equality_proof = EqualityProof::new(
source_keypair,
@ -305,8 +302,12 @@ impl TransferProof {
);
// generate ciphertext validity proof
let validity_proof =
ValidityProof::new(dest_pk, auditor_pk, transfer_amt, openings, &mut transcript);
let validity_proof = AggregatedValidityProof::new(
(dest_pk, auditor_pk),
transfer_amt,
openings,
&mut transcript,
);
// generate the range proof
let range_proof = RangeProof::new(
@ -336,7 +337,7 @@ impl TransferProof {
let commitment: PedersenCommitment = self.source_commitment.try_into()?;
let equality_proof: EqualityProof = self.equality_proof.try_into()?;
let validity_proof: ValidityProof = self.validity_proof.try_into()?;
let aggregated_validity_proof: AggregatedValidityProof = self.validity_proof.try_into()?;
let range_proof: RangeProof = self.range_proof.try_into()?;
// add a domain separator to record the start of the protocol
@ -376,9 +377,8 @@ impl TransferProof {
let handle_hi_auditor: DecryptHandle = decryption_handles_hi.auditor.try_into()?;
// TODO: validity proof
validity_proof.verify(
&dest_elgamal_pubkey,
&auditor_elgamal_pubkey,
aggregated_validity_proof.verify(
(&dest_elgamal_pubkey, &auditor_elgamal_pubkey),
(&amount_comm_lo, &amount_comm_hi),
(&handle_lo_dest, &handle_hi_dest),
(&handle_lo_auditor, &handle_hi_auditor),

View File

@ -1,3 +1,13 @@
//! The equality sigma proof system.
//!
//! An equality proof is defined with respect to two cryptographic objects: a twisted ElGamal
//! ciphertext and a Pedersen commitment. The proof certifies that a given ciphertext and
//! commitment pair encrypts/encodes the same message. To generate the proof, a prover must provide
//! the decryption key for the ciphertext and the Pedersen opening for the commitment.
//!
//! The protocol guarantees computationally soundness (by the hardness of discrete log) and perfect
//! zero-knowledge in the random oracle model.
#[cfg(not(target_arch = "bpf"))]
use {
crate::encryption::{
@ -6,6 +16,7 @@ use {
},
curve25519_dalek::traits::MultiscalarMul,
rand::rngs::OsRng,
zeroize::Zeroize,
};
use {
crate::{sigma_proofs::errors::EqualityProofError, transcript::TranscriptProtocol},
@ -18,24 +29,43 @@ use {
merlin::Transcript,
};
/// Equality proof.
///
/// Contains all the elliptic curve and scalar components that make up the sigma protocol.
#[allow(non_snake_case)]
#[derive(Clone)]
pub struct EqualityProof {
pub Y_0: CompressedRistretto,
pub Y_1: CompressedRistretto,
pub Y_2: CompressedRistretto,
pub z_s: Scalar,
pub z_x: Scalar,
pub z_r: Scalar,
Y_0: CompressedRistretto,
Y_1: CompressedRistretto,
Y_2: CompressedRistretto,
z_s: Scalar,
z_x: Scalar,
z_r: Scalar,
}
#[allow(non_snake_case)]
#[cfg(not(target_arch = "bpf"))]
impl EqualityProof {
/// Equality proof constructor.
///
/// The function does *not* hash the public key, ciphertext, or commitment into the transcript.
/// For security, the caller (the main protocol) should hash these public components prior to
/// invoking this constructor.
///
/// This function is randomized. It uses `OsRng` internally to generate random scalars.
///
/// Note that the proof constructor does not take the actual Pedersen commitment as input; it
/// takes the associated Pedersen opening instead.
///
/// * `elgamal_keypair` - The ElGamal keypair associated with the ciphertext to be proved
/// * `ciphertext` - The main ElGamal ciphertext to be proved
/// * `amount` - The message associated with the ElGamal ciphertext and Pedersen commitment
/// * `opening` - The opening associated with the main Pedersen commitment to be proved
/// * `transcript` - The transcript that does the bookkeeping for the Fiat-Shamir heuristic
pub fn new(
elgamal_keypair: &ElGamalKeypair,
ciphertext: &ElGamalCiphertext,
message: u64,
amount: u64,
opening: &PedersenOpening,
transcript: &mut Transcript,
) -> Self {
@ -44,19 +74,19 @@ impl EqualityProof {
let D_EG = ciphertext.handle.get_point();
let s = elgamal_keypair.secret.get_scalar();
let x = Scalar::from(message);
let x = Scalar::from(amount);
let r = opening.get_scalar();
// generate random masking factors that also serves as a nonce
let y_s = Scalar::random(&mut OsRng);
let y_x = Scalar::random(&mut OsRng);
let y_r = Scalar::random(&mut OsRng);
// generate random masking factors that also serves as nonces
let mut y_s = Scalar::random(&mut OsRng);
let mut y_x = Scalar::random(&mut OsRng);
let mut y_r = Scalar::random(&mut OsRng);
let Y_0 = (y_s * P_EG).compress();
let Y_1 = RistrettoPoint::multiscalar_mul(vec![y_x, y_s], vec![&(*G), D_EG]).compress();
let Y_2 = RistrettoPoint::multiscalar_mul(vec![y_x, y_r], vec![&(*G), &(*H)]).compress();
let Y_0 = (&y_s * P_EG).compress();
let Y_1 = RistrettoPoint::multiscalar_mul(vec![&y_x, &y_s], vec![&(*G), D_EG]).compress();
let Y_2 = RistrettoPoint::multiscalar_mul(vec![&y_x, &y_r], vec![&(*G), &(*H)]).compress();
// record masking factors in transcript
// record masking factors in the transcript
transcript.append_point(b"Y_0", &Y_0);
transcript.append_point(b"Y_1", &Y_1);
transcript.append_point(b"Y_2", &Y_2);
@ -65,9 +95,14 @@ impl EqualityProof {
transcript.challenge_scalar(b"w");
// compute the masked values
let z_s = c * s + y_s;
let z_x = c * x + y_x;
let z_r = c * r + y_r;
let z_s = &(&c * s) + &y_s;
let z_x = &(&c * &x) + &y_x;
let z_r = &(&c * r) + &y_r;
// zeroize random scalars
y_s.zeroize();
y_x.zeroize();
y_r.zeroize();
EqualityProof {
Y_0,
@ -79,6 +114,12 @@ impl EqualityProof {
}
}
/// Equality proof verifier.
///
/// * `elgamal_pubkey` - The ElGamal pubkey associated with the ciphertext to be proved
/// * `ciphertext` - The main ElGamal ciphertext to be proved
/// * `commitment` - The main Pedersen commitment to be proved
/// * `transcript` - The transcript that does the bookkeeping for the Fiat-Shamir heuristic
pub fn verify(
self,
elgamal_pubkey: &ElGamalPubkey,
@ -90,7 +131,6 @@ impl EqualityProof {
let P_EG = elgamal_pubkey.get_point();
let C_EG = ciphertext.commitment.get_point();
let D_EG = ciphertext.handle.get_point();
let C_Ped = commitment.get_point();
// include Y_0, Y_1, Y_2 to transcript and extract challenges
@ -99,8 +139,11 @@ impl EqualityProof {
transcript.validate_and_append_point(b"Y_2", &self.Y_2)?;
let c = transcript.challenge_scalar(b"c");
let w = transcript.challenge_scalar(b"w");
let ww = w * w;
let w = transcript.challenge_scalar(b"w"); // w used for batch verification
let ww = &w * &w;
let w_negated = -&w;
let ww_negated = -&ww;
// check that the required algebraic condition holds
let Y_0 = self.Y_0.decompress().ok_or(EqualityProofError::Format)?;
@ -109,30 +152,30 @@ impl EqualityProof {
let check = RistrettoPoint::vartime_multiscalar_mul(
vec![
self.z_s,
-c,
-Scalar::one(),
w * self.z_x,
w * self.z_s,
-w * c,
-w,
ww * self.z_x,
ww * self.z_r,
-ww * c,
-ww,
&self.z_s, // z_s
&(-&c), // -c
&(-&Scalar::one()), // -identity
&(&w * &self.z_x), // w * z_x
&(&w * &self.z_s), // w * z_s
&(&w_negated * &c), // -w * c
&w_negated, // -w
&(&ww * &self.z_x), // ww * z_x
&(&ww * &self.z_r), // ww * z_r
&(&ww_negated * &c), // -ww * c
&ww_negated, // -ww
],
vec![
P_EG,
&(*H),
&Y_0,
&(*G),
D_EG,
C_EG,
&Y_1,
&(*G),
&(*H),
C_Ped,
&Y_2,
P_EG, // P_EG
&(*H), // H
&Y_0, // Y_0
&(*G), // G
D_EG, // D_EG
C_EG, // C_EG
&Y_1, // Y_1
&(*G), // G
&(*H), // H
C_Ped, // C_Ped
&Y_2, // Y_2
],
);
@ -180,10 +223,10 @@ impl EqualityProof {
#[cfg(test)]
mod test {
use super::*;
use crate::encryption::pedersen::Pedersen;
use crate::encryption::{elgamal::ElGamalSecretKey, pedersen::Pedersen};
#[test]
fn test_equality_proof() {
fn test_equality_proof_correctness() {
// success case
let elgamal_keypair = ElGamalKeypair::new_rand();
let message: u64 = 55;
@ -239,4 +282,123 @@ mod test {
)
.is_err());
}
#[test]
fn test_equality_proof_edge_cases() {
// if ElGamal public key zero (public key is invalid), then the proof should always reject
let public = ElGamalPubkey::from_bytes(&[0u8; 32]).unwrap();
let secret = ElGamalSecretKey::new_rand();
let elgamal_keypair = ElGamalKeypair { public, secret };
let message: u64 = 55;
let ciphertext = elgamal_keypair.public.encrypt(message);
let (commitment, opening) = Pedersen::new(message);
let mut transcript_prover = Transcript::new(b"Test");
let mut transcript_verifier = Transcript::new(b"Test");
let proof = EqualityProof::new(
&elgamal_keypair,
&ciphertext,
message,
&opening,
&mut transcript_prover,
);
assert!(proof
.verify(
&elgamal_keypair.public,
&ciphertext,
&commitment,
&mut transcript_verifier
)
.is_err());
// if ciphertext is all-zero (valid commitment of 0) and commitment is also all-zero, then
// the proof should still accept
let elgamal_keypair = ElGamalKeypair::new_rand();
let message: u64 = 0;
let ciphertext = ElGamalCiphertext::from_bytes(&[0u8; 64]).unwrap();
let commitment = PedersenCommitment::from_bytes(&[0u8; 32]).unwrap();
let opening = PedersenOpening::from_bytes(&[0u8; 32]).unwrap();
let mut transcript_prover = Transcript::new(b"Test");
let mut transcript_verifier = Transcript::new(b"Test");
let proof = EqualityProof::new(
&elgamal_keypair,
&ciphertext,
message,
&opening,
&mut transcript_prover,
);
assert!(proof
.verify(
&elgamal_keypair.public,
&ciphertext,
&commitment,
&mut transcript_verifier
)
.is_ok());
// if commitment is all-zero and the ciphertext is a correct encryption of 0, then the
// proof should still accept
let elgamal_keypair = ElGamalKeypair::new_rand();
let message: u64 = 0;
let ciphertext = elgamal_keypair.public.encrypt(message);
let commitment = PedersenCommitment::from_bytes(&[0u8; 32]).unwrap();
let opening = PedersenOpening::from_bytes(&[0u8; 32]).unwrap();
let mut transcript_prover = Transcript::new(b"Test");
let mut transcript_verifier = Transcript::new(b"Test");
let proof = EqualityProof::new(
&elgamal_keypair,
&ciphertext,
message,
&opening,
&mut transcript_prover,
);
assert!(proof
.verify(
&elgamal_keypair.public,
&ciphertext,
&commitment,
&mut transcript_verifier
)
.is_ok());
// if ciphertext is all zero and commitment correctly encodes 0, then the proof should
// still accept
let elgamal_keypair = ElGamalKeypair::new_rand();
let message: u64 = 0;
let ciphertext = ElGamalCiphertext::from_bytes(&[0u8; 64]).unwrap();
let (commitment, opening) = Pedersen::new(message);
let mut transcript_prover = Transcript::new(b"Test");
let mut transcript_verifier = Transcript::new(b"Test");
let proof = EqualityProof::new(
&elgamal_keypair,
&ciphertext,
message,
&opening,
&mut transcript_prover,
);
assert!(proof
.verify(
&elgamal_keypair.public,
&ciphertext,
&commitment,
&mut transcript_verifier
)
.is_ok());
}
}

View File

@ -1,3 +1,7 @@
//! The sigma proofs for transfer fees.
//!
//! TODO: Add detail on how the fee is calculated.
#[cfg(not(target_arch = "bpf"))]
use {
crate::encryption::pedersen::{PedersenCommitment, PedersenOpening, G, H},
@ -11,223 +15,294 @@ use {
traits::{IsIdentity, MultiscalarMul, VartimeMultiscalarMul},
},
merlin::Transcript,
subtle::{Choice, ConditionallySelectable, ConstantTimeGreater},
};
/// Fee sigma proof.
///
/// The proof consists of two main components: `fee_max_proof` and `fee_equality_proof`. If the fee
/// surpasses the maximum fee bound, then the `fee_max_proof` should properly be generated and
/// `fee_equality_proof` should be simulated. If the fee is below the maximum fee bound, then the
/// `fee_equality_proof` should be properly generated and `fee_max_proof` should be simulated.
#[derive(Clone)]
pub struct FeeProof {
pub fee_max_proof: FeeMaxProof,
pub fee_equality_proof: FeeEqualityProof,
pub struct FeeSigmaProof {
/// Proof that the committed fee amount equals the maximum fee bound
fee_max_proof: FeeMaxProof,
/// Proof that the "real" delta value is equal to the "claimed" delta value
fee_equality_proof: FeeEqualityProof,
}
#[allow(non_snake_case, dead_code)]
#[cfg(not(target_arch = "bpf"))]
impl FeeProof {
#[allow(clippy::too_many_arguments)]
impl FeeSigmaProof {
/// Creates a fee sigma proof assuming that the committed fee is greater than the maximum fee
/// bound.
///
/// * `(fee_amount, commitment_fee, opening_fee)` - The amount, Pedersen commitment, and
/// opening of the transfer fee
/// * `(delta_fee, commitment_delta, opening_delta)` - The amount, Pedersen commitment, and
/// opening of the "real" delta amount
/// * `(commitment_claimed, opening_claimed)` - The Pedersen commitment and opening of the
/// "claimed" delta amount
/// * `max_fee` - The maximum fee bound
/// * `transcript` - The transcript that does the bookkeeping for the Fiat-Shamir heuristic
pub fn new(
amount_fee: u64,
(fee_amount, commitment_fee, opening_fee): (u64, &PedersenCommitment, &PedersenOpening),
(delta_fee, commitment_delta, opening_delta): (u64, &PedersenCommitment, &PedersenOpening),
(commitment_claimed, opening_claimed): (&PedersenCommitment, &PedersenOpening),
max_fee: u64,
delta_fee: u64,
commitment_fee: &PedersenCommitment,
opening_fee: &PedersenOpening,
commitment_delta_real: &PedersenCommitment,
opening_delta_real: &PedersenOpening,
commitment_delta_claimed: &PedersenCommitment,
opening_delta_claimed: &PedersenOpening,
transcript: &mut Transcript,
) -> Self {
// extract the relevant scalar and Ristretto points from the input
let x = Scalar::from(delta_fee);
let m = Scalar::from(max_fee);
let mut transcript_fee_above_max = transcript.clone();
let mut transcript_fee_below_max = transcript.clone();
let C_max = commitment_fee.get_point();
let r_max = opening_fee.get_scalar();
// compute proof for both cases `fee_amount' >= `max_fee` and `fee_amount` < `max_fee`
let proof_fee_above_max = Self::create_proof_fee_above_max(
opening_fee,
commitment_delta,
commitment_claimed,
&mut transcript_fee_above_max,
);
let C_delta_real = commitment_delta_real.get_point();
let r_delta_real = opening_delta_real.get_scalar();
let proof_fee_below_max = Self::create_proof_fee_below_max(
commitment_fee,
(delta_fee, opening_delta),
opening_claimed,
max_fee,
&mut transcript_fee_below_max,
);
let C_delta_claimed = commitment_delta_claimed.get_point();
let r_delta_claimed = opening_delta_claimed.get_scalar();
let above_max = u64::ct_gt(&max_fee, &fee_amount);
// record public values in transcript
//
// TODO: consider committing to these points outside this method
transcript.append_point(b"C_max", &C_max.compress());
transcript.append_point(b"C_delta_real", &C_delta_real.compress());
transcript.append_point(b"C_delta_claimed", &C_delta_claimed.compress());
// generate z values depending on whether the fee exceeds max fee or not
//
// TODO: must implement this for constant time
if amount_fee < max_fee {
// simulate max proof
let z_max = Scalar::random(&mut OsRng);
let c_max = Scalar::random(&mut OsRng);
let Y_max = RistrettoPoint::multiscalar_mul(
vec![z_max, -c_max, c_max * m],
vec![&(*H), C_max, &(*G)],
)
.compress();
let fee_max_proof = FeeMaxProof {
Y_max,
z_max,
c_max,
};
// generate equality proof
let y_x = Scalar::random(&mut OsRng);
let y_delta_real = Scalar::random(&mut OsRng);
let y_delta_claimed = Scalar::random(&mut OsRng);
let Y_delta_real =
RistrettoPoint::multiscalar_mul(vec![y_x, y_delta_real], vec![&(*G), &(*H)])
.compress();
let Y_delta_claimed =
RistrettoPoint::multiscalar_mul(vec![y_x, y_delta_claimed], vec![&(*G), &(*H)])
.compress();
transcript.append_point(b"Y_max", &Y_max);
transcript.append_point(b"Y_delta_real", &Y_delta_real);
transcript.append_point(b"Y_delta_claimed", &Y_delta_claimed);
let c = transcript.challenge_scalar(b"c");
let c_equality = c - c_max;
transcript.challenge_scalar(b"w");
let z_x = c_equality * x + y_x;
let z_delta_real = c_equality * r_delta_real + y_delta_real;
let z_delta_claimed = c_equality * r_delta_claimed + y_delta_claimed;
let fee_equality_proof = FeeEqualityProof {
Y_delta_real,
Y_delta_claimed,
z_x,
z_delta_real,
z_delta_claimed,
};
Self {
fee_max_proof,
fee_equality_proof,
}
// conditionally assign transcript; transcript is not conditionally selectable
if bool::from(above_max) {
*transcript = transcript_fee_above_max;
} else {
// simulate equality proof
let z_x = Scalar::random(&mut OsRng);
let z_delta_real = Scalar::random(&mut OsRng);
let z_delta_claimed = Scalar::random(&mut OsRng);
let c_equality = Scalar::random(&mut OsRng);
*transcript = transcript_fee_below_max;
}
let Y_delta_real = RistrettoPoint::multiscalar_mul(
vec![z_x, z_delta_real, -c_equality],
vec![&(*G), &(*H), C_delta_real],
)
.compress();
let Y_delta_claimed = RistrettoPoint::multiscalar_mul(
vec![z_x, z_delta_claimed, -c_equality],
vec![&(*G), &(*H), C_delta_claimed],
)
.compress();
let fee_equality_proof = FeeEqualityProof {
Y_delta_real,
Y_delta_claimed,
z_x,
z_delta_real,
z_delta_claimed,
};
// generate max proof
let y_max = Scalar::random(&mut OsRng);
let Y_max = (y_max * &(*H)).compress();
transcript.append_point(b"Y_max", &Y_max);
transcript.append_point(b"Y_delta_real", &Y_delta_real);
transcript.append_point(b"Y_delta_claimed", &Y_delta_claimed);
let c = transcript.challenge_scalar(b"c");
let c_max = c - c_equality;
transcript.challenge_scalar(b"w");
let z_max = c_max * r_max + y_max;
let fee_max_proof = FeeMaxProof {
Y_max,
z_max,
c_max,
};
Self {
fee_max_proof,
fee_equality_proof,
}
Self {
fee_max_proof: FeeMaxProof::conditional_select(
&proof_fee_above_max.fee_max_proof,
&proof_fee_below_max.fee_max_proof,
above_max,
),
fee_equality_proof: FeeEqualityProof::conditional_select(
&proof_fee_above_max.fee_equality_proof,
&proof_fee_below_max.fee_equality_proof,
above_max,
),
}
}
/// Creates a fee sigma proof assuming that the committed fee is greater than the maximum fee
/// bound.
///
/// * `opening_fee` - The opening of the Pedersen commitment of the transfer fee
/// * `commitment_delta` - The Pedersen commitment of the "real" delta value
/// * `commitment_claimed` - The Pedersen commitment of the "claimed" delta value
/// * `transcript` - The transcript that does the bookkeeping for the Fiat-Shamir heuristic
fn create_proof_fee_above_max(
opening_fee: &PedersenOpening,
commitment_delta: &PedersenCommitment,
commitment_claimed: &PedersenCommitment,
transcript: &mut Transcript,
) -> Self {
// simulate equality proof
let C_delta = commitment_delta.get_point();
let C_claimed = commitment_claimed.get_point();
let z_x = Scalar::random(&mut OsRng);
let z_delta = Scalar::random(&mut OsRng);
let z_claimed = Scalar::random(&mut OsRng);
let c_equality = Scalar::random(&mut OsRng);
let Y_delta = RistrettoPoint::multiscalar_mul(
vec![z_x, z_delta, -c_equality],
vec![&(*G), &(*H), C_delta],
)
.compress();
let Y_claimed = RistrettoPoint::multiscalar_mul(
vec![z_x, z_claimed, -c_equality],
vec![&(*G), &(*H), C_claimed],
)
.compress();
let fee_equality_proof = FeeEqualityProof {
Y_delta,
Y_claimed,
z_x,
z_delta,
z_claimed,
};
// generate max proof
let r_fee = opening_fee.get_scalar();
let y_max_proof = Scalar::random(&mut OsRng);
let Y_max_proof = (y_max_proof * &(*H)).compress();
transcript.append_point(b"Y_max_proof", &Y_max_proof);
transcript.append_point(b"Y_delta", &Y_delta);
transcript.append_point(b"Y_claimed", &Y_claimed);
let c = transcript.challenge_scalar(b"c");
let c_max_proof = c - c_equality;
transcript.challenge_scalar(b"w");
let z_max_proof = c_max_proof * r_fee + y_max_proof;
let fee_max_proof = FeeMaxProof {
Y_max_proof,
z_max_proof,
c_max_proof,
};
Self {
fee_max_proof,
fee_equality_proof,
}
}
/// Creates a fee sigma proof assuming that the committed fee is less than the maximum fee
/// bound.
///
/// * `commitment_fee` - The Pedersen commitment of the transfer fee
/// * `(delta_fee, opening_delta)` - The Pedersen commitment and opening of the "real" delta
/// value
/// * `opening_claimed` - The opening of the Pedersen commitment of the "claimed" delta value
/// * `max_fee` - The maximum fee bound
/// * `transcript` - The transcript that does the bookkeeping for the Fiat-Shamir heuristic
fn create_proof_fee_below_max(
commitment_fee: &PedersenCommitment,
(delta_fee, opening_delta): (u64, &PedersenOpening),
opening_claimed: &PedersenOpening,
max_fee: u64,
transcript: &mut Transcript,
) -> Self {
// simulate max proof
let m = Scalar::from(max_fee);
let C_fee = commitment_fee.get_point();
let z_max_proof = Scalar::random(&mut OsRng);
let c_max_proof = Scalar::random(&mut OsRng); // random challenge
// solve for Y_max in the verification algebraic relation
let Y_max_proof = RistrettoPoint::multiscalar_mul(
vec![z_max_proof, -c_max_proof, c_max_proof * m],
vec![&(*H), C_fee, &(*G)],
)
.compress();
let fee_max_proof = FeeMaxProof {
Y_max_proof,
z_max_proof,
c_max_proof,
};
// generate equality proof
let x = Scalar::from(delta_fee);
let r_delta = opening_delta.get_scalar();
let r_claimed = opening_claimed.get_scalar();
let y_x = Scalar::random(&mut OsRng);
let y_delta = Scalar::random(&mut OsRng);
let y_claimed = Scalar::random(&mut OsRng);
let Y_delta =
RistrettoPoint::multiscalar_mul(vec![y_x, y_delta], vec![&(*G), &(*H)]).compress();
let Y_claimed =
RistrettoPoint::multiscalar_mul(vec![y_x, y_claimed], vec![&(*G), &(*H)]).compress();
transcript.append_point(b"Y_max_proof", &Y_max_proof);
transcript.append_point(b"Y_delta", &Y_delta);
transcript.append_point(b"Y_claimed", &Y_claimed);
let c = transcript.challenge_scalar(b"c");
let c_equality = c - c_max_proof;
transcript.challenge_scalar(b"w");
let z_x = c_equality * x + y_x;
let z_delta = c_equality * r_delta + y_delta;
let z_claimed = c_equality * r_claimed + y_claimed;
let fee_equality_proof = FeeEqualityProof {
Y_delta,
Y_claimed,
z_x,
z_delta,
z_claimed,
};
Self {
fee_max_proof,
fee_equality_proof,
}
}
/// Fee sigma proof verifier.
///
/// * `commitment_fee` - The Pedersen commitment of the transfer fee
/// * `commitment_delta` - The Pedersen commitment of the "real" delta value
/// * `commitment_claimed` - The Pedersen commitment of the "claimed" delta value
/// * `max_fee` - The maximum fee bound
/// * `transcript` - The transcript that does the bookkeeping for the Fiat-Shamir heuristic
pub fn verify(
self,
commitment_fee: &PedersenCommitment,
commitment_delta: &PedersenCommitment,
commitment_claimed: &PedersenCommitment,
max_fee: u64,
commitment_fee: PedersenCommitment,
commitment_delta_real: PedersenCommitment,
commitment_delta_claimed: PedersenCommitment,
transcript: &mut Transcript,
) -> Result<(), FeeProofError> {
// extract the relevant scalar and Ristretto points from the input
let m = Scalar::from(max_fee);
let C_max = commitment_fee.get_point();
let C_delta_real = commitment_delta_real.get_point();
let C_delta_claimed = commitment_delta_claimed.get_point();
let C_delta = commitment_delta.get_point();
let C_claimed = commitment_claimed.get_point();
// record public values in transcript
//
// TODO: consider committing to these points outside this method
transcript.validate_and_append_point(b"C_max", &C_max.compress())?;
transcript.validate_and_append_point(b"C_delta_real", &C_delta_real.compress())?;
transcript.validate_and_append_point(b"C_delta_claimed", &C_delta_claimed.compress())?;
transcript.validate_and_append_point(b"Y_max", &self.fee_max_proof.Y_max)?;
transcript
.validate_and_append_point(b"Y_delta_real", &self.fee_equality_proof.Y_delta_real)?;
transcript.validate_and_append_point(
b"Y_delta_claimed",
&self.fee_equality_proof.Y_delta_claimed,
)?;
transcript.validate_and_append_point(b"Y_max_proof", &self.fee_max_proof.Y_max_proof)?;
transcript.validate_and_append_point(b"Y_delta", &self.fee_equality_proof.Y_delta)?;
transcript.validate_and_append_point(b"Y_claimed", &self.fee_equality_proof.Y_claimed)?;
let Y_max = self
.fee_max_proof
.Y_max
.Y_max_proof
.decompress()
.ok_or(FeeProofError::Format)?;
let z_max = self.fee_max_proof.z_max;
let z_max = self.fee_max_proof.z_max_proof;
let Y_delta_real = self
.fee_equality_proof
.Y_delta_real
.Y_delta
.decompress()
.ok_or(FeeProofError::Format)?;
let Y_delta_claimed = self
let Y_claimed = self
.fee_equality_proof
.Y_delta_claimed
.Y_claimed
.decompress()
.ok_or(FeeProofError::Format)?;
let z_x = self.fee_equality_proof.z_x;
let z_delta_real = self.fee_equality_proof.z_delta_real;
let z_delta_claimed = self.fee_equality_proof.z_delta_claimed;
let z_delta_real = self.fee_equality_proof.z_delta;
let z_claimed = self.fee_equality_proof.z_claimed;
let c = transcript.challenge_scalar(b"c");
let c_max = self.fee_max_proof.c_max;
let c_equality = c - c_max;
let c_max_proof = self.fee_max_proof.c_max_proof;
let c_equality = c - c_max_proof;
let w = transcript.challenge_scalar(b"w");
let ww = w * w;
let check = RistrettoPoint::vartime_multiscalar_mul(
vec![
c_max,
-c_max * m,
c_max_proof,
-c_max_proof * m,
-z_max,
Scalar::one(),
w * z_x,
@ -235,7 +310,7 @@ impl FeeProof {
-w * c_equality,
-w,
ww * z_x,
ww * z_delta_claimed,
ww * z_claimed,
-ww * c_equality,
-ww,
],
@ -246,12 +321,12 @@ impl FeeProof {
&Y_max,
&(*G),
&(*H),
C_delta_real,
C_delta,
&Y_delta_real,
&(*G),
&(*H),
C_delta_claimed,
&Y_delta_claimed,
C_claimed,
&Y_claimed,
],
);
@ -263,22 +338,64 @@ impl FeeProof {
}
}
/// The fee max proof.
///
/// The proof certifies that the transfer fee Pedersen commitment encodes the maximum fee bound.
#[allow(non_snake_case)]
#[derive(Clone)]
#[derive(Clone, Copy)]
pub struct FeeMaxProof {
pub Y_max: CompressedRistretto,
pub z_max: Scalar,
pub c_max: Scalar,
Y_max_proof: CompressedRistretto,
z_max_proof: Scalar,
c_max_proof: Scalar,
}
impl ConditionallySelectable for FeeMaxProof {
fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self {
Self {
Y_max_proof: conditional_select_ristretto(&a.Y_max_proof, &b.Y_max_proof, choice),
z_max_proof: Scalar::conditional_select(&a.z_max_proof, &b.z_max_proof, choice),
c_max_proof: Scalar::conditional_select(&a.c_max_proof, &b.c_max_proof, choice),
}
}
}
/// The fee equality proof.
///
/// The proof certifies that the "real" delta value commitment and the "claimed" delta value
/// commitment encode the same message.
#[allow(non_snake_case)]
#[derive(Clone)]
#[derive(Clone, Copy)]
pub struct FeeEqualityProof {
pub Y_delta_real: CompressedRistretto,
pub Y_delta_claimed: CompressedRistretto,
pub z_x: Scalar,
pub z_delta_real: Scalar,
pub z_delta_claimed: Scalar,
Y_delta: CompressedRistretto,
Y_claimed: CompressedRistretto,
z_x: Scalar,
z_delta: Scalar,
z_claimed: Scalar,
}
impl ConditionallySelectable for FeeEqualityProof {
fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self {
Self {
Y_delta: conditional_select_ristretto(&a.Y_delta, &b.Y_delta, choice),
Y_claimed: conditional_select_ristretto(&a.Y_claimed, &b.Y_claimed, choice),
z_x: Scalar::conditional_select(&a.z_x, &b.z_x, choice),
z_delta: Scalar::conditional_select(&a.z_delta, &b.z_delta, choice),
z_claimed: Scalar::conditional_select(&a.z_claimed, &b.z_claimed, choice),
}
}
}
#[allow(clippy::needless_range_loop)]
fn conditional_select_ristretto(
a: &CompressedRistretto,
b: &CompressedRistretto,
choice: Choice,
) -> CompressedRistretto {
let mut bytes = [0u8; 32];
for i in 0..32 {
bytes[i] = u8::conditional_select(&a.as_bytes()[i], &b.as_bytes()[i], choice);
}
CompressedRistretto(bytes)
}
#[cfg(test)]
@ -287,47 +404,89 @@ mod test {
use crate::encryption::pedersen::Pedersen;
#[test]
fn test_fee_proof() {
fn test_fee_above_max_proof() {
let transfer_amount: u64 = 55;
let max_fee: u64 = 77;
let max_fee: u64 = 3;
let rate_fee: u16 = 555; // 5.55%
let amount_fee = 3;
let delta_fee: u64 = 525;
let amount_fee: u64 = 4;
let delta: u64 = 9475; // 4*10000 - 55*555
let (commitment_transfer, opening_transfer) = Pedersen::new(transfer_amount);
let (commitment_fee, opening_fee) = Pedersen::new(amount_fee);
let (commitment_fee, opening_fee) = Pedersen::new(max_fee);
let scalar_rate = Scalar::from(rate_fee);
let commitment_delta_real =
commitment_transfer * scalar_rate - commitment_fee * Scalar::from(10000_u64);
let opening_delta_real =
opening_transfer * scalar_rate - opening_fee.clone() * Scalar::from(10000_u64);
let commitment_delta =
&commitment_fee * &Scalar::from(10000_u64) - &commitment_transfer * &scalar_rate;
let opening_delta =
&opening_fee * &Scalar::from(10000_u64) - &opening_transfer * &scalar_rate;
let (commitment_delta_claimed, opening_delta_claimed) = Pedersen::new(delta_fee);
let (commitment_claimed, opening_claimed) = Pedersen::new(0_u64);
let mut transcript_prover = Transcript::new(b"test");
let mut transcript_verifier = Transcript::new(b"test");
let proof = FeeProof::new(
amount_fee,
let proof = FeeSigmaProof::new(
(amount_fee, &commitment_fee, &opening_fee),
(delta, &commitment_delta, &opening_delta),
(&commitment_claimed, &opening_claimed),
max_fee,
delta_fee,
&commitment_fee,
&opening_fee,
&commitment_delta_real,
&opening_delta_real,
&commitment_delta_claimed,
&opening_delta_claimed,
&mut transcript_prover,
);
assert!(proof
.verify(
&commitment_fee,
&commitment_delta,
&commitment_claimed,
max_fee,
&mut transcript_verifier,
)
.is_ok());
}
#[test]
fn test_fee_below_max_proof() {
let transfer_amount: u64 = 55;
let max_fee: u64 = 77;
let rate_fee: u16 = 555; // 5.55%
let amount_fee: u64 = 4;
let delta: u64 = 9475; // 4*10000 - 55*555
let (commitment_transfer, opening_transfer) = Pedersen::new(transfer_amount);
let (commitment_fee, opening_fee) = Pedersen::new(amount_fee);
let scalar_rate = Scalar::from(rate_fee);
let commitment_delta =
&commitment_fee * &Scalar::from(10000_u64) - &commitment_transfer * &scalar_rate;
let opening_delta =
&opening_fee * &Scalar::from(10000_u64) - &opening_transfer * &scalar_rate;
let (commitment_claimed, opening_claimed) = Pedersen::new(delta);
assert_eq!(
commitment_delta.get_point() - opening_delta.get_scalar() * &(*H),
commitment_claimed.get_point() - opening_claimed.get_scalar() * &(*H)
);
let mut transcript_prover = Transcript::new(b"test");
let mut transcript_verifier = Transcript::new(b"test");
let proof = FeeSigmaProof::new(
(amount_fee, &commitment_fee, &opening_fee),
(delta, &commitment_delta, &opening_delta),
(&commitment_claimed, &opening_claimed),
max_fee,
&mut transcript_prover,
);
assert!(proof
.verify(
&commitment_fee,
&commitment_delta,
&commitment_claimed,
max_fee,
commitment_fee,
commitment_delta_real,
commitment_delta_claimed,
&mut transcript_verifier,
)
.is_ok());

View File

@ -1,3 +1,20 @@
//! Collection of sigma proofs (more precisely, "arguments") that are used in the Solana zk-token
//! protocol.
//!
//! The module contains implementations of the following proof systems that work on Pedersen
//! commitments and twisted ElGamal ciphertexts:
//! - Equality proof: can be used to certify that a twisted ElGamal ciphertext and a Pedersen
//! commitment encrypt/encode the same message.
//! - Validity proof: can be used to certify that a twisted ElGamal ciphertext is a properly-formed
//! ciphertext with respect to a pair of ElGamal public keys.
//! - Zero-balance proof: can be used to certify that a twisted ElGamal ciphertext encrypts the
//! message 0.
//! - Fee proof: can be used to certify that an ElGamal ciphertext properly encrypts a transfer
//! fee.
//!
//! We refer to the zk-token paper for the formal details and security proofs of these argument
//! systems.
pub mod equality_proof;
pub mod errors;
pub mod fee_proof;

View File

@ -1,3 +1,13 @@
//! The ciphertext validity sigma proof system.
//!
//! The ciphertext validity proof is defined with respect to a Pedersen commitment and two
//! decryption handles. The proof certifies that a given Pedersen commitment can be decrypted using
//! ElGamal private keys that are associated with each of the two decryption handles. To generate
//! the proof, a prover must provide the Pedersen opening associated with the commitment.
//!
//! The protocol guarantees computational soundness (by the hardness of discrete log) and perfect
//! zero-knowledge in the random oracle model.
#[cfg(not(target_arch = "bpf"))]
use {
crate::encryption::{
@ -6,6 +16,7 @@ use {
},
curve25519_dalek::traits::MultiscalarMul,
rand::rngs::OsRng,
zeroize::Zeroize,
};
use {
crate::{sigma_proofs::errors::ValidityProofError, transcript::TranscriptProtocol},
@ -18,54 +29,73 @@ use {
merlin::Transcript,
};
/// The ciphertext validity proof.
///
/// Contains all the elliptic curve and scalar components that make up the sigma protocol.
#[allow(non_snake_case)]
#[derive(Clone)]
pub struct ValidityProof {
pub Y_0: CompressedRistretto,
pub Y_1: CompressedRistretto,
pub Y_2: CompressedRistretto,
pub z_r: Scalar,
pub z_x: Scalar,
Y_0: CompressedRistretto,
Y_1: CompressedRistretto,
Y_2: CompressedRistretto,
z_r: Scalar,
z_x: Scalar,
}
#[allow(non_snake_case)]
#[cfg(not(target_arch = "bpf"))]
impl ValidityProof {
pub fn new(
elgamal_pubkey_dest: &ElGamalPubkey,
elgamal_pubkey_auditor: &ElGamalPubkey,
messages: (u64, u64),
openings: (&PedersenOpening, &PedersenOpening),
/// The ciphertext validity proof constructor.
///
/// The function does *not* hash the public keys, commitment, or decryption handles into the
/// transcript. For security, the caller (the main protocol) should hash these public
/// components prior to invoking this constructor.
///
/// This function is randomized. It uses `OsRng` internally to generate random scalars.
///
/// Note that the proof constructor does not take the actual Pedersen commitment or decryption
/// handles as input; it only takes the associated Pedersen opening instead.
///
/// * `(pubkey_dest, pubkey_auditor)` - The ElGamal public keys associated with the decryption
/// handles
/// * `amount` - The committed message in the commitment
/// * `opening` - The opening associated with the Pedersen commitment
/// * `transcript` - The transcript that does the bookkeeping for the Fiat-Shamir heuristic
pub fn new<T: Into<Scalar>>(
(pubkey_dest, pubkey_auditor): (&ElGamalPubkey, &ElGamalPubkey),
amount: T,
opening: &PedersenOpening,
transcript: &mut Transcript,
) -> Self {
// extract the relevant scalar and Ristretto points from the inputs
let P_dest = elgamal_pubkey_dest.get_point();
let P_auditor = elgamal_pubkey_auditor.get_point();
let P_dest = pubkey_dest.get_point();
let P_auditor = pubkey_auditor.get_point();
// generate random masking factors that also serves as a nonce
let y_r = Scalar::random(&mut OsRng);
let y_x = Scalar::random(&mut OsRng);
let x = amount.into();
let r = opening.get_scalar();
let Y_0 = RistrettoPoint::multiscalar_mul(vec![y_r, y_x], vec![&(*H), &(*G)]).compress();
let Y_1 = (y_r * P_dest).compress();
let Y_2 = (y_r * P_auditor).compress();
// generate random masking factors that also serves as nonces
let mut y_r = Scalar::random(&mut OsRng);
let mut y_x = Scalar::random(&mut OsRng);
let Y_0 = RistrettoPoint::multiscalar_mul(vec![&y_r, &y_x], vec![&(*H), &(*G)]).compress();
let Y_1 = (&y_r * P_dest).compress();
let Y_2 = (&y_r * P_auditor).compress();
// record masking factors in transcript and get challenges
transcript.append_point(b"Y_0", &Y_0);
transcript.append_point(b"Y_1", &Y_1);
transcript.append_point(b"Y_2", &Y_2);
let t = transcript.challenge_scalar(b"t");
let c = transcript.challenge_scalar(b"c");
transcript.challenge_scalar(b"w");
// aggregate lo and hi messages and openings
let x = Scalar::from(messages.0) + t * Scalar::from(messages.1);
let r = openings.0.get_scalar() + t * openings.1.get_scalar();
// compute masked message and opening
let z_r = c * r + y_r;
let z_x = c * x + y_x;
let z_r = &(&c * r) + &y_r;
let z_x = &(&c * &x) + &y_x;
y_r.zeroize();
y_x.zeroize();
Self {
Y_0,
@ -76,13 +106,18 @@ impl ValidityProof {
}
}
/// The ciphertext validity proof verifier.
///
/// * `commitment` - The Pedersen commitment
/// * `(pubkey_dest, pubkey_auditor)` - The ElGamal pubkeys associated with the decryption
/// handles
/// * `(handle_dest, handle_audtior)` - The decryption handles
/// * `transcript` - The transcript that does the bookkeeping for the Fiat-Shamir heuristic
pub fn verify(
self,
elgamal_pubkey_dest: &ElGamalPubkey,
elgamal_pubkey_auditor: &ElGamalPubkey,
commitments: (&PedersenCommitment, &PedersenCommitment),
handle_dest: (&DecryptHandle, &DecryptHandle),
handle_auditor: (&DecryptHandle, &DecryptHandle),
commitment: &PedersenCommitment,
(pubkey_dest, pubkey_auditor): (&ElGamalPubkey, &ElGamalPubkey),
(handle_dest, handle_auditor): (&DecryptHandle, &DecryptHandle),
transcript: &mut Transcript,
) -> Result<(), ValidityProofError> {
// include Y_0, Y_1, Y_2 to transcript and extract challenges
@ -90,47 +125,49 @@ impl ValidityProof {
transcript.validate_and_append_point(b"Y_1", &self.Y_1)?;
transcript.validate_and_append_point(b"Y_2", &self.Y_2)?;
let t = transcript.challenge_scalar(b"t");
let c = transcript.challenge_scalar(b"c");
let w = transcript.challenge_scalar(b"w");
let ww = w * w;
let ww = &w * &w;
let w_negated = -&w;
let ww_negated = -&ww;
// check the required algebraic conditions
let Y_0 = self.Y_0.decompress().ok_or(ValidityProofError::Format)?;
let Y_1 = self.Y_1.decompress().ok_or(ValidityProofError::Format)?;
let Y_2 = self.Y_2.decompress().ok_or(ValidityProofError::Format)?;
let P_dest = elgamal_pubkey_dest.get_point();
let P_auditor = elgamal_pubkey_auditor.get_point();
let P_dest = pubkey_dest.get_point();
let P_auditor = pubkey_auditor.get_point();
let C = commitments.0.get_point() + t * commitments.1.get_point();
let D_dest = handle_dest.0.get_point() + t * handle_dest.1.get_point();
let D_auditor = handle_auditor.0.get_point() + t * handle_auditor.1.get_point();
let C = commitment.get_point();
let D_dest = handle_dest.get_point();
let D_auditor = handle_auditor.get_point();
let check = RistrettoPoint::vartime_multiscalar_mul(
vec![
self.z_r,
self.z_x,
-c,
-Scalar::one(),
w * self.z_r,
-w * c,
-w,
ww * self.z_r,
-ww * c,
-ww,
&self.z_r, // z_r
&self.z_x, // z_x
&(-&c), // -c
&-(&Scalar::one()), // -identity
&(&w * &self.z_r), // w * z_r
&(&w_negated * &c), // -w * c
&w_negated, // -w
&(&ww * &self.z_r), // ww * z_r
&(&ww_negated * &c), // -ww * c
&ww_negated, // -ww
],
vec![
&(*H),
&(*G),
&C,
&Y_0,
P_dest,
&D_dest,
&Y_1,
P_auditor,
&D_auditor,
&Y_2,
&(*H), // H
&(*G), // G
C, // C
&Y_0, // Y_0
P_dest, // P_dest
D_dest, // D_dest
&Y_1, // Y_1
P_auditor, // P_auditor
D_auditor, // D_auditor
&Y_2, // Y_2
],
);
@ -172,21 +209,251 @@ impl ValidityProof {
}
}
/// Aggregated ciphertext validity proof.
///
/// An aggregated ciphertext validity proof certifies the validity of two instances of a standard
/// ciphertext validity proof. An instance of a standard validity proof consist of one ciphertext
/// and two decryption handles `(commitment, handle_dest, handle_auditor)`. An instance of an
/// aggregated ciphertext validity proof is a pair `(commitment_0, handle_dest_0,
/// handle_auditor_0)` and `(commitment_1, handle_dest_1, handle_auditor_1)`. The proof certifies
/// the analogous decryptable properties for each one of these pair of commitment and decryption
/// handles.
#[allow(non_snake_case)]
#[derive(Clone)]
pub struct AggregatedValidityProof(ValidityProof);
#[allow(non_snake_case)]
#[cfg(not(target_arch = "bpf"))]
impl AggregatedValidityProof {
/// Aggregated ciphertext validity proof constructor.
///
/// The function simples aggregates the input openings and invokes the standard ciphertext
/// validity proof constructor.
pub fn new<T: Into<Scalar>>(
(pubkey_dest, pubkey_auditor): (&ElGamalPubkey, &ElGamalPubkey),
(amount_lo, amount_hi): (T, T),
(opening_lo, opening_hi): (&PedersenOpening, &PedersenOpening),
transcript: &mut Transcript,
) -> Self {
let t = transcript.challenge_scalar(b"t");
let aggregated_message = amount_lo.into() + amount_hi.into() * t;
let aggregated_opening = opening_lo + &(opening_hi * &t);
AggregatedValidityProof(ValidityProof::new(
(pubkey_dest, pubkey_auditor),
aggregated_message,
&aggregated_opening,
transcript,
))
}
/// Aggregated ciphertext validity proof verifier.
///
/// The function does *not* hash the public keys, commitment, or decryption handles into the
/// transcript. For security, the caller (the main protocol) should hash these public
/// components prior to invoking this constructor.
///
/// This function is randomized. It uses `OsRng` internally to generate random scalars.
pub fn verify(
self,
(pubkey_dest, pubkey_auditor): (&ElGamalPubkey, &ElGamalPubkey),
(commitment_lo, commitment_hi): (&PedersenCommitment, &PedersenCommitment),
(handle_lo_dest, handle_hi_dest): (&DecryptHandle, &DecryptHandle),
(handle_lo_auditor, handle_hi_auditor): (&DecryptHandle, &DecryptHandle),
transcript: &mut Transcript,
) -> Result<(), ValidityProofError> {
let t = transcript.challenge_scalar(b"t");
let aggregated_commitment = commitment_lo + commitment_hi * t;
let aggregated_handle_dest = handle_lo_dest + handle_hi_dest * t;
let aggregated_handle_auditor = handle_lo_auditor + handle_hi_auditor * t;
let AggregatedValidityProof(validity_proof) = self;
validity_proof.verify(
&aggregated_commitment,
(pubkey_dest, pubkey_auditor),
(&aggregated_handle_dest, &aggregated_handle_auditor),
transcript,
)
}
pub fn to_bytes(&self) -> [u8; 160] {
self.0.to_bytes()
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, ValidityProofError> {
ValidityProof::from_bytes(bytes).map(Self)
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::encryption::{elgamal::ElGamalKeypair, pedersen::Pedersen};
#[test]
fn test_validity_proof() {
fn test_validity_proof_correctness() {
let elgamal_pubkey_dest = ElGamalKeypair::new_rand().public;
let elgamal_pubkey_auditor = ElGamalKeypair::new_rand().public;
let x_lo: u64 = 55;
let x_hi: u64 = 77;
let amount: u64 = 55;
let (commitment, opening) = Pedersen::new(amount);
let (commitment_lo, open_lo) = Pedersen::new(x_lo);
let (commitment_hi, open_hi) = Pedersen::new(x_hi);
let handle_dest = elgamal_pubkey_dest.decrypt_handle(&opening);
let handle_auditor = elgamal_pubkey_auditor.decrypt_handle(&opening);
let mut transcript_prover = Transcript::new(b"Test");
let mut transcript_verifier = Transcript::new(b"Test");
let proof = ValidityProof::new(
(&elgamal_pubkey_dest, &elgamal_pubkey_auditor),
amount,
&opening,
&mut transcript_prover,
);
assert!(proof
.verify(
&commitment,
(&elgamal_pubkey_dest, &elgamal_pubkey_auditor),
(&handle_dest, &handle_auditor),
&mut transcript_verifier,
)
.is_ok());
}
#[test]
fn test_validity_proof_edge_cases() {
// if destination public key zeroed, then the proof should always reject
let elgamal_pubkey_dest = ElGamalPubkey::from_bytes(&[0u8; 32]).unwrap();
let elgamal_pubkey_auditor = ElGamalKeypair::new_rand().public;
let amount: u64 = 55;
let (commitment, opening) = Pedersen::new(amount);
let handle_dest = elgamal_pubkey_dest.decrypt_handle(&opening);
let handle_auditor = elgamal_pubkey_auditor.decrypt_handle(&opening);
let mut transcript_prover = Transcript::new(b"Test");
let mut transcript_verifier = Transcript::new(b"Test");
let proof = ValidityProof::new(
(&elgamal_pubkey_dest, &elgamal_pubkey_auditor),
amount,
&opening,
&mut transcript_prover,
);
assert!(proof
.verify(
&commitment,
(&elgamal_pubkey_dest, &elgamal_pubkey_auditor),
(&handle_dest, &handle_auditor),
&mut transcript_verifier,
)
.is_err());
// if auditor public key zeroed, then the proof should always reject
let elgamal_pubkey_dest = ElGamalKeypair::new_rand().public;
let elgamal_pubkey_auditor = ElGamalPubkey::from_bytes(&[0u8; 32]).unwrap();
let amount: u64 = 55;
let (commitment, opening) = Pedersen::new(amount);
let handle_dest = elgamal_pubkey_dest.decrypt_handle(&opening);
let handle_auditor = elgamal_pubkey_auditor.decrypt_handle(&opening);
let mut transcript_prover = Transcript::new(b"Test");
let mut transcript_verifier = Transcript::new(b"Test");
let proof = ValidityProof::new(
(&elgamal_pubkey_dest, &elgamal_pubkey_auditor),
amount,
&opening,
&mut transcript_prover,
);
assert!(proof
.verify(
&commitment,
(&elgamal_pubkey_dest, &elgamal_pubkey_auditor),
(&handle_dest, &handle_auditor),
&mut transcript_verifier,
)
.is_err());
// all zeroed ciphertext should still be valid
let elgamal_pubkey_dest = ElGamalKeypair::new_rand().public;
let elgamal_pubkey_auditor = ElGamalKeypair::new_rand().public;
let amount: u64 = 0;
let commitment = PedersenCommitment::from_bytes(&[0u8; 32]).unwrap();
let opening = PedersenOpening::from_bytes(&[0u8; 32]).unwrap();
let handle_dest = elgamal_pubkey_dest.decrypt_handle(&opening);
let handle_auditor = elgamal_pubkey_auditor.decrypt_handle(&opening);
let mut transcript_prover = Transcript::new(b"Test");
let mut transcript_verifier = Transcript::new(b"Test");
let proof = ValidityProof::new(
(&elgamal_pubkey_dest, &elgamal_pubkey_auditor),
amount,
&opening,
&mut transcript_prover,
);
assert!(proof
.verify(
&commitment,
(&elgamal_pubkey_dest, &elgamal_pubkey_auditor),
(&handle_dest, &handle_auditor),
&mut transcript_verifier,
)
.is_ok());
// decryption handles can be zero as long as the Pedersen commitment is valid
let elgamal_pubkey_dest = ElGamalKeypair::new_rand().public;
let elgamal_pubkey_auditor = ElGamalKeypair::new_rand().public;
let amount: u64 = 55;
let (commitment, opening) = Pedersen::new(amount);
let handle_dest = elgamal_pubkey_dest.decrypt_handle(&opening);
let handle_auditor = elgamal_pubkey_auditor.decrypt_handle(&opening);
let mut transcript_prover = Transcript::new(b"Test");
let mut transcript_verifier = Transcript::new(b"Test");
let proof = ValidityProof::new(
(&elgamal_pubkey_dest, &elgamal_pubkey_auditor),
amount,
&opening,
&mut transcript_prover,
);
assert!(proof
.verify(
&commitment,
(&elgamal_pubkey_dest, &elgamal_pubkey_auditor),
(&handle_dest, &handle_auditor),
&mut transcript_verifier,
)
.is_ok());
}
#[test]
fn test_aggregated_validity_proof() {
let elgamal_pubkey_dest = ElGamalKeypair::new_rand().public;
let elgamal_pubkey_auditor = ElGamalKeypair::new_rand().public;
let amount_lo: u64 = 55;
let amount_hi: u64 = 77;
let (commitment_lo, open_lo) = Pedersen::new(amount_lo);
let (commitment_hi, open_hi) = Pedersen::new(amount_hi);
let handle_lo_dest = elgamal_pubkey_dest.decrypt_handle(&open_lo);
let handle_hi_dest = elgamal_pubkey_dest.decrypt_handle(&open_hi);
@ -197,27 +464,21 @@ mod test {
let mut transcript_prover = Transcript::new(b"Test");
let mut transcript_verifier = Transcript::new(b"Test");
let proof = ValidityProof::new(
&elgamal_pubkey_dest,
&elgamal_pubkey_auditor,
(x_lo, x_hi),
let proof = AggregatedValidityProof::new(
(&elgamal_pubkey_dest, &elgamal_pubkey_auditor),
(amount_lo, amount_hi),
(&open_lo, &open_hi),
&mut transcript_prover,
);
assert!(proof
.verify(
&elgamal_pubkey_dest,
&elgamal_pubkey_auditor,
(&elgamal_pubkey_dest, &elgamal_pubkey_auditor),
(&commitment_lo, &commitment_hi),
(&handle_lo_dest, &handle_hi_dest),
(&handle_lo_auditor, &handle_hi_auditor),
&mut transcript_verifier,
)
.is_ok());
// TODO: Test invalid cases
// TODO: Test serialization, deserialization
}
}

View File

@ -1,3 +1,12 @@
//! The zero-balance sigma proof system.
//!
//! A zero-balance proof is defined with respect to a twisted ElGamal ciphertext. The proof
//! certifies that a given ciphertext encrypts the message 0 (`Scalar::zero()`). To generate the
//! proof, a prover must provide the decryption key for the ciphertext.
//!
//! The protocol guarantees computationally soundness (by the hardness of discrete log) and perfect
//! zero-knowledge in the random oracle model.
#[cfg(not(target_arch = "bpf"))]
use {
crate::encryption::{
@ -6,6 +15,7 @@ use {
},
curve25519_dalek::traits::MultiscalarMul,
rand::rngs::OsRng,
zeroize::Zeroize,
};
use {
crate::{sigma_proofs::errors::ZeroBalanceProofError, transcript::TranscriptProtocol},
@ -18,83 +28,112 @@ use {
merlin::Transcript,
};
/// Zero-balance proof.
///
/// Contains all the elliptic curve and scalar components that make up the sigma protocol.
#[allow(non_snake_case)]
#[derive(Clone)]
pub struct ZeroBalanceProof {
pub Y_P: CompressedRistretto,
pub Y_D: CompressedRistretto,
pub z: Scalar,
Y_P: CompressedRistretto,
Y_D: CompressedRistretto,
z: Scalar,
}
#[allow(non_snake_case)]
#[cfg(not(target_arch = "bpf"))]
impl ZeroBalanceProof {
/// Zero-balance proof constructor.
///
/// The function does *not* hash the public key and ciphertext into the transcript. For
/// security, the caller (the main protocol) should hash these public components prior to
/// invoking this constructor.
///
/// This function is randomized. It uses `OsRng` internally to generate random scalars.
///
/// Note that the proof constructor does not take the actual ElGamal ciphertext as input; it
/// uses the ElGamal private key instead to generate the proof.
///
/// * `elgamal_keypair` - The ElGamal keypair associated with the ciphertext to be proved
/// * `ciphertext` - The main ElGamal ciphertext to be proved
/// * `transcript` - The transcript that does the bookkeeping for the Fiat-Shamir heuristic
pub fn new(
elgamal_keypair: &ElGamalKeypair,
elgamal_ciphertext: &ElGamalCiphertext,
ciphertext: &ElGamalCiphertext,
transcript: &mut Transcript,
) -> Self {
// extract the relevant scalar and Ristretto points from the input
let P = elgamal_keypair.public.get_point();
let s = elgamal_keypair.secret.get_scalar();
let C = elgamal_ciphertext.commitment.get_point();
let D = elgamal_ciphertext.handle.get_point();
// record ElGamal pubkey and ciphertext in the transcript
transcript.append_point(b"P", &P.compress());
transcript.append_point(b"C", &C.compress());
transcript.append_point(b"D", &D.compress());
let D = ciphertext.handle.get_point();
// generate a random masking factor that also serves as a nonce
let y = Scalar::random(&mut OsRng);
let Y_P = (y * P).compress();
let Y_D = (y * D).compress();
let mut y = Scalar::random(&mut OsRng);
let Y_P = (&y * P).compress();
let Y_D = (&y * D).compress();
// record Y in transcript and receive a challenge scalar
// record Y in the transcript and receive a challenge scalar
transcript.append_point(b"Y_P", &Y_P);
transcript.append_point(b"Y_D", &Y_D);
let c = transcript.challenge_scalar(b"c");
transcript.challenge_scalar(b"w");
// compute the masked secret key
let z = c * s + y;
let z = &(&c * s) + &y;
// zeroize random scalar
y.zeroize();
Self { Y_P, Y_D, z }
}
/// Zero-balance proof verifier.
///
/// * `elgamal_pubkey` - The ElGamal pubkey associated with the ciphertext to be proved
/// * `ciphertext` - The main ElGamal ciphertext to be proved
/// * `transcript` - The transcript that does the bookkeeping for the Fiat-Shamir heuristic
pub fn verify(
self,
elgamal_pubkey: &ElGamalPubkey,
elgamal_ciphertext: &ElGamalCiphertext,
ciphertext: &ElGamalCiphertext,
transcript: &mut Transcript,
) -> Result<(), ZeroBalanceProofError> {
// extract the relevant scalar and Ristretto points from the input
let P = elgamal_pubkey.get_point();
let C = elgamal_ciphertext.commitment.get_point();
let D = elgamal_ciphertext.handle.get_point();
// record ElGamal pubkey and ciphertext in the transcript
transcript.validate_and_append_point(b"P", &P.compress())?;
transcript.append_point(b"C", &C.compress());
transcript.append_point(b"D", &D.compress());
let C = ciphertext.commitment.get_point();
let D = ciphertext.handle.get_point();
// record Y in transcript and receive challenge scalars
transcript.validate_and_append_point(b"Y_P", &self.Y_P)?;
transcript.append_point(b"Y_D", &self.Y_D);
let c = transcript.challenge_scalar(b"c");
let w = transcript.challenge_scalar(b"w"); // w used for multiscalar multiplication verification
let w = transcript.challenge_scalar(b"w"); // w used for batch verification
let w_negated = -&w;
// decompress R or return verification error
let Y_P = self.Y_P.decompress().ok_or(ZeroBalanceProofError::Format)?;
let Y_D = self.Y_D.decompress().ok_or(ZeroBalanceProofError::Format)?;
let z = self.z;
// check the required algebraic relation
let check = RistrettoPoint::multiscalar_mul(
vec![z, -c, -Scalar::one(), w * z, -w * c, -w],
vec![P, &(*H), &Y_P, D, C, &Y_D],
vec![
&self.z, // z
&(-&c), // -c
&(-&Scalar::one()), // -identity
&(&w * &self.z), // w * z
&(&w_negated * &c), // -w * c
&w_negated, // -w
],
vec![
P, // P
&(*H), // H
&Y_P, // Y_P
D, // D
C, // C
&Y_D, // Y_D
],
);
if check.is_identity() {
@ -129,12 +168,12 @@ impl ZeroBalanceProof {
mod test {
use super::*;
use crate::encryption::{
elgamal::{DecryptHandle, ElGamalKeypair},
pedersen::{Pedersen, PedersenOpening},
elgamal::{DecryptHandle, ElGamalKeypair, ElGamalSecretKey},
pedersen::{Pedersen, PedersenCommitment, PedersenOpening},
};
#[test]
fn test_zero_balance_proof() {
fn test_zero_balance_proof_correctness() {
let source_keypair = ElGamalKeypair::new_rand();
let mut transcript_prover = Transcript::new(b"test");
@ -163,51 +202,89 @@ mod test {
&mut transcript_verifier
)
.is_err());
}
// // edge case: all zero ciphertext - such ciphertext should always be a valid encryption of 0
let zeroed_ct = ElGamalCiphertext::default();
let proof = ZeroBalanceProof::new(&source_keypair, &zeroed_ct, &mut transcript_prover);
assert!(proof
.verify(&source_keypair.public, &zeroed_ct, &mut transcript_verifier)
.is_ok());
#[test]
fn test_zero_balance_proof_edge_cases() {
let source_keypair = ElGamalKeypair::new_rand();
// edge cases: only C or D is zero - such ciphertext is always invalid
let zeroed_comm = Pedersen::with(0_u64, &PedersenOpening::default());
let handle = elgamal_ciphertext.handle;
let mut transcript_prover = Transcript::new(b"test");
let mut transcript_verifier = Transcript::new(b"test");
let zeroed_comm_ciphertext = ElGamalCiphertext {
commitment: zeroed_comm,
handle,
};
// all zero ciphertext should always be a valid encryption of 0
let ciphertext = ElGamalCiphertext::from_bytes(&[0u8; 64]).unwrap();
let proof = ZeroBalanceProof::new(&source_keypair, &ciphertext, &mut transcript_prover);
let proof = ZeroBalanceProof::new(
&source_keypair,
&zeroed_comm_ciphertext,
&mut transcript_prover,
);
assert!(proof
.verify(
&source_keypair.public,
&zeroed_comm_ciphertext,
&ciphertext,
&mut transcript_verifier
)
.is_ok());
// if only either commitment or handle is zero, the ciphertext is always invalid and proof
// verification should always reject
let mut transcript_prover = Transcript::new(b"test");
let mut transcript_verifier = Transcript::new(b"test");
let zeroed_commitment = PedersenCommitment::from_bytes(&[0u8; 32]).unwrap();
let handle = source_keypair
.public
.decrypt_handle(&PedersenOpening::new_rand());
let ciphertext = ElGamalCiphertext {
commitment: zeroed_commitment,
handle,
};
let proof = ZeroBalanceProof::new(&source_keypair, &ciphertext, &mut transcript_prover);
assert!(proof
.verify(
&source_keypair.public,
&ciphertext,
&mut transcript_verifier
)
.is_err());
let (zero_comm, _) = Pedersen::new(0_u64);
let zeroed_handle_ciphertext = ElGamalCiphertext {
commitment: zero_comm,
handle: DecryptHandle::default(),
let mut transcript_prover = Transcript::new(b"test");
let mut transcript_verifier = Transcript::new(b"test");
let (zeroed_commitment, _) = Pedersen::new(0_u64);
let ciphertext = ElGamalCiphertext {
commitment: zeroed_commitment,
handle: DecryptHandle::from_bytes(&[0u8; 32]).unwrap(),
};
let proof = ZeroBalanceProof::new(
&source_keypair,
&zeroed_handle_ciphertext,
&mut transcript_prover,
);
let proof = ZeroBalanceProof::new(&source_keypair, &ciphertext, &mut transcript_prover);
assert!(proof
.verify(
&source_keypair.public,
&zeroed_handle_ciphertext,
&ciphertext,
&mut transcript_verifier
)
.is_err());
// if public key is always zero, then the proof should always reject
let mut transcript_prover = Transcript::new(b"test");
let mut transcript_verifier = Transcript::new(b"test");
let public = ElGamalPubkey::from_bytes(&[0u8; 32]).unwrap();
let secret = ElGamalSecretKey::new_rand();
let elgamal_keypair = ElGamalKeypair { public, secret };
let ciphertext = elgamal_keypair.public.encrypt(0_u64);
let proof = ZeroBalanceProof::new(&source_keypair, &ciphertext, &mut transcript_prover);
assert!(proof
.verify(
&source_keypair.public,
&ciphertext,
&mut transcript_verifier
)
.is_err());

View File

@ -23,7 +23,9 @@ mod target_arch {
errors::ProofError,
range_proof::{errors::RangeProofError, RangeProof},
sigma_proofs::{
equality_proof::EqualityProof, errors::*, validity_proof::ValidityProof,
equality_proof::EqualityProof,
errors::*,
validity_proof::{AggregatedValidityProof, ValidityProof},
zero_balance_proof::ZeroBalanceProof,
},
},
@ -172,6 +174,20 @@ mod target_arch {
}
}
impl From<AggregatedValidityProof> for pod::AggregatedValidityProof {
fn from(proof: AggregatedValidityProof) -> Self {
Self(proof.to_bytes())
}
}
impl TryFrom<pod::AggregatedValidityProof> for AggregatedValidityProof {
type Error = ValidityProofError;
fn try_from(pod: pod::AggregatedValidityProof) -> Result<Self, Self::Error> {
Self::from_bytes(&pod.0)
}
}
impl From<ZeroBalanceProof> for pod::ZeroBalanceProof {
fn from(proof: ZeroBalanceProof) -> Self {
Self(proof.to_bytes())

View File

@ -75,6 +75,16 @@ pub struct ValidityProof(pub [u8; 160]);
unsafe impl Zeroable for ValidityProof {}
unsafe impl Pod for ValidityProof {}
/// Serialization of aggregated validity proofs
#[derive(Clone, Copy)]
#[repr(transparent)]
pub struct AggregatedValidityProof(pub [u8; 160]);
// `AggregatedValidityProof` is a Pod and Zeroable.
// Add the marker traits manually because `bytemuck` only adds them for some `u8` arrays
unsafe impl Zeroable for AggregatedValidityProof {}
unsafe impl Pod for AggregatedValidityProof {}
/// Serialization of zero balance proofs
#[derive(Clone, Copy)]
#[repr(transparent)]