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:
@ -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),
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
|
@ -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())
|
||||
|
@ -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)]
|
||||
|
Reference in New Issue
Block a user