diff --git a/zk-token-sdk/src/instruction/transfer.rs b/zk-token-sdk/src/instruction/transfer.rs index 5e6c09d607..bda43292c9 100644 --- a/zk-token-sdk/src/instruction/transfer.rs +++ b/zk-token-sdk/src/instruction/transfer.rs @@ -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), diff --git a/zk-token-sdk/src/sigma_proofs/equality_proof.rs b/zk-token-sdk/src/sigma_proofs/equality_proof.rs index 1d96b63c64..3a25e5f3e9 100644 --- a/zk-token-sdk/src/sigma_proofs/equality_proof.rs +++ b/zk-token-sdk/src/sigma_proofs/equality_proof.rs @@ -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()); + } } diff --git a/zk-token-sdk/src/sigma_proofs/fee_proof.rs b/zk-token-sdk/src/sigma_proofs/fee_proof.rs index 018c86bd97..bf952e70a7 100644 --- a/zk-token-sdk/src/sigma_proofs/fee_proof.rs +++ b/zk-token-sdk/src/sigma_proofs/fee_proof.rs @@ -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()); diff --git a/zk-token-sdk/src/sigma_proofs/mod.rs b/zk-token-sdk/src/sigma_proofs/mod.rs index c7d731d9f2..42a392bdf6 100644 --- a/zk-token-sdk/src/sigma_proofs/mod.rs +++ b/zk-token-sdk/src/sigma_proofs/mod.rs @@ -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; diff --git a/zk-token-sdk/src/sigma_proofs/validity_proof.rs b/zk-token-sdk/src/sigma_proofs/validity_proof.rs index adeac231d9..ea3c7dd8cf 100644 --- a/zk-token-sdk/src/sigma_proofs/validity_proof.rs +++ b/zk-token-sdk/src/sigma_proofs/validity_proof.rs @@ -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>( + (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>( + (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 { + 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 } } diff --git a/zk-token-sdk/src/sigma_proofs/zero_balance_proof.rs b/zk-token-sdk/src/sigma_proofs/zero_balance_proof.rs index 6027d5e6fc..38681674bc 100644 --- a/zk-token-sdk/src/sigma_proofs/zero_balance_proof.rs +++ b/zk-token-sdk/src/sigma_proofs/zero_balance_proof.rs @@ -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()); diff --git a/zk-token-sdk/src/zk_token_elgamal/convert.rs b/zk-token-sdk/src/zk_token_elgamal/convert.rs index 2d8ee19557..c63ebd71e0 100644 --- a/zk-token-sdk/src/zk_token_elgamal/convert.rs +++ b/zk-token-sdk/src/zk_token_elgamal/convert.rs @@ -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 for pod::AggregatedValidityProof { + fn from(proof: AggregatedValidityProof) -> Self { + Self(proof.to_bytes()) + } + } + + impl TryFrom for AggregatedValidityProof { + type Error = ValidityProofError; + + fn try_from(pod: pod::AggregatedValidityProof) -> Result { + Self::from_bytes(&pod.0) + } + } + impl From for pod::ZeroBalanceProof { fn from(proof: ZeroBalanceProof) -> Self { Self(proof.to_bytes()) diff --git a/zk-token-sdk/src/zk_token_elgamal/pod.rs b/zk-token-sdk/src/zk_token_elgamal/pod.rs index 4cd650db60..0d820f8593 100644 --- a/zk-token-sdk/src/zk_token_elgamal/pod.rs +++ b/zk-token-sdk/src/zk_token_elgamal/pod.rs @@ -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)]