From 3426f7ec0a60735b40d1ef3b8a8f63fea66dde84 Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Thu, 18 Apr 2024 17:44:26 +0900 Subject: [PATCH] [zk-token-sdk] Add ciphertext validity proof with 3 handles (#809) * refactor ciphertext validity sigma proofs for 2 handles * add ciphertext validity sigma proofs for 3 handles * add pod types for ciphertext validity sigma proofs for 3 handles * add `allow(dead_code)` temporarily * cargo clippy * Update zk-token-sdk/src/zk_token_elgamal/pod/sigma_proofs.rs Co-authored-by: Jon C --------- Co-authored-by: Jon C --- .../handles_2.rs} | 0 .../handles_3.rs | 196 ++++++++ .../mod.rs | 7 + .../handles_2.rs} | 0 .../handles_3.rs | 450 ++++++++++++++++++ .../grouped_ciphertext_validity_proof/mod.rs | 7 + zk-token-sdk/src/zk_token_elgamal/pod/mod.rs | 2 +- .../src/zk_token_elgamal/pod/sigma_proofs.rs | 70 +++ 8 files changed, 731 insertions(+), 1 deletion(-) rename zk-token-sdk/src/sigma_proofs/{batched_grouped_ciphertext_validity_proof.rs => batched_grouped_ciphertext_validity_proof/handles_2.rs} (100%) create mode 100644 zk-token-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity_proof/handles_3.rs create mode 100644 zk-token-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity_proof/mod.rs rename zk-token-sdk/src/sigma_proofs/{grouped_ciphertext_validity_proof.rs => grouped_ciphertext_validity_proof/handles_2.rs} (100%) create mode 100644 zk-token-sdk/src/sigma_proofs/grouped_ciphertext_validity_proof/handles_3.rs create mode 100644 zk-token-sdk/src/sigma_proofs/grouped_ciphertext_validity_proof/mod.rs diff --git a/zk-token-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity_proof.rs b/zk-token-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity_proof/handles_2.rs similarity index 100% rename from zk-token-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity_proof.rs rename to zk-token-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity_proof/handles_2.rs diff --git a/zk-token-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity_proof/handles_3.rs b/zk-token-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity_proof/handles_3.rs new file mode 100644 index 000000000..2faaa81c3 --- /dev/null +++ b/zk-token-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity_proof/handles_3.rs @@ -0,0 +1,196 @@ +//! The batched ciphertext with 3 handles validity sigma proof system. +//! +//! A batched grouped ciphertext validity proof certifies the validity of two instances of a +//! standard grouped ciphertext validity proof. An instance of a standard grouped ciphertext +//! with 3 handles validity proof consists of one ciphertext and three decryption handles: +//! `(commitment, source_handle, destination_handle, auditor_handle)`. An instance of a batched +//! grouped ciphertext with 3 handles validity proof consist of a pair of `(commitment_0, +//! source_handle_0, destination_handle_0, auditor_handle_0)` and `(commitment_1, source_handle_1, +//! destination_handle_1, auditor_handle_1)`. The proof certifies the anagolous decryptable +//! properties for each one of these pairs of commitment and decryption handles. +//! +//! The protocol guarantees computational soundness (by the hardness of discrete log) and perfect +//! zero-knowledge in the random oracle model. + +#[cfg(not(target_os = "solana"))] +use crate::encryption::{ + elgamal::{DecryptHandle, ElGamalPubkey}, + pedersen::{PedersenCommitment, PedersenOpening}, +}; +use { + crate::{ + sigma_proofs::{ + errors::ValidityProofVerificationError, + grouped_ciphertext_validity_proof::GroupedCiphertext3HandlesValidityProof, + }, + transcript::TranscriptProtocol, + UNIT_LEN, + }, + curve25519_dalek::scalar::Scalar, + merlin::Transcript, +}; + +/// Byte length of a batched grouped ciphertext validity proof for 3 handles +#[allow(dead_code)] +const BATCHED_GROUPED_CIPHERTEXT_3_HANDLES_VALIDITY_PROOF_LEN: usize = UNIT_LEN * 6; + +/// Batched grouped ciphertext validity proof with two handles. +#[allow(non_snake_case)] +#[derive(Clone)] +pub struct BatchedGroupedCiphertext3HandlesValidityProof(GroupedCiphertext3HandlesValidityProof); + +#[allow(non_snake_case)] +#[allow(dead_code)] +#[cfg(not(target_os = "solana"))] +impl BatchedGroupedCiphertext3HandlesValidityProof { + /// Creates a batched grouped ciphertext validity proof. + /// + /// The function simply batches the input openings and invokes the standard grouped ciphertext + /// validity proof constructor. + pub fn new>( + source_pubkey: &ElGamalPubkey, + destination_pubkey: &ElGamalPubkey, + auditor_pubkey: &ElGamalPubkey, + amount_lo: T, + amount_hi: T, + opening_lo: &PedersenOpening, + opening_hi: &PedersenOpening, + transcript: &mut Transcript, + ) -> Self { + transcript.batched_grouped_ciphertext_validity_proof_domain_separator(); + + let t = transcript.challenge_scalar(b"t"); + + let batched_message = amount_lo.into() + amount_hi.into() * t; + let batched_opening = opening_lo + &(opening_hi * &t); + + BatchedGroupedCiphertext3HandlesValidityProof(GroupedCiphertext3HandlesValidityProof::new( + source_pubkey, + destination_pubkey, + auditor_pubkey, + batched_message, + &batched_opening, + transcript, + )) + } + + /// Verifies a batched grouped ciphertext validity proof. + /// + /// 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. + #[allow(clippy::too_many_arguments)] + pub fn verify( + self, + source_pubkey: &ElGamalPubkey, + destination_pubkey: &ElGamalPubkey, + auditor_pubkey: &ElGamalPubkey, + commitment_lo: &PedersenCommitment, + commitment_hi: &PedersenCommitment, + source_handle_lo: &DecryptHandle, + source_handle_hi: &DecryptHandle, + destination_handle_lo: &DecryptHandle, + destination_handle_hi: &DecryptHandle, + auditor_handle_lo: &DecryptHandle, + auditor_handle_hi: &DecryptHandle, + transcript: &mut Transcript, + ) -> Result<(), ValidityProofVerificationError> { + transcript.batched_grouped_ciphertext_validity_proof_domain_separator(); + + let t = transcript.challenge_scalar(b"t"); + + let batched_commitment = commitment_lo + commitment_hi * t; + let source_batched_handle = source_handle_lo + source_handle_hi * t; + let destination_batched_handle = destination_handle_lo + destination_handle_hi * t; + let auditor_batched_handle = auditor_handle_lo + auditor_handle_hi * t; + + let BatchedGroupedCiphertext3HandlesValidityProof(validity_proof) = self; + + validity_proof.verify( + &batched_commitment, + source_pubkey, + destination_pubkey, + auditor_pubkey, + &source_batched_handle, + &destination_batched_handle, + &auditor_batched_handle, + transcript, + ) + } + + pub fn to_bytes(&self) -> [u8; BATCHED_GROUPED_CIPHERTEXT_3_HANDLES_VALIDITY_PROOF_LEN] { + self.0.to_bytes() + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + GroupedCiphertext3HandlesValidityProof::from_bytes(bytes).map(Self) + } +} + +#[cfg(test)] +mod test { + use { + super::*, + crate::encryption::{elgamal::ElGamalKeypair, pedersen::Pedersen}, + }; + + #[test] + fn test_batched_grouped_ciphertext_validity_proof() { + let source_keypair = ElGamalKeypair::new_rand(); + let source_pubkey = source_keypair.pubkey(); + + let destination_keypair = ElGamalKeypair::new_rand(); + let destination_pubkey = destination_keypair.pubkey(); + + let auditor_keypair = ElGamalKeypair::new_rand(); + let auditor_pubkey = auditor_keypair.pubkey(); + + 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 source_handle_lo = source_pubkey.decrypt_handle(&open_lo); + let source_handle_hi = source_pubkey.decrypt_handle(&open_hi); + + let destination_handle_lo = destination_pubkey.decrypt_handle(&open_lo); + let destination_handle_hi = destination_pubkey.decrypt_handle(&open_hi); + + let auditor_handle_lo = auditor_pubkey.decrypt_handle(&open_lo); + let auditor_handle_hi = auditor_pubkey.decrypt_handle(&open_hi); + + let mut prover_transcript = Transcript::new(b"Test"); + let mut verifier_transcript = Transcript::new(b"Test"); + + let proof = BatchedGroupedCiphertext3HandlesValidityProof::new( + source_pubkey, + destination_pubkey, + auditor_pubkey, + amount_lo, + amount_hi, + &open_lo, + &open_hi, + &mut prover_transcript, + ); + + assert!(proof + .verify( + source_pubkey, + destination_pubkey, + auditor_pubkey, + &commitment_lo, + &commitment_hi, + &source_handle_lo, + &source_handle_hi, + &destination_handle_lo, + &destination_handle_hi, + &auditor_handle_lo, + &auditor_handle_hi, + &mut verifier_transcript, + ) + .is_ok()); + } +} diff --git a/zk-token-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity_proof/mod.rs b/zk-token-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity_proof/mod.rs new file mode 100644 index 000000000..0c8f9cecd --- /dev/null +++ b/zk-token-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity_proof/mod.rs @@ -0,0 +1,7 @@ +mod handles_2; +mod handles_3; + +pub use { + handles_2::BatchedGroupedCiphertext2HandlesValidityProof, + handles_3::BatchedGroupedCiphertext3HandlesValidityProof, +}; diff --git a/zk-token-sdk/src/sigma_proofs/grouped_ciphertext_validity_proof.rs b/zk-token-sdk/src/sigma_proofs/grouped_ciphertext_validity_proof/handles_2.rs similarity index 100% rename from zk-token-sdk/src/sigma_proofs/grouped_ciphertext_validity_proof.rs rename to zk-token-sdk/src/sigma_proofs/grouped_ciphertext_validity_proof/handles_2.rs diff --git a/zk-token-sdk/src/sigma_proofs/grouped_ciphertext_validity_proof/handles_3.rs b/zk-token-sdk/src/sigma_proofs/grouped_ciphertext_validity_proof/handles_3.rs new file mode 100644 index 000000000..132429231 --- /dev/null +++ b/zk-token-sdk/src/sigma_proofs/grouped_ciphertext_validity_proof/handles_3.rs @@ -0,0 +1,450 @@ +//! The grouped ciphertext with 3 handles validity sigma proof system. +//! +//! This ciphertext validity proof is defined with respect to a Pedersen commitment and three +//! decryption handles. The proof certifies that a given Pedersen commitment can be decrypted using +//! ElGamal private keys that are associated with each of the three 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. +//! +//! In accordance with its application to the SPL Token program, the first decryption handle +//! associated with the proof is referred to as the "source" handle, the second as the +//! "destination" handle, and the third as the "auditor" handle. + +#[cfg(not(target_os = "solana"))] +use { + crate::{ + encryption::{ + elgamal::{DecryptHandle, ElGamalPubkey}, + pedersen::{PedersenCommitment, PedersenOpening, G, H}, + }, + sigma_proofs::{canonical_scalar_from_optional_slice, ristretto_point_from_optional_slice}, + UNIT_LEN, + }, + curve25519_dalek::traits::MultiscalarMul, + rand::rngs::OsRng, + zeroize::Zeroize, +}; +use { + crate::{ + sigma_proofs::errors::{SigmaProofVerificationError, ValidityProofVerificationError}, + transcript::TranscriptProtocol, + }, + curve25519_dalek::{ + ristretto::{CompressedRistretto, RistrettoPoint}, + scalar::Scalar, + traits::{IsIdentity, VartimeMultiscalarMul}, + }, + merlin::Transcript, +}; + +/// Byte length of a grouped ciphertext validity proof for 3 handles +const GROUPED_CIPHERTEXT_3_HANDLES_VALIDITY_PROOF_LEN: usize = UNIT_LEN * 6; + +/// The grouped ciphertext validity proof for 3 handles. +/// +/// Contains all the elliptic curve and scalar components that make up the sigma protocol. +#[allow(non_snake_case)] +#[derive(Clone)] +pub struct GroupedCiphertext3HandlesValidityProof { + Y_0: CompressedRistretto, + Y_1: CompressedRistretto, + Y_2: CompressedRistretto, + Y_3: CompressedRistretto, + z_r: Scalar, + z_x: Scalar, +} + +#[allow(non_snake_case)] +#[cfg(not(target_os = "solana"))] +impl GroupedCiphertext3HandlesValidityProof { + /// Creates a grouped ciphertext with 3 handles validity proof. + /// + /// 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. + /// + /// * `source_pubkey` - The source ElGamal public key + /// * `destination_pubkey` - The destination ElGamal public key + /// * `auditor_pubkey` - The auditor ElGamal public key + /// * `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>( + source_pubkey: &ElGamalPubkey, + destination_pubkey: &ElGamalPubkey, + auditor_pubkey: &ElGamalPubkey, + amount: T, + opening: &PedersenOpening, + transcript: &mut Transcript, + ) -> Self { + transcript.grouped_ciphertext_validity_proof_domain_separator(); + + // extract the relevant scalar and Ristretto points from the inputs + let P_source = source_pubkey.get_point(); + let P_destination = destination_pubkey.get_point(); + let P_auditor = auditor_pubkey.get_point(); + + let x = amount.into(); + let r = opening.get_scalar(); + + // 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_source).compress(); + let Y_2 = (&y_r * P_destination).compress(); + let Y_3 = (&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); + transcript.append_point(b"Y_3", &Y_3); + + let c = transcript.challenge_scalar(b"c"); + transcript.challenge_scalar(b"w"); + + // compute masked message and opening + let z_r = &(&c * r) + &y_r; + let z_x = &(&c * &x) + &y_x; + + y_r.zeroize(); + y_x.zeroize(); + + Self { + Y_0, + Y_1, + Y_2, + Y_3, + z_r, + z_x, + } + } + + /// Verifies a grouped ciphertext with 3 handles validity proof. + /// + /// * `commitment` - The Pedersen commitment + /// * `source_pubkey` - The source ElGamal public key + /// * `destination_pubkey` - The destination ElGamal public key + /// * `auditor_pubkey` - The auditor ElGamal public key + /// * `source_handle` - The source decryption handle + /// * `destination_handle` - The destination decryption handle + /// * `auditor_handle` - The auditor decryption handle + /// * `transcript` - The transcript that does the bookkeeping for the Fiat-Shamir heuristic + pub fn verify( + self, + commitment: &PedersenCommitment, + source_pubkey: &ElGamalPubkey, + destination_pubkey: &ElGamalPubkey, + auditor_pubkey: &ElGamalPubkey, + source_handle: &DecryptHandle, + destination_handle: &DecryptHandle, + auditor_handle: &DecryptHandle, + transcript: &mut Transcript, + ) -> Result<(), ValidityProofVerificationError> { + transcript.grouped_ciphertext_validity_proof_domain_separator(); + + // include `Y_0`, `Y_1`, `Y_2` to transcript and extract challenges + transcript.validate_and_append_point(b"Y_0", &self.Y_0)?; + transcript.validate_and_append_point(b"Y_1", &self.Y_1)?; + transcript.validate_and_append_point(b"Y_2", &self.Y_2)?; + // the point `Y_3` is defined with respect to the auditor public key and can be zero if the + // auditor public key is zero + transcript.append_point(b"Y_3", &self.Y_3); + + let c = transcript.challenge_scalar(b"c"); + let w = transcript.challenge_scalar(b"w"); + let ww = &w * &w; + let www = &w * &ww; + + let w_negated = -&w; + let ww_negated = -&ww; + let www_negated = -&www; + + // check the required algebraic conditions + let Y_0 = self + .Y_0 + .decompress() + .ok_or(SigmaProofVerificationError::Deserialization)?; + let Y_1 = self + .Y_1 + .decompress() + .ok_or(SigmaProofVerificationError::Deserialization)?; + let Y_2 = self + .Y_2 + .decompress() + .ok_or(SigmaProofVerificationError::Deserialization)?; + let Y_3 = self + .Y_3 + .decompress() + .ok_or(SigmaProofVerificationError::Deserialization)?; + + let P_source = source_pubkey.get_point(); + let P_destination = destination_pubkey.get_point(); + let P_auditor = auditor_pubkey.get_point(); + + let C = commitment.get_point(); + let D_source = source_handle.get_point(); + let D_destination = destination_handle.get_point(); + let D_auditor = auditor_handle.get_point(); + + let check = RistrettoPoint::vartime_multiscalar_mul( + vec![ + &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 + &(&www * &self.z_r), // www * z_r + &(&www_negated * &c), // -www * c + &www_negated, // -www + ], + vec![ + &(*H), // H + &(*G), // G + C, // C + &Y_0, // Y_0 + P_source, // P_destination + D_source, // D_destination + &Y_1, // Y_1 + P_destination, // P_destination + D_destination, // D_destination + &Y_2, // Y_1 + P_auditor, // P_auditor + D_auditor, // D_auditor + &Y_3, // Y_2 + ], + ); + + if check.is_identity() { + Ok(()) + } else { + Err(SigmaProofVerificationError::AlgebraicRelation.into()) + } + } + + pub fn to_bytes(&self) -> [u8; GROUPED_CIPHERTEXT_3_HANDLES_VALIDITY_PROOF_LEN] { + let mut buf = [0_u8; GROUPED_CIPHERTEXT_3_HANDLES_VALIDITY_PROOF_LEN]; + let mut chunks = buf.chunks_mut(UNIT_LEN); + chunks.next().unwrap().copy_from_slice(self.Y_0.as_bytes()); + chunks.next().unwrap().copy_from_slice(self.Y_1.as_bytes()); + chunks.next().unwrap().copy_from_slice(self.Y_2.as_bytes()); + chunks.next().unwrap().copy_from_slice(self.Y_3.as_bytes()); + chunks.next().unwrap().copy_from_slice(self.z_r.as_bytes()); + chunks.next().unwrap().copy_from_slice(self.z_x.as_bytes()); + buf + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + let mut chunks = bytes.chunks(UNIT_LEN); + let Y_0 = ristretto_point_from_optional_slice(chunks.next())?; + let Y_1 = ristretto_point_from_optional_slice(chunks.next())?; + let Y_2 = ristretto_point_from_optional_slice(chunks.next())?; + let Y_3 = ristretto_point_from_optional_slice(chunks.next())?; + let z_r = canonical_scalar_from_optional_slice(chunks.next())?; + let z_x = canonical_scalar_from_optional_slice(chunks.next())?; + + Ok(GroupedCiphertext3HandlesValidityProof { + Y_0, + Y_1, + Y_2, + Y_3, + z_r, + z_x, + }) + } +} + +#[cfg(test)] +mod test { + use { + super::*, + crate::encryption::{elgamal::ElGamalKeypair, pedersen::Pedersen}, + }; + + #[test] + fn test_grouped_ciphertext_3_handles_validity_proof_correctness() { + let source_keypair = ElGamalKeypair::new_rand(); + let source_pubkey = source_keypair.pubkey(); + + let destination_keypair = ElGamalKeypair::new_rand(); + let destination_pubkey = destination_keypair.pubkey(); + + let auditor_keypair = ElGamalKeypair::new_rand(); + let auditor_pubkey = auditor_keypair.pubkey(); + + let amount: u64 = 55; + let (commitment, opening) = Pedersen::new(amount); + + let source_handle = source_pubkey.decrypt_handle(&opening); + let destination_handle = destination_pubkey.decrypt_handle(&opening); + let auditor_handle = auditor_pubkey.decrypt_handle(&opening); + + let mut prover_transcript = Transcript::new(b"Test"); + let mut verifier_transcript = Transcript::new(b"Test"); + + let proof = GroupedCiphertext3HandlesValidityProof::new( + source_pubkey, + destination_pubkey, + auditor_pubkey, + amount, + &opening, + &mut prover_transcript, + ); + + assert!(proof + .verify( + &commitment, + source_pubkey, + destination_pubkey, + auditor_pubkey, + &source_handle, + &destination_handle, + &auditor_handle, + &mut verifier_transcript, + ) + .is_ok()); + } + + #[test] + fn test_grouped_ciphertext_3_handles_validity_proof_edge_cases() { + // if source or destination public key zeroed, then the proof should always reject + let source_pubkey = ElGamalPubkey::try_from([0u8; 32].as_slice()).unwrap(); + let destination_pubkey = ElGamalPubkey::try_from([0u8; 32].as_slice()).unwrap(); + + let auditor_keypair = ElGamalKeypair::new_rand(); + let auditor_pubkey = auditor_keypair.pubkey(); + + let amount: u64 = 55; + let (commitment, opening) = Pedersen::new(amount); + + let source_handle = destination_pubkey.decrypt_handle(&opening); + let destination_handle = destination_pubkey.decrypt_handle(&opening); + let auditor_handle = auditor_pubkey.decrypt_handle(&opening); + + let mut prover_transcript = Transcript::new(b"Test"); + let mut verifier_transcript = Transcript::new(b"Test"); + + let proof = GroupedCiphertext3HandlesValidityProof::new( + &source_pubkey, + &destination_pubkey, + auditor_pubkey, + amount, + &opening, + &mut prover_transcript, + ); + + assert!(proof + .verify( + &commitment, + &source_pubkey, + &destination_pubkey, + auditor_pubkey, + &source_handle, + &destination_handle, + &auditor_handle, + &mut verifier_transcript, + ) + .is_err()); + + // all zeroed ciphertext should still be valid + let source_keypair = ElGamalKeypair::new_rand(); + let source_pubkey = source_keypair.pubkey(); + + let destination_keypair = ElGamalKeypair::new_rand(); + let destination_pubkey = destination_keypair.pubkey(); + + let auditor_keypair = ElGamalKeypair::new_rand(); + let auditor_pubkey = auditor_keypair.pubkey(); + + let amount: u64 = 0; + let commitment = PedersenCommitment::from_bytes(&[0u8; 32]).unwrap(); + let opening = PedersenOpening::from_bytes(&[0u8; 32]).unwrap(); + + let source_handle = source_pubkey.decrypt_handle(&opening); + let destination_handle = destination_pubkey.decrypt_handle(&opening); + let auditor_handle = auditor_pubkey.decrypt_handle(&opening); + + let mut prover_transcript = Transcript::new(b"Test"); + let mut verifier_transcript = Transcript::new(b"Test"); + + let proof = GroupedCiphertext3HandlesValidityProof::new( + source_pubkey, + destination_pubkey, + auditor_pubkey, + amount, + &opening, + &mut prover_transcript, + ); + + assert!(proof + .verify( + &commitment, + source_pubkey, + destination_pubkey, + auditor_pubkey, + &source_handle, + &destination_handle, + &auditor_handle, + &mut verifier_transcript, + ) + .is_ok()); + + // decryption handles can be zero as long as the Pedersen commitment is valid + let source_keypair = ElGamalKeypair::new_rand(); + let source_pubkey = source_keypair.pubkey(); + + let destination_keypair = ElGamalKeypair::new_rand(); + let destination_pubkey = destination_keypair.pubkey(); + + let auditor_keypair = ElGamalKeypair::new_rand(); + let auditor_pubkey = auditor_keypair.pubkey(); + + let amount: u64 = 55; + let zeroed_opening = PedersenOpening::default(); + + let commitment = Pedersen::with(amount, &zeroed_opening); + + let source_handle = source_pubkey.decrypt_handle(&zeroed_opening); + let destination_handle = destination_pubkey.decrypt_handle(&zeroed_opening); + let auditor_handle = auditor_pubkey.decrypt_handle(&zeroed_opening); + + let mut prover_transcript = Transcript::new(b"Test"); + let mut verifier_transcript = Transcript::new(b"Test"); + + let proof = GroupedCiphertext3HandlesValidityProof::new( + source_pubkey, + destination_pubkey, + auditor_pubkey, + amount, + &opening, + &mut prover_transcript, + ); + + assert!(proof + .verify( + &commitment, + source_pubkey, + destination_pubkey, + auditor_pubkey, + &source_handle, + &destination_handle, + &auditor_handle, + &mut verifier_transcript, + ) + .is_ok()); + } +} diff --git a/zk-token-sdk/src/sigma_proofs/grouped_ciphertext_validity_proof/mod.rs b/zk-token-sdk/src/sigma_proofs/grouped_ciphertext_validity_proof/mod.rs new file mode 100644 index 000000000..8b6555ce1 --- /dev/null +++ b/zk-token-sdk/src/sigma_proofs/grouped_ciphertext_validity_proof/mod.rs @@ -0,0 +1,7 @@ +mod handles_2; +mod handles_3; + +pub use { + handles_2::GroupedCiphertext2HandlesValidityProof, + handles_3::GroupedCiphertext3HandlesValidityProof, +}; diff --git a/zk-token-sdk/src/zk_token_elgamal/pod/mod.rs b/zk-token-sdk/src/zk_token_elgamal/pod/mod.rs index d782672c8..e5a8d9cd7 100644 --- a/zk-token-sdk/src/zk_token_elgamal/pod/mod.rs +++ b/zk-token-sdk/src/zk_token_elgamal/pod/mod.rs @@ -23,7 +23,7 @@ pub use { sigma_proofs::{ BatchedGroupedCiphertext2HandlesValidityProof, CiphertextCiphertextEqualityProof, CiphertextCommitmentEqualityProof, FeeSigmaProof, GroupedCiphertext2HandlesValidityProof, - PubkeyValidityProof, ZeroBalanceProof, + GroupedCiphertext3HandlesValidityProof, PubkeyValidityProof, ZeroBalanceProof, }, }; diff --git a/zk-token-sdk/src/zk_token_elgamal/pod/sigma_proofs.rs b/zk-token-sdk/src/zk_token_elgamal/pod/sigma_proofs.rs index 0bfc4cfad..f0f43e662 100644 --- a/zk-token-sdk/src/zk_token_elgamal/pod/sigma_proofs.rs +++ b/zk-token-sdk/src/zk_token_elgamal/pod/sigma_proofs.rs @@ -3,10 +3,12 @@ #[cfg(not(target_os = "solana"))] use crate::sigma_proofs::{ batched_grouped_ciphertext_validity_proof::BatchedGroupedCiphertext2HandlesValidityProof as DecodedBatchedGroupedCiphertext2HandlesValidityProof, + batched_grouped_ciphertext_validity_proof::BatchedGroupedCiphertext3HandlesValidityProof as DecodedBatchedGroupedCiphertext3HandlesValidityProof, ciphertext_ciphertext_equality_proof::CiphertextCiphertextEqualityProof as DecodedCiphertextCiphertextEqualityProof, ciphertext_commitment_equality_proof::CiphertextCommitmentEqualityProof as DecodedCiphertextCommitmentEqualityProof, errors::*, fee_proof::FeeSigmaProof as DecodedFeeSigmaProof, grouped_ciphertext_validity_proof::GroupedCiphertext2HandlesValidityProof as DecodedGroupedCiphertext2HandlesValidityProof, + grouped_ciphertext_validity_proof::GroupedCiphertext3HandlesValidityProof as DecodedGroupedCiphertext3HandlesValidityProof, pubkey_proof::PubkeyValidityProof as DecodedPubkeyValidityProof, zero_balance_proof::ZeroBalanceProof as DecodedZeroBalanceProof, }; @@ -21,9 +23,15 @@ const CIPHERTEXT_CIPHERTEXT_EQUALITY_PROOF_LEN: usize = 224; /// Byte length of a grouped ciphertext for 2 handles validity proof const GROUPED_CIPHERTEXT_2_HANDLES_VALIDITY_PROOF_LEN: usize = 160; +/// Byte length of a grouped ciphertext for 3 handles validity proof +const GROUPED_CIPHERTEXT_3_HANDLES_VALIDITY_PROOF_LEN: usize = 192; + /// Byte length of a batched grouped ciphertext for 2 handles validity proof const BATCHED_GROUPED_CIPHERTEXT_2_HANDLES_VALIDITY_PROOF_LEN: usize = 160; +/// Byte length of a batched grouped ciphertext for 3 handles validity proof +const BATCHED_GROUPED_CIPHERTEXT_3_HANDLES_VALIDITY_PROOF_LEN: usize = 192; + /// Byte length of a zero-balance proof const ZERO_BALANCE_PROOF_LEN: usize = 96; @@ -102,6 +110,33 @@ impl TryFrom } } +/// The `GroupedCiphertext3HandlesValidityProof` type as a `Pod`. +#[derive(Clone, Copy)] +#[repr(transparent)] +pub struct GroupedCiphertext3HandlesValidityProof( + pub [u8; GROUPED_CIPHERTEXT_3_HANDLES_VALIDITY_PROOF_LEN], +); + +#[cfg(not(target_os = "solana"))] +impl From + for GroupedCiphertext3HandlesValidityProof +{ + fn from(decoded_proof: DecodedGroupedCiphertext3HandlesValidityProof) -> Self { + Self(decoded_proof.to_bytes()) + } +} + +#[cfg(not(target_os = "solana"))] +impl TryFrom + for DecodedGroupedCiphertext3HandlesValidityProof +{ + type Error = ValidityProofVerificationError; + + fn try_from(pod_proof: GroupedCiphertext3HandlesValidityProof) -> Result { + Self::from_bytes(&pod_proof.0) + } +} + /// The `BatchedGroupedCiphertext2HandlesValidityProof` type as a `Pod`. #[derive(Clone, Copy)] #[repr(transparent)] @@ -131,6 +166,35 @@ impl TryFrom } } +/// The `BatchedGroupedCiphertext3HandlesValidityProof` type as a `Pod`. +#[derive(Clone, Copy)] +#[repr(transparent)] +pub struct BatchedGroupedCiphertext3HandlesValidityProof( + pub [u8; BATCHED_GROUPED_CIPHERTEXT_3_HANDLES_VALIDITY_PROOF_LEN], +); + +#[cfg(not(target_os = "solana"))] +impl From + for BatchedGroupedCiphertext3HandlesValidityProof +{ + fn from(decoded_proof: DecodedBatchedGroupedCiphertext3HandlesValidityProof) -> Self { + Self(decoded_proof.to_bytes()) + } +} + +#[cfg(not(target_os = "solana"))] +impl TryFrom + for DecodedBatchedGroupedCiphertext3HandlesValidityProof +{ + type Error = ValidityProofVerificationError; + + fn try_from( + pod_proof: BatchedGroupedCiphertext3HandlesValidityProof, + ) -> Result { + Self::from_bytes(&pod_proof.0) + } +} + /// The `ZeroBalanceProof` type as a `Pod`. #[derive(Clone, Copy)] #[repr(transparent)] @@ -206,8 +270,14 @@ unsafe impl Pod for CiphertextCiphertextEqualityProof {} unsafe impl Zeroable for GroupedCiphertext2HandlesValidityProof {} unsafe impl Pod for GroupedCiphertext2HandlesValidityProof {} +unsafe impl Zeroable for GroupedCiphertext3HandlesValidityProof {} +unsafe impl Pod for GroupedCiphertext3HandlesValidityProof {} + unsafe impl Zeroable for BatchedGroupedCiphertext2HandlesValidityProof {} unsafe impl Pod for BatchedGroupedCiphertext2HandlesValidityProof {} +unsafe impl Zeroable for BatchedGroupedCiphertext3HandlesValidityProof {} +unsafe impl Pod for BatchedGroupedCiphertext3HandlesValidityProof {} + unsafe impl Zeroable for ZeroBalanceProof {} unsafe impl Pod for ZeroBalanceProof {}