From 2a1d4f937aa3a96db55a0c14b92c2453dc10045d Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Fri, 26 Apr 2024 09:58:41 +0900 Subject: [PATCH 1/4] add `sigma_proofs` and `transcript` modules --- Cargo.lock | 1 + zk-sdk/Cargo.toml | 1 + zk-sdk/src/errors.rs | 6 + zk-sdk/src/lib.rs | 2 + .../handles_2.rs | 158 +++++ .../handles_3.rs | 196 ++++++ .../mod.rs | 7 + .../ciphertext_ciphertext_equality_proof.rs | 341 +++++++++ .../ciphertext_commitment_equality_proof.rs | 427 ++++++++++++ zk-sdk/src/sigma_proofs/errors.rs | 49 ++ zk-sdk/src/sigma_proofs/fee_proof.rs | 656 ++++++++++++++++++ .../handles_2.rs | 372 ++++++++++ .../handles_3.rs | 450 ++++++++++++ .../grouped_ciphertext_validity_proof/mod.rs | 7 + zk-sdk/src/sigma_proofs/mod.rs | 58 ++ zk-sdk/src/sigma_proofs/pubkey_proof.rs | 171 +++++ zk-sdk/src/sigma_proofs/zero_balance_proof.rs | 302 ++++++++ zk-sdk/src/transcript.rs | 143 ++++ 18 files changed, 3347 insertions(+) create mode 100644 zk-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity_proof/handles_2.rs create mode 100644 zk-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity_proof/handles_3.rs create mode 100644 zk-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity_proof/mod.rs create mode 100644 zk-sdk/src/sigma_proofs/ciphertext_ciphertext_equality_proof.rs create mode 100644 zk-sdk/src/sigma_proofs/ciphertext_commitment_equality_proof.rs create mode 100644 zk-sdk/src/sigma_proofs/errors.rs create mode 100644 zk-sdk/src/sigma_proofs/fee_proof.rs create mode 100644 zk-sdk/src/sigma_proofs/grouped_ciphertext_validity_proof/handles_2.rs create mode 100644 zk-sdk/src/sigma_proofs/grouped_ciphertext_validity_proof/handles_3.rs create mode 100644 zk-sdk/src/sigma_proofs/grouped_ciphertext_validity_proof/mod.rs create mode 100644 zk-sdk/src/sigma_proofs/mod.rs create mode 100644 zk-sdk/src/sigma_proofs/pubkey_proof.rs create mode 100644 zk-sdk/src/sigma_proofs/zero_balance_proof.rs create mode 100644 zk-sdk/src/transcript.rs diff --git a/Cargo.lock b/Cargo.lock index 21e35435fe4e87..eb29264628c828 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7703,6 +7703,7 @@ dependencies = [ "curve25519-dalek", "itertools", "lazy_static", + "merlin", "rand 0.7.3", "serde", "serde_json", diff --git a/zk-sdk/Cargo.toml b/zk-sdk/Cargo.toml index 347ad4f3d9cae6..34d817425b2536 100644 --- a/zk-sdk/Cargo.toml +++ b/zk-sdk/Cargo.toml @@ -23,6 +23,7 @@ bincode = { workspace = true } curve25519-dalek = { workspace = true, features = ["serde"] } itertools = { workspace = true } lazy_static = { workspace = true } +merlin = { workspace = true } rand = { version = "0.7" } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } diff --git a/zk-sdk/src/errors.rs b/zk-sdk/src/errors.rs index 5a7690fe436094..fcd8e248cb9f78 100644 --- a/zk-sdk/src/errors.rs +++ b/zk-sdk/src/errors.rs @@ -30,3 +30,9 @@ pub enum ElGamalError { #[error("failed to deserialize secret key")] SecretKeyDeserialization, } + +#[derive(Error, Clone, Debug, Eq, PartialEq)] +pub enum TranscriptError { + #[error("point is the identity")] + ValidationError, +} diff --git a/zk-sdk/src/lib.rs b/zk-sdk/src/lib.rs index 699a3a8085bb90..d25df67c6e085d 100644 --- a/zk-sdk/src/lib.rs +++ b/zk-sdk/src/lib.rs @@ -23,6 +23,8 @@ pub mod encryption; pub mod errors; +mod sigma_proofs; +mod transcript; /// Byte length of a compressed Ristretto point or scalar in Curve255519 const UNIT_LEN: usize = 32; diff --git a/zk-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity_proof/handles_2.rs b/zk-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity_proof/handles_2.rs new file mode 100644 index 00000000000000..7fbe17fb195ddd --- /dev/null +++ b/zk-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity_proof/handles_2.rs @@ -0,0 +1,158 @@ +//! 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_os = "solana"))] +use crate::encryption::{ + elgamal::{DecryptHandle, ElGamalPubkey}, + pedersen::{PedersenCommitment, PedersenOpening}, +}; +use { + crate::{ + sigma_proofs::{ + errors::ValidityProofVerificationError, + grouped_ciphertext_validity_proof::GroupedCiphertext2HandlesValidityProof, + }, + transcript::TranscriptProtocol, + }, + curve25519_dalek::scalar::Scalar, + merlin::Transcript, +}; + +/// Batched grouped ciphertext validity proof with two handles. +/// +/// A batched grouped ciphertext validity proof certifies the validity of two instances of a +/// standard ciphertext validity proof. An instance of a standard validity proof consists of one +/// ciphertext and two decryption handles: `(commitment, destination_handle, auditor_handle)`. An +/// instance of a batched ciphertext validity proof is a pair `(commitment_0, +/// destination_handle_0, auditor_handle_0)` and `(commitment_1, destination_handle_1, +/// auditor_handle_1)`. The proof certifies the analogous decryptable properties for each one of +/// these pairs of commitment and decryption handles. +#[allow(non_snake_case)] +#[derive(Clone)] +pub struct BatchedGroupedCiphertext2HandlesValidityProof(GroupedCiphertext2HandlesValidityProof); + +#[allow(non_snake_case)] +#[cfg(not(target_os = "solana"))] +impl BatchedGroupedCiphertext2HandlesValidityProof { + /// Creates a batched grouped ciphertext validity proof. + /// + /// The function simply batches the input openings and invokes the standard grouped ciphertext + /// validity proof constructor. + /// + /// This function is randomized. It uses `OsRng` internally to generate random scalars. + pub fn new>( + (destination_pubkey, auditor_pubkey): (&ElGamalPubkey, &ElGamalPubkey), + (amount_lo, amount_hi): (T, T), + (opening_lo, opening_hi): (&PedersenOpening, &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); + + BatchedGroupedCiphertext2HandlesValidityProof(GroupedCiphertext2HandlesValidityProof::new( + (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. + pub fn verify( + self, + (destination_pubkey, auditor_pubkey): (&ElGamalPubkey, &ElGamalPubkey), + (commitment_lo, commitment_hi): (&PedersenCommitment, &PedersenCommitment), + (destination_handle_lo, destination_handle_hi): (&DecryptHandle, &DecryptHandle), + (auditor_handle_lo, auditor_handle_hi): (&DecryptHandle, &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 destination_batched_handle = destination_handle_lo + destination_handle_hi * t; + let auditor_batched_handle = auditor_handle_lo + auditor_handle_hi * t; + + let BatchedGroupedCiphertext2HandlesValidityProof(validity_proof) = self; + + validity_proof.verify( + &batched_commitment, + (destination_pubkey, auditor_pubkey), + (&destination_batched_handle, &auditor_batched_handle), + transcript, + ) + } + + pub fn to_bytes(&self) -> [u8; 160] { + self.0.to_bytes() + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + GroupedCiphertext2HandlesValidityProof::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 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 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 = BatchedGroupedCiphertext2HandlesValidityProof::new( + (destination_pubkey, auditor_pubkey), + (amount_lo, amount_hi), + (&open_lo, &open_hi), + &mut prover_transcript, + ); + + assert!(proof + .verify( + (destination_pubkey, auditor_pubkey), + (&commitment_lo, &commitment_hi), + (&destination_handle_lo, &destination_handle_hi), + (&auditor_handle_lo, &auditor_handle_hi), + &mut verifier_transcript, + ) + .is_ok()); + } +} diff --git a/zk-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity_proof/handles_3.rs b/zk-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity_proof/handles_3.rs new file mode 100644 index 00000000000000..2faaa81c34421e --- /dev/null +++ b/zk-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-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity_proof/mod.rs b/zk-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity_proof/mod.rs new file mode 100644 index 00000000000000..0c8f9cecd9e2cf --- /dev/null +++ b/zk-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-sdk/src/sigma_proofs/ciphertext_ciphertext_equality_proof.rs b/zk-sdk/src/sigma_proofs/ciphertext_ciphertext_equality_proof.rs new file mode 100644 index 00000000000000..70a5de9c4c5efb --- /dev/null +++ b/zk-sdk/src/sigma_proofs/ciphertext_ciphertext_equality_proof.rs @@ -0,0 +1,341 @@ +//! The ciphertext-ciphertext equality sigma proof system. +//! +//! 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::{ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey}, + pedersen::{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::{EqualityProofVerificationError, SigmaProofVerificationError}, + transcript::TranscriptProtocol, + }, + curve25519_dalek::{ + ristretto::{CompressedRistretto, RistrettoPoint}, + scalar::Scalar, + traits::{IsIdentity, VartimeMultiscalarMul}, + }, + merlin::Transcript, +}; + +/// Byte length of a ciphertext-ciphertext equality proof. +const CIPHERTEXT_CIPHERTEXT_EQUALITY_PROOF_LEN: usize = UNIT_LEN * 7; + +/// The ciphertext-ciphertext equality proof. +/// +/// Contains all the elliptic curve and scalar components that make up the sigma protocol. +#[allow(non_snake_case)] +#[derive(Clone)] +pub struct CiphertextCiphertextEqualityProof { + Y_0: CompressedRistretto, + Y_1: CompressedRistretto, + Y_2: CompressedRistretto, + Y_3: CompressedRistretto, + z_s: Scalar, + z_x: Scalar, + z_r: Scalar, +} + +#[allow(non_snake_case)] +#[cfg(not(target_os = "solana"))] +impl CiphertextCiphertextEqualityProof { + /// Creates a ciphertext-ciphertext equality proof. + /// + /// 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. + /// + /// * `source_keypair` - The ElGamal keypair associated with the first ciphertext to be proved + /// * `destination_pubkey` - The ElGamal pubkey associated with the second ElGamal ciphertext + /// * `source_ciphertext` - The first ElGamal ciphertext for which the prover knows a + /// decryption key for + /// * `destination_opening` - The opening (randomness) associated with the second ElGamal ciphertext + /// * `amount` - The message associated with the ElGamal ciphertext and Pedersen commitment + /// * `transcript` - The transcript that does the bookkeeping for the Fiat-Shamir heuristic + pub fn new( + source_keypair: &ElGamalKeypair, + destination_pubkey: &ElGamalPubkey, + source_ciphertext: &ElGamalCiphertext, + destination_opening: &PedersenOpening, + amount: u64, + transcript: &mut Transcript, + ) -> Self { + transcript.equality_proof_domain_separator(); + + // extract the relevant scalar and Ristretto points from the inputs + let P_source = source_keypair.pubkey().get_point(); + let D_source = source_ciphertext.handle.get_point(); + let P_destination = destination_pubkey.get_point(); + + let s = source_keypair.secret().get_scalar(); + let x = Scalar::from(amount); + let r = destination_opening.get_scalar(); + + // 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_source).compress(); + let Y_1 = + RistrettoPoint::multiscalar_mul(vec![&y_x, &y_s], vec![&(*G), D_source]).compress(); + let Y_2 = RistrettoPoint::multiscalar_mul(vec![&y_x, &y_r], vec![&(*G), &(*H)]).compress(); + let Y_3 = (&y_r * P_destination).compress(); + + // 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); + transcript.append_point(b"Y_3", &Y_3); + + let c = transcript.challenge_scalar(b"c"); + 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; + + // zeroize random scalars + y_s.zeroize(); + y_x.zeroize(); + y_r.zeroize(); + + CiphertextCiphertextEqualityProof { + Y_0, + Y_1, + Y_2, + Y_3, + z_s, + z_x, + z_r, + } + } + + /// Verifies a ciphertext-ciphertext equality proof. + /// + /// * `source_pubkey` - The ElGamal pubkey associated with the first ciphertext to be proved + /// * `destination_pubkey` - The ElGamal pubkey associated with the second ciphertext to be proved + /// * `source_ciphertext` - The first ElGamal ciphertext to be proved + /// * `destination_ciphertext` - The second ElGamal ciphertext to be proved + /// * `transcript` - The transcript that does the bookkeeping for the Fiat-Shamir heuristic + pub fn verify( + self, + source_pubkey: &ElGamalPubkey, + destination_pubkey: &ElGamalPubkey, + source_ciphertext: &ElGamalCiphertext, + destination_ciphertext: &ElGamalCiphertext, + transcript: &mut Transcript, + ) -> Result<(), EqualityProofVerificationError> { + transcript.equality_proof_domain_separator(); + + // extract the relevant scalar and Ristretto points from the inputs + let P_source = source_pubkey.get_point(); + let C_source = source_ciphertext.commitment.get_point(); + let D_source = source_ciphertext.handle.get_point(); + + let P_destination = destination_pubkey.get_point(); + let C_destination = destination_ciphertext.commitment.get_point(); + let D_destination = destination_ciphertext.handle.get_point(); + + // 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)?; + transcript.validate_and_append_point(b"Y_3", &self.Y_3)?; + + let c = transcript.challenge_scalar(b"c"); + let w = transcript.challenge_scalar(b"w"); // w used for batch verification + let ww = &w * &w; + let www = &w * &ww; + + let w_negated = -&w; + let ww_negated = -&ww; + let www_negated = -&www; + + // check that the required algebraic condition holds + 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 check = RistrettoPoint::vartime_multiscalar_mul( + vec![ + &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 + &(&www * &self.z_r), // z_r + &(&www_negated * &c), // -www * c + &www_negated, + ], + vec![ + P_source, // P_source + &(*H), // H + &Y_0, // Y_0 + &(*G), // G + D_source, // D_source + C_source, // C_source + &Y_1, // Y_1 + &(*G), // G + &(*H), // H + C_destination, // C_destination + &Y_2, // Y_2 + P_destination, // P_destination + D_destination, // D_destination + &Y_3, // Y_3 + ], + ); + + if check.is_identity() { + Ok(()) + } else { + Err(SigmaProofVerificationError::AlgebraicRelation.into()) + } + } + + pub fn to_bytes(&self) -> [u8; CIPHERTEXT_CIPHERTEXT_EQUALITY_PROOF_LEN] { + let mut buf = [0_u8; CIPHERTEXT_CIPHERTEXT_EQUALITY_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_s.as_bytes()); + chunks.next().unwrap().copy_from_slice(self.z_x.as_bytes()); + chunks.next().unwrap().copy_from_slice(self.z_r.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_s = canonical_scalar_from_optional_slice(chunks.next())?; + let z_x = canonical_scalar_from_optional_slice(chunks.next())?; + let z_r = canonical_scalar_from_optional_slice(chunks.next())?; + + Ok(CiphertextCiphertextEqualityProof { + Y_0, + Y_1, + Y_2, + Y_3, + z_s, + z_x, + z_r, + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_ciphertext_ciphertext_equality_proof_correctness() { + // success case + let source_keypair = ElGamalKeypair::new_rand(); + let destination_keypair = ElGamalKeypair::new_rand(); + let message: u64 = 55; + + let source_ciphertext = source_keypair.pubkey().encrypt(message); + + let destination_opening = PedersenOpening::new_rand(); + let destination_ciphertext = destination_keypair + .pubkey() + .encrypt_with(message, &destination_opening); + + let mut prover_transcript = Transcript::new(b"Test"); + let mut verifier_transcript = Transcript::new(b"Test"); + + let proof = CiphertextCiphertextEqualityProof::new( + &source_keypair, + destination_keypair.pubkey(), + &source_ciphertext, + &destination_opening, + message, + &mut prover_transcript, + ); + + assert!(proof + .verify( + source_keypair.pubkey(), + destination_keypair.pubkey(), + &source_ciphertext, + &destination_ciphertext, + &mut verifier_transcript + ) + .is_ok()); + + // fail case: encrypted and committed messages are different + let source_message: u64 = 55; + let destination_message: u64 = 77; + + let source_ciphertext = source_keypair.pubkey().encrypt(source_message); + + let destination_opening = PedersenOpening::new_rand(); + let destination_ciphertext = destination_keypair + .pubkey() + .encrypt_with(destination_message, &destination_opening); + + let mut prover_transcript = Transcript::new(b"Test"); + let mut verifier_transcript = Transcript::new(b"Test"); + + let proof = CiphertextCiphertextEqualityProof::new( + &source_keypair, + destination_keypair.pubkey(), + &source_ciphertext, + &destination_opening, + message, + &mut prover_transcript, + ); + + assert!(proof + .verify( + source_keypair.pubkey(), + destination_keypair.pubkey(), + &source_ciphertext, + &destination_ciphertext, + &mut verifier_transcript + ) + .is_err()); + } +} diff --git a/zk-sdk/src/sigma_proofs/ciphertext_commitment_equality_proof.rs b/zk-sdk/src/sigma_proofs/ciphertext_commitment_equality_proof.rs new file mode 100644 index 00000000000000..768b07b216cdbe --- /dev/null +++ b/zk-sdk/src/sigma_proofs/ciphertext_commitment_equality_proof.rs @@ -0,0 +1,427 @@ +//! The ciphertext-commitment equality sigma proof system. +//! +//! A ciphertext-commitment equality proof is defined with respect to a twisted ElGamal ciphertext +//! and a Pedersen commitment. The proof certifies that a given ciphertext and a commitment pair +//! encrypts/encodes the same message. To generate the proof, a prover must provide the decryption +//! key for the first 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_os = "solana"))] +use { + crate::{ + encryption::{ + elgamal::{ElGamalCiphertext, ElGamalKeypair, 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::{EqualityProofVerificationError, SigmaProofVerificationError}, + transcript::TranscriptProtocol, + }, + curve25519_dalek::{ + ristretto::{CompressedRistretto, RistrettoPoint}, + scalar::Scalar, + traits::{IsIdentity, VartimeMultiscalarMul}, + }, + merlin::Transcript, +}; + +/// Byte length of a ciphertext-commitment equality proof. +const CIPHERTEXT_COMMITMENT_EQUALITY_PROOF_LEN: usize = UNIT_LEN * 6; + +/// Equality proof. +/// +/// Contains all the elliptic curve and scalar components that make up the sigma protocol. +#[allow(non_snake_case)] +#[derive(Clone)] +pub struct CiphertextCommitmentEqualityProof { + 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_os = "solana"))] +impl CiphertextCommitmentEqualityProof { + /// Creates a ciphertext-commitment equality proof. + /// + /// 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. + /// + /// * `source_keypair` - The ElGamal keypair associated with the first to be proved + /// * `source_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( + source_keypair: &ElGamalKeypair, + source_ciphertext: &ElGamalCiphertext, + opening: &PedersenOpening, + amount: u64, + transcript: &mut Transcript, + ) -> Self { + transcript.equality_proof_domain_separator(); + + // extract the relevant scalar and Ristretto points from the inputs + let P_source = source_keypair.pubkey().get_point(); + let D_source = source_ciphertext.handle.get_point(); + + let s = source_keypair.secret().get_scalar(); + let x = Scalar::from(amount); + let r = opening.get_scalar(); + + // 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_source).compress(); + let Y_1 = + RistrettoPoint::multiscalar_mul(vec![&y_x, &y_s], vec![&(*G), D_source]).compress(); + let Y_2 = RistrettoPoint::multiscalar_mul(vec![&y_x, &y_r], vec![&(*G), &(*H)]).compress(); + + // 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); + + let c = transcript.challenge_scalar(b"c"); + 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; + + // zeroize random scalars + y_s.zeroize(); + y_x.zeroize(); + y_r.zeroize(); + + CiphertextCommitmentEqualityProof { + Y_0, + Y_1, + Y_2, + z_s, + z_x, + z_r, + } + } + + /// Verifies a ciphertext-commitment equality proof. + /// + /// * `source_pubkey` - The ElGamal pubkey associated with the ciphertext to be proved + /// * `source_ciphertext` - The main ElGamal ciphertext to be proved + /// * `destination_commitment` - The main Pedersen commitment to be proved + /// * `transcript` - The transcript that does the bookkeeping for the Fiat-Shamir heuristic + pub fn verify( + self, + source_pubkey: &ElGamalPubkey, + source_ciphertext: &ElGamalCiphertext, + destination_commitment: &PedersenCommitment, + transcript: &mut Transcript, + ) -> Result<(), EqualityProofVerificationError> { + transcript.equality_proof_domain_separator(); + + // extract the relevant scalar and Ristretto points from the inputs + let P_source = source_pubkey.get_point(); + let C_source = source_ciphertext.commitment.get_point(); + let D_source = source_ciphertext.handle.get_point(); + let C_destination = destination_commitment.get_point(); + + // 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)?; + + let c = transcript.challenge_scalar(b"c"); + 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(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 check = RistrettoPoint::vartime_multiscalar_mul( + vec![ + &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_source, // P_source + &(*H), // H + &Y_0, // Y_0 + &(*G), // G + D_source, // D_source + C_source, // C_source + &Y_1, // Y_1 + &(*G), // G + &(*H), // H + C_destination, // C_destination + &Y_2, // Y_2 + ], + ); + + if check.is_identity() { + Ok(()) + } else { + Err(SigmaProofVerificationError::AlgebraicRelation.into()) + } + } + + pub fn to_bytes(&self) -> [u8; CIPHERTEXT_COMMITMENT_EQUALITY_PROOF_LEN] { + let mut buf = [0_u8; CIPHERTEXT_COMMITMENT_EQUALITY_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.z_s.as_bytes()); + chunks.next().unwrap().copy_from_slice(self.z_x.as_bytes()); + chunks.next().unwrap().copy_from_slice(self.z_r.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 z_s = canonical_scalar_from_optional_slice(chunks.next())?; + let z_x = canonical_scalar_from_optional_slice(chunks.next())?; + let z_r = canonical_scalar_from_optional_slice(chunks.next())?; + + Ok(CiphertextCommitmentEqualityProof { + Y_0, + Y_1, + Y_2, + z_s, + z_x, + z_r, + }) + } +} + +#[cfg(test)] +mod test { + use { + super::*, + crate::encryption::{elgamal::ElGamalSecretKey, pedersen::Pedersen}, + }; + + #[test] + fn test_ciphertext_commitment_equality_proof_correctness() { + // success case + let source_keypair = ElGamalKeypair::new_rand(); + let message: u64 = 55; + + let source_ciphertext = source_keypair.pubkey().encrypt(message); + let (destination_commitment, destination_opening) = Pedersen::new(message); + + let mut prover_transcript = Transcript::new(b"Test"); + let mut verifier_transcript = Transcript::new(b"Test"); + + let proof = CiphertextCommitmentEqualityProof::new( + &source_keypair, + &source_ciphertext, + &destination_opening, + message, + &mut prover_transcript, + ); + + assert!(proof + .verify( + source_keypair.pubkey(), + &source_ciphertext, + &destination_commitment, + &mut verifier_transcript + ) + .is_ok()); + + // fail case: encrypted and committed messages are different + let source_keypair = ElGamalKeypair::new_rand(); + let encrypted_message: u64 = 55; + let committed_message: u64 = 77; + + let source_ciphertext = source_keypair.pubkey().encrypt(encrypted_message); + let (destination_commitment, destination_opening) = Pedersen::new(committed_message); + + let mut prover_transcript = Transcript::new(b"Test"); + let mut verifier_transcript = Transcript::new(b"Test"); + + let proof = CiphertextCommitmentEqualityProof::new( + &source_keypair, + &source_ciphertext, + &destination_opening, + message, + &mut prover_transcript, + ); + + assert!(proof + .verify( + source_keypair.pubkey(), + &source_ciphertext, + &destination_commitment, + &mut verifier_transcript + ) + .is_err()); + } + + #[test] + fn test_ciphertext_commitment_equality_proof_edge_cases() { + // if ElGamal public key zero (public key is invalid), then the proof should always reject + let public = ElGamalPubkey::try_from([0u8; 32].as_slice()).unwrap(); + let secret = ElGamalSecretKey::new_rand(); + + let elgamal_keypair = ElGamalKeypair::new_for_tests(public, secret); + + let message: u64 = 55; + let ciphertext = elgamal_keypair.pubkey().encrypt(message); + let (commitment, opening) = Pedersen::new(message); + + let mut prover_transcript = Transcript::new(b"Test"); + let mut verifier_transcript = Transcript::new(b"Test"); + + let proof = CiphertextCommitmentEqualityProof::new( + &elgamal_keypair, + &ciphertext, + &opening, + message, + &mut prover_transcript, + ); + + assert!(proof + .verify( + elgamal_keypair.pubkey(), + &ciphertext, + &commitment, + &mut verifier_transcript + ) + .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 prover_transcript = Transcript::new(b"Test"); + let mut verifier_transcript = Transcript::new(b"Test"); + + let proof = CiphertextCommitmentEqualityProof::new( + &elgamal_keypair, + &ciphertext, + &opening, + message, + &mut prover_transcript, + ); + + assert!(proof + .verify( + elgamal_keypair.pubkey(), + &ciphertext, + &commitment, + &mut verifier_transcript + ) + .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.pubkey().encrypt(message); + let commitment = PedersenCommitment::from_bytes(&[0u8; 32]).unwrap(); + let opening = PedersenOpening::from_bytes(&[0u8; 32]).unwrap(); + + let mut prover_transcript = Transcript::new(b"Test"); + let mut verifier_transcript = Transcript::new(b"Test"); + + let proof = CiphertextCommitmentEqualityProof::new( + &elgamal_keypair, + &ciphertext, + &opening, + message, + &mut prover_transcript, + ); + + assert!(proof + .verify( + elgamal_keypair.pubkey(), + &ciphertext, + &commitment, + &mut verifier_transcript + ) + .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 prover_transcript = Transcript::new(b"Test"); + let mut verifier_transcript = Transcript::new(b"Test"); + + let proof = CiphertextCommitmentEqualityProof::new( + &elgamal_keypair, + &ciphertext, + &opening, + message, + &mut prover_transcript, + ); + + assert!(proof + .verify( + elgamal_keypair.pubkey(), + &ciphertext, + &commitment, + &mut verifier_transcript + ) + .is_ok()); + } +} diff --git a/zk-sdk/src/sigma_proofs/errors.rs b/zk-sdk/src/sigma_proofs/errors.rs new file mode 100644 index 00000000000000..1ce28b01bf85a8 --- /dev/null +++ b/zk-sdk/src/sigma_proofs/errors.rs @@ -0,0 +1,49 @@ +//! Errors related to proving and verifying sigma proofs. +use {crate::errors::TranscriptError, thiserror::Error}; + +#[derive(Error, Clone, Debug, Eq, PartialEq)] +pub enum SigmaProofVerificationError { + #[error("required algebraic relation does not hold")] + AlgebraicRelation, + #[error("malformed proof")] + Deserialization, + #[error("multiscalar multiplication failed")] + MultiscalarMul, + #[error("transcript failed to produce a challenge")] + Transcript(#[from] TranscriptError), +} + +macro_rules! impl_from_transcript_error { + ($sigma_error_type:ty) => { + impl From for $sigma_error_type { + fn from(err: TranscriptError) -> Self { + SigmaProofVerificationError::Transcript(err).into() + } + } + }; +} + +#[derive(Error, Clone, Debug, Eq, PartialEq)] +#[error("equality proof verification failed: {0}")] +pub struct EqualityProofVerificationError(#[from] pub(crate) SigmaProofVerificationError); +impl_from_transcript_error!(EqualityProofVerificationError); + +#[derive(Error, Clone, Debug, Eq, PartialEq)] +#[error("validity proof verification failed: {0}")] +pub struct ValidityProofVerificationError(#[from] pub(crate) SigmaProofVerificationError); +impl_from_transcript_error!(ValidityProofVerificationError); + +#[derive(Error, Clone, Debug, Eq, PartialEq)] +#[error("zero-balance proof verification failed: {0}")] +pub struct ZeroBalanceProofVerificationError(#[from] pub(crate) SigmaProofVerificationError); +impl_from_transcript_error!(ZeroBalanceProofVerificationError); + +#[derive(Error, Clone, Debug, Eq, PartialEq)] +#[error("fee sigma proof verification failed: {0}")] +pub struct FeeSigmaProofVerificationError(#[from] pub(crate) SigmaProofVerificationError); +impl_from_transcript_error!(FeeSigmaProofVerificationError); + +#[derive(Error, Clone, Debug, Eq, PartialEq)] +#[error("public key validity proof verification failed: {0}")] +pub struct PubkeyValidityProofVerificationError(#[from] pub(crate) SigmaProofVerificationError); +impl_from_transcript_error!(PubkeyValidityProofVerificationError); diff --git a/zk-sdk/src/sigma_proofs/fee_proof.rs b/zk-sdk/src/sigma_proofs/fee_proof.rs new file mode 100644 index 00000000000000..c3a431768f1226 --- /dev/null +++ b/zk-sdk/src/sigma_proofs/fee_proof.rs @@ -0,0 +1,656 @@ +//! The fee sigma proof. +//! +//! A fee sigma proof certifies that an ElGamal ciphertext encrypts a properly computed transfer fee. +//! +//! A detailed description of how transfer fees and proofs are calculated is provided in the [`ZK +//! Token proof program`] documentation. +//! +//! The protocol guarantees computational soundness (by the hardness of discrete log) and perfect +//! zero-knowledge in the random oracle model. +//! +//! [`ZK Token proof program`]: https://docs.solanalabs.com/runtime/zk-token-proof + +#[cfg(not(target_os = "solana"))] +use { + crate::{ + encryption::pedersen::{PedersenCommitment, PedersenOpening, G, H}, + sigma_proofs::{canonical_scalar_from_optional_slice, ristretto_point_from_optional_slice}, + UNIT_LEN, + }, + rand::rngs::OsRng, +}; +use { + crate::{ + sigma_proofs::errors::{FeeSigmaProofVerificationError, SigmaProofVerificationError}, + transcript::TranscriptProtocol, + }, + curve25519_dalek::{ + ristretto::{CompressedRistretto, RistrettoPoint}, + scalar::Scalar, + traits::{IsIdentity, MultiscalarMul, VartimeMultiscalarMul}, + }, + merlin::Transcript, + subtle::{Choice, ConditionallySelectable, ConstantTimeGreater}, +}; + +/// Byte length of a fee sigma proof. +const FEE_SIGMA_PROOF_LEN: usize = UNIT_LEN * 8; + +/// Fee sigma proof. +/// +/// The proof consists of two main components: `fee_max_proof` and `fee_equality_proof`. If the fee +/// is greater than the maximum fee bound, then the `fee_max_proof` is properly generated and +/// `fee_equality_proof` is simulated. If the fee is smaller than the maximum fee bound, the +/// `fee_equality_proof` is properly generated and `fee_max_proof` is simulated. +#[derive(Clone)] +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_os = "solana"))] +impl FeeSigmaProof { + /// Creates a fee sigma proof assuming that the committed fee is greater than the maximum fee + /// bound. + /// + /// A transfer fee amount `fee_amount` for a `transfer_amount` is determined by two parameters: + /// - the `fee_rate_basis_point`, which defines the fee rate in units of 0.01%, + /// - the `max_fee`, which defines the cap amount for a transfer fee. + /// + /// This means that there are two cases to consider. If `fee_amount >= max_fee`, then the + /// `fee_amount` must always equal `max_fee`. + /// + /// If `fee_amount < max_fee`, then assuming that there is no division rounding, the + /// `fee_amount` must satisfy the relation `transfer_amount * (fee_rate_basis_point / + /// 10_000) = fee_amount` or equivalently, `(transfer_amount * fee_rate_basis_point) - (10_000 + /// * fee_amount) = 0`. More generally, let `delta_fee = (transfer_amount * + /// fee_rate_basis_point) - (10_000 * fee_amount)`. Then assuming that a division rounding + /// could occur, the `delta_fee` must satisfy the bound `0 <= delta_fee < 10_000`. + /// + /// If `fee_amount >= max_fee`, then `fee_amount = max_fee` and therefore, the prover can + /// generate a proof certifying that a fee commitment exactly encodes `max_fee`. If + /// `fee_amount < max_fee`, then the prover can create a commitment to `delta_fee` and + /// create a range proof certifying that the committed value satisfies the bound `0 <= + /// delta_fee < 10_000`. + /// + /// Since the type of proof that a prover generates reveals information about the transfer + /// amount and transfer fee, the prover must generate and include both types of proof. If + /// `fee_amount >= max_fee`, then the prover generates a valid `fee_max_proof`, but commits + /// to 0 as the "claimed" delta value and simulates ("fakes") a proof (`fee_equality_proof`) + /// that this is valid. If `fee_amount > max_fee`, then the prover simulates a + /// `fee_max_proof`, and creates a valid `fee_equality_proof` certifying that the claimed delta + /// value is equal to the "real" delta value. + /// + /// Note: In the implementation, the proof is generated twice via `create_proof_fee_above_max` + /// and `create_proof_fee_below_max` to enforce that the function executes in constant time. + /// + /// * `(fee_amount, fee_commitment, fee_opening)` - The amount, Pedersen commitment, and + /// opening of the transfer fee + /// * `(delta_fee, delta_commitment, delta_opening)` - The amount, Pedersen commitment, and + /// opening of the "real" delta amount + /// * `(claimed_commitment, claimed_opening)` - 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( + (fee_amount, fee_commitment, fee_opening): (u64, &PedersenCommitment, &PedersenOpening), + (delta_fee, delta_commitment, delta_opening): (u64, &PedersenCommitment, &PedersenOpening), + (claimed_commitment, claimed_opening): (&PedersenCommitment, &PedersenOpening), + max_fee: u64, + transcript: &mut Transcript, + ) -> Self { + let mut transcript_fee_above_max = transcript.clone(); + let mut transcript_fee_below_max = transcript.clone(); + + // 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( + fee_opening, + delta_commitment, + claimed_commitment, + &mut transcript_fee_above_max, + ); + + let proof_fee_below_max = Self::create_proof_fee_below_max( + fee_commitment, + (delta_fee, delta_opening), + claimed_opening, + max_fee, + &mut transcript_fee_below_max, + ); + + let below_max = u64::ct_gt(&max_fee, &fee_amount); + + // choose one of `proof_fee_above_max` or `proof_fee_below_max` according to whether the + // fee amount is greater than `max_fee` or not + let fee_max_proof = FeeMaxProof::conditional_select( + &proof_fee_above_max.fee_max_proof, + &proof_fee_below_max.fee_max_proof, + below_max, + ); + + let fee_equality_proof = FeeEqualityProof::conditional_select( + &proof_fee_above_max.fee_equality_proof, + &proof_fee_below_max.fee_equality_proof, + below_max, + ); + + transcript.append_point(b"Y_max_proof", &fee_max_proof.Y_max_proof); + transcript.append_point(b"Y_delta", &fee_equality_proof.Y_delta); + transcript.append_point(b"Y_claimed", &fee_equality_proof.Y_claimed); + transcript.challenge_scalar(b"c"); + transcript.challenge_scalar(b"w"); + + Self { + fee_max_proof, + fee_equality_proof, + } + } + + /// Creates a fee sigma proof assuming that the committed fee is greater than the maximum fee + /// bound. + /// + /// * `fee_opening` - The opening of the Pedersen commitment of the transfer fee + /// * `delta_commitment` - The Pedersen commitment of the "real" delta value + /// * `claimed_commitment` - 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( + fee_opening: &PedersenOpening, + delta_commitment: &PedersenCommitment, + claimed_commitment: &PedersenCommitment, + transcript: &mut Transcript, + ) -> Self { + // simulate equality proof + let C_delta = delta_commitment.get_point(); + let C_claimed = claimed_commitment.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 = fee_opening.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. + /// + /// * `fee_commitment` - The Pedersen commitment of the transfer fee + /// * `(delta_fee, delta_opening)` - The Pedersen commitment and opening of the "real" delta + /// value + /// * `claimed_opening` - 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( + fee_commitment: &PedersenCommitment, + (delta_fee, delta_opening): (u64, &PedersenOpening), + claimed_opening: &PedersenOpening, + max_fee: u64, + transcript: &mut Transcript, + ) -> Self { + // simulate max proof + let m = Scalar::from(max_fee); + let C_fee = fee_commitment.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 = delta_opening.get_scalar(); + let r_claimed = claimed_opening.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, + } + } + + /// Verifies a fee sigma proof + /// + /// * `fee_commitment` - The Pedersen commitment of the transfer fee + /// * `delta_commitment` - The Pedersen commitment of the "real" delta value + /// * `claimed_commitment` - 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, + fee_commitment: &PedersenCommitment, + delta_commitment: &PedersenCommitment, + claimed_commitment: &PedersenCommitment, + max_fee: u64, + transcript: &mut Transcript, + ) -> Result<(), FeeSigmaProofVerificationError> { + // extract the relevant scalar and Ristretto points from the input + let m = Scalar::from(max_fee); + + let C_max = fee_commitment.get_point(); + let C_delta = delta_commitment.get_point(); + let C_claimed = claimed_commitment.get_point(); + + 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_proof + .decompress() + .ok_or(SigmaProofVerificationError::Deserialization)?; + let z_max = self.fee_max_proof.z_max_proof; + + let Y_delta_real = self + .fee_equality_proof + .Y_delta + .decompress() + .ok_or(SigmaProofVerificationError::Deserialization)?; + let Y_claimed = self + .fee_equality_proof + .Y_claimed + .decompress() + .ok_or(SigmaProofVerificationError::Deserialization)?; + let z_x = self.fee_equality_proof.z_x; + 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_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_proof, + -c_max_proof * m, + -z_max, + Scalar::one(), + w * z_x, + w * z_delta_real, + -w * c_equality, + -w, + ww * z_x, + ww * z_claimed, + -ww * c_equality, + -ww, + ], + vec![ + C_max, + &(*G), + &(*H), + &Y_max, + &(*G), + &(*H), + C_delta, + &Y_delta_real, + &(*G), + &(*H), + C_claimed, + &Y_claimed, + ], + ); + + if check.is_identity() { + Ok(()) + } else { + Err(SigmaProofVerificationError::AlgebraicRelation.into()) + } + } + + pub fn to_bytes(&self) -> [u8; FEE_SIGMA_PROOF_LEN] { + let mut buf = [0_u8; FEE_SIGMA_PROOF_LEN]; + let mut chunks = buf.chunks_mut(UNIT_LEN); + chunks + .next() + .unwrap() + .copy_from_slice(self.fee_max_proof.Y_max_proof.as_bytes()); + chunks + .next() + .unwrap() + .copy_from_slice(self.fee_max_proof.z_max_proof.as_bytes()); + chunks + .next() + .unwrap() + .copy_from_slice(self.fee_max_proof.c_max_proof.as_bytes()); + chunks + .next() + .unwrap() + .copy_from_slice(self.fee_equality_proof.Y_delta.as_bytes()); + chunks + .next() + .unwrap() + .copy_from_slice(self.fee_equality_proof.Y_claimed.as_bytes()); + chunks + .next() + .unwrap() + .copy_from_slice(self.fee_equality_proof.z_x.as_bytes()); + chunks + .next() + .unwrap() + .copy_from_slice(self.fee_equality_proof.z_delta.as_bytes()); + chunks + .next() + .unwrap() + .copy_from_slice(self.fee_equality_proof.z_claimed.as_bytes()); + buf + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + let mut chunks = bytes.chunks(UNIT_LEN); + let Y_max_proof = ristretto_point_from_optional_slice(chunks.next())?; + let z_max_proof = canonical_scalar_from_optional_slice(chunks.next())?; + let c_max_proof = canonical_scalar_from_optional_slice(chunks.next())?; + + let Y_delta = ristretto_point_from_optional_slice(chunks.next())?; + let Y_claimed = ristretto_point_from_optional_slice(chunks.next())?; + let z_x = canonical_scalar_from_optional_slice(chunks.next())?; + let z_delta = canonical_scalar_from_optional_slice(chunks.next())?; + let z_claimed = canonical_scalar_from_optional_slice(chunks.next())?; + + Ok(Self { + fee_max_proof: FeeMaxProof { + Y_max_proof, + z_max_proof, + c_max_proof, + }, + fee_equality_proof: FeeEqualityProof { + Y_delta, + Y_claimed, + z_x, + z_delta, + z_claimed, + }, + }) + } +} + +/// The fee max proof. +/// +/// The proof certifies that the transfer fee Pedersen commitment encodes the maximum fee bound. +#[allow(non_snake_case)] +#[derive(Clone, Copy)] +pub struct FeeMaxProof { + 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, Copy)] +pub struct FeeEqualityProof { + 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), + } + } +} + +/// Selects one of two Ristretto points in constant time. +#[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)] +mod test { + use {super::*, crate::encryption::pedersen::Pedersen}; + + #[test] + fn test_fee_above_max_proof() { + let transfer_amount: u64 = 55; + let max_fee: u64 = 3; + + let fee_rate: u16 = 555; // 5.55% + let fee_amount: u64 = 4; + let delta: u64 = 9475; // 4*10000 - 55*555 + + let (transfer_commitment, transfer_opening) = Pedersen::new(transfer_amount); + let (fee_commitment, fee_opening) = Pedersen::new(max_fee); + + let scalar_rate = Scalar::from(fee_rate); + let delta_commitment = + &fee_commitment * &Scalar::from(10000_u64) - &transfer_commitment * &scalar_rate; + let delta_opening = + &fee_opening * &Scalar::from(10000_u64) - &transfer_opening * &scalar_rate; + + let (claimed_commitment, claimed_opening) = Pedersen::new(0_u64); + + let mut prover_transcript = Transcript::new(b"test"); + let mut verifier_transcript = Transcript::new(b"test"); + + let proof = FeeSigmaProof::new( + (fee_amount, &fee_commitment, &fee_opening), + (delta, &delta_commitment, &delta_opening), + (&claimed_commitment, &claimed_opening), + max_fee, + &mut prover_transcript, + ); + + assert!(proof + .verify( + &fee_commitment, + &delta_commitment, + &claimed_commitment, + max_fee, + &mut verifier_transcript, + ) + .is_ok()); + } + + #[test] + fn test_fee_below_max_proof() { + let transfer_amount: u64 = 1; + let max_fee: u64 = 3; + + let fee_rate: u16 = 400; // 5.55% + let fee_amount: u64 = 1; + let delta: u64 = 9600; // 4*10000 - 55*555 + + let (transfer_commitment, transfer_opening) = Pedersen::new(transfer_amount); + let (fee_commitment, fee_opening) = Pedersen::new(fee_amount); + + let scalar_rate = Scalar::from(fee_rate); + let delta_commitment = + &fee_commitment * &Scalar::from(10000_u64) - &transfer_commitment * &scalar_rate; + let delta_opening = + &fee_opening * &Scalar::from(10000_u64) - &transfer_opening * &scalar_rate; + + let (claimed_commitment, claimed_opening) = Pedersen::new(delta); + + assert_eq!( + delta_commitment.get_point() - delta_opening.get_scalar() * &(*H), + claimed_commitment.get_point() - claimed_opening.get_scalar() * &(*H) + ); + + let mut prover_transcript = Transcript::new(b"test"); + let mut verifier_transcript = Transcript::new(b"test"); + + let proof = FeeSigmaProof::new( + (fee_amount, &fee_commitment, &fee_opening), + (delta, &delta_commitment, &delta_opening), + (&claimed_commitment, &claimed_opening), + max_fee, + &mut prover_transcript, + ); + + assert!(proof + .verify( + &fee_commitment, + &delta_commitment, + &claimed_commitment, + max_fee, + &mut verifier_transcript, + ) + .is_ok()); + } + + #[test] + fn test_fee_delta_is_zero() { + let transfer_amount: u64 = 100; + let max_fee: u64 = 3; + + let fee_rate: u16 = 100; // 1.00% + let fee_amount: u64 = 1; + let delta: u64 = 0; // 1*10000 - 100*100 + + let (transfer_commitment, transfer_opening) = Pedersen::new(transfer_amount); + let (fee_commitment, fee_opening) = Pedersen::new(fee_amount); + + let scalar_rate = Scalar::from(fee_rate); + let delta_commitment = + &(&fee_commitment * &Scalar::from(10000_u64)) - &(&transfer_commitment * &scalar_rate); + let delta_opening = + &(&fee_opening * &Scalar::from(10000_u64)) - &(&transfer_opening * &scalar_rate); + + let (claimed_commitment, claimed_opening) = Pedersen::new(delta); + + let mut prover_transcript = Transcript::new(b"test"); + let mut verifier_transcript = Transcript::new(b"test"); + + let proof = FeeSigmaProof::new( + (fee_amount, &fee_commitment, &fee_opening), + (delta, &delta_commitment, &delta_opening), + (&claimed_commitment, &claimed_opening), + max_fee, + &mut prover_transcript, + ); + + assert!(proof + .verify( + &fee_commitment, + &delta_commitment, + &claimed_commitment, + max_fee, + &mut verifier_transcript, + ) + .is_ok()); + } +} diff --git a/zk-sdk/src/sigma_proofs/grouped_ciphertext_validity_proof/handles_2.rs b/zk-sdk/src/sigma_proofs/grouped_ciphertext_validity_proof/handles_2.rs new file mode 100644 index 00000000000000..1c1a57997e4740 --- /dev/null +++ b/zk-sdk/src/sigma_proofs/grouped_ciphertext_validity_proof/handles_2.rs @@ -0,0 +1,372 @@ +//! 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_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 2 handles +const GROUPED_CIPHERTEXT_2_HANDLES_VALIDITY_PROOF_LEN: usize = UNIT_LEN * 5; + +/// The grouped ciphertext validity proof for 2 handles. +/// +/// Contains all the elliptic curve and scalar components that make up the sigma protocol. +#[allow(non_snake_case)] +#[derive(Clone)] +pub struct GroupedCiphertext2HandlesValidityProof { + Y_0: CompressedRistretto, + Y_1: CompressedRistretto, + Y_2: CompressedRistretto, + z_r: Scalar, + z_x: Scalar, +} + +#[allow(non_snake_case)] +#[cfg(not(target_os = "solana"))] +impl GroupedCiphertext2HandlesValidityProof { + /// Creates a grouped ciphertext validity proof for 2 handles. + /// + /// 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. + /// + /// * `(destination_pubkey, auditor_pubkey)` - 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>( + (destination_pubkey, auditor_pubkey): (&ElGamalPubkey, &ElGamalPubkey), // TODO: rename auditor_pubkey + 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_dest = 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_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 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, + z_r, + z_x, + } + } + + /// Verifies a grouped ciphertext validity proof for 2 handles. + /// + /// * `commitment` - The Pedersen commitment + /// * `(destination_pubkey, auditor_pubkey)` - The ElGamal pubkeys associated with the decryption + /// handles + /// * `(destination_handle, auditor_handle)` - The decryption handles + /// * `transcript` - The transcript that does the bookkeeping for the Fiat-Shamir heuristic + pub fn verify( + self, + commitment: &PedersenCommitment, + (destination_pubkey, auditor_pubkey): (&ElGamalPubkey, &ElGamalPubkey), + (destination_handle, auditor_handle): (&DecryptHandle, &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)?; + // Y_2 can be an all zero point if the auditor public key is all zero + transcript.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_negated = -&w; + let ww_negated = -&ww; + + // 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 P_dest = destination_pubkey.get_point(); + let P_auditor = auditor_pubkey.get_point(); + + let C = commitment.get_point(); + let D_dest = 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 + ], + vec![ + &(*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 + ], + ); + + if check.is_identity() { + Ok(()) + } else { + Err(SigmaProofVerificationError::AlgebraicRelation.into()) + } + } + + pub fn to_bytes(&self) -> [u8; GROUPED_CIPHERTEXT_2_HANDLES_VALIDITY_PROOF_LEN] { + let mut buf = [0_u8; GROUPED_CIPHERTEXT_2_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.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 z_r = canonical_scalar_from_optional_slice(chunks.next())?; + let z_x = canonical_scalar_from_optional_slice(chunks.next())?; + + Ok(GroupedCiphertext2HandlesValidityProof { + Y_0, + Y_1, + Y_2, + z_r, + z_x, + }) + } +} + +#[cfg(test)] +mod test { + use { + super::*, + crate::encryption::{elgamal::ElGamalKeypair, pedersen::Pedersen}, + }; + + #[test] + fn test_grouped_ciphertext_validity_proof_correctness() { + 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 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 = GroupedCiphertext2HandlesValidityProof::new( + (destination_pubkey, auditor_pubkey), + amount, + &opening, + &mut prover_transcript, + ); + + assert!(proof + .verify( + &commitment, + (destination_pubkey, auditor_pubkey), + (&destination_handle, &auditor_handle), + &mut verifier_transcript, + ) + .is_ok()); + } + + #[test] + fn test_grouped_ciphertext_validity_proof_edge_cases() { + // if destination public key zeroed, then the proof should always reject + 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 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 = GroupedCiphertext2HandlesValidityProof::new( + (&destination_pubkey, auditor_pubkey), + amount, + &opening, + &mut prover_transcript, + ); + + assert!(proof + .verify( + &commitment, + (&destination_pubkey, auditor_pubkey), + (&destination_handle, &auditor_handle), + &mut verifier_transcript, + ) + .is_err()); + + // all zeroed ciphertext should still be valid + 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 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 = GroupedCiphertext2HandlesValidityProof::new( + (destination_pubkey, auditor_pubkey), + amount, + &opening, + &mut prover_transcript, + ); + + assert!(proof + .verify( + &commitment, + (destination_pubkey, auditor_pubkey), + (&destination_handle, &auditor_handle), + &mut verifier_transcript, + ) + .is_ok()); + + // decryption handles can be zero as long as the Pedersen commitment is valid + 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 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 = GroupedCiphertext2HandlesValidityProof::new( + (destination_pubkey, auditor_pubkey), + amount, + &opening, + &mut prover_transcript, + ); + + assert!(proof + .verify( + &commitment, + (destination_pubkey, auditor_pubkey), + (&destination_handle, &auditor_handle), + &mut verifier_transcript, + ) + .is_ok()); + } +} diff --git a/zk-sdk/src/sigma_proofs/grouped_ciphertext_validity_proof/handles_3.rs b/zk-sdk/src/sigma_proofs/grouped_ciphertext_validity_proof/handles_3.rs new file mode 100644 index 00000000000000..1324292315a04c --- /dev/null +++ b/zk-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-sdk/src/sigma_proofs/grouped_ciphertext_validity_proof/mod.rs b/zk-sdk/src/sigma_proofs/grouped_ciphertext_validity_proof/mod.rs new file mode 100644 index 00000000000000..8b6555ce10c65d --- /dev/null +++ b/zk-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-sdk/src/sigma_proofs/mod.rs b/zk-sdk/src/sigma_proofs/mod.rs new file mode 100644 index 00000000000000..bad707157a8c33 --- /dev/null +++ b/zk-sdk/src/sigma_proofs/mod.rs @@ -0,0 +1,58 @@ +//! Collection of sigma proofs that are used in the ZK Token proof program. +//! +//! Formal documentation and security proofs for the sigma proofs in this module can be found in +//! [`ZK Token proof`] program documentation. +//! +//! [`ZK Token proof`]: https://docs.solanalabs.com/runtime/zk-token-proof + +pub mod errors; + +#[cfg(not(target_os = "solana"))] +pub mod batched_grouped_ciphertext_validity_proof; +#[cfg(not(target_os = "solana"))] +pub mod ciphertext_ciphertext_equality_proof; +#[cfg(not(target_os = "solana"))] +pub mod ciphertext_commitment_equality_proof; +#[cfg(not(target_os = "solana"))] +pub mod fee_proof; +#[cfg(not(target_os = "solana"))] +pub mod grouped_ciphertext_validity_proof; +#[cfg(not(target_os = "solana"))] +pub mod pubkey_proof; +#[cfg(not(target_os = "solana"))] +pub mod zero_balance_proof; + +#[cfg(not(target_os = "solana"))] +use { + crate::{sigma_proofs::errors::SigmaProofVerificationError, RISTRETTO_POINT_LEN, SCALAR_LEN}, + curve25519_dalek::{ristretto::CompressedRistretto, scalar::Scalar}, +}; + +/// Deserializes an optional slice of bytes to a compressed Ristretto point. +/// +/// This is a helper function for deserializing byte encodings of sigma proofs. It is designed to +/// be used with `std::slice::Chunks`. +#[cfg(not(target_os = "solana"))] +fn ristretto_point_from_optional_slice( + optional_slice: Option<&[u8]>, +) -> Result { + optional_slice + .and_then(|slice| (slice.len() == RISTRETTO_POINT_LEN).then_some(slice)) + .map(CompressedRistretto::from_slice) + .ok_or(SigmaProofVerificationError::Deserialization) +} + +/// Deserializes an optional slice of bytes to a scalar. +/// +/// This is a helper function for deserializing byte encodings of sigma proofs. It is designed to +/// be used with `std::slice::Chunks`. +#[cfg(not(target_os = "solana"))] +fn canonical_scalar_from_optional_slice( + optional_slice: Option<&[u8]>, +) -> Result { + optional_slice + .and_then(|slice| (slice.len() == SCALAR_LEN).then_some(slice)) // if chunk is the wrong length, convert to None + .and_then(|slice| slice.try_into().ok()) // convert to array + .and_then(Scalar::from_canonical_bytes) + .ok_or(SigmaProofVerificationError::Deserialization) +} diff --git a/zk-sdk/src/sigma_proofs/pubkey_proof.rs b/zk-sdk/src/sigma_proofs/pubkey_proof.rs new file mode 100644 index 00000000000000..e0d80f2a528ef8 --- /dev/null +++ b/zk-sdk/src/sigma_proofs/pubkey_proof.rs @@ -0,0 +1,171 @@ +//! The public-key (validity) proof system. +//! +//! 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::{ElGamalKeypair, ElGamalPubkey}, + pedersen::H, + }, + sigma_proofs::{canonical_scalar_from_optional_slice, ristretto_point_from_optional_slice}, + UNIT_LEN, + }, + rand::rngs::OsRng, + zeroize::Zeroize, +}; +use { + crate::{ + sigma_proofs::errors::{PubkeyValidityProofVerificationError, SigmaProofVerificationError}, + transcript::TranscriptProtocol, + }, + curve25519_dalek::{ + ristretto::{CompressedRistretto, RistrettoPoint}, + scalar::Scalar, + traits::{IsIdentity, VartimeMultiscalarMul}, + }, + merlin::Transcript, +}; + +/// Byte length of a public key validity proof. +const PUBKEY_VALIDITY_PROOF_LEN: usize = UNIT_LEN * 2; + +/// Public-key proof. +/// +/// Contains all the elliptic curve and scalar components that make up the sigma protocol. +#[allow(non_snake_case)] +#[derive(Clone)] +pub struct PubkeyValidityProof { + Y: CompressedRistretto, + z: Scalar, +} + +#[allow(non_snake_case)] +#[cfg(not(target_os = "solana"))] +impl PubkeyValidityProof { + /// Creates a public key validity proof. + /// + /// The function does *not* hash the public key and ciphertext into the transcript. For + /// security, the caller (the main protocol) should hash these public key components prior to + /// invoking this constructor. + /// + /// This function is randomized. It uses `OsRng` internally to generate random scalars. + /// + /// This function panics if the provided keypair is not valid (i.e. secret key is not + /// invertible). + /// + /// * `elgamal_keypair` = The ElGamal keypair that pertains to the ElGamal public key to be + /// proved + /// * `transcript` - The transcript that does the bookkeeping for the Fiat-Shamir heuristic + pub fn new(elgamal_keypair: &ElGamalKeypair, transcript: &mut Transcript) -> Self { + transcript.pubkey_proof_domain_separator(); + + // extract the relevant scalar and Ristretto points from the input + let s = elgamal_keypair.secret().get_scalar(); + + assert!(s != &Scalar::zero()); + let s_inv = s.invert(); + + // generate a random masking factor that also serves as a nonce + let mut y = Scalar::random(&mut OsRng); + let Y = (&y * &(*H)).compress(); + + // record masking factors in transcript and get challenges + transcript.append_point(b"Y", &Y); + let c = transcript.challenge_scalar(b"c"); + + // compute masked secret key + let z = &(&c * s_inv) + &y; + + y.zeroize(); + + Self { Y, z } + } + + /// Verifies a public key validity proof. + /// + /// * `elgamal_pubkey` - The ElGamal public key to be proved + /// * `transcript` - The transcript that does the bookkeeping for the Fiat-Shamir heuristic + pub fn verify( + self, + elgamal_pubkey: &ElGamalPubkey, + transcript: &mut Transcript, + ) -> Result<(), PubkeyValidityProofVerificationError> { + transcript.pubkey_proof_domain_separator(); + + // extract the relvant scalar and Ristretto points from the input + let P = elgamal_pubkey.get_point(); + + // include Y to transcript and extract challenge + transcript.validate_and_append_point(b"Y", &self.Y)?; + let c = transcript.challenge_scalar(b"c"); + + // check that the required algebraic condition holds + let Y = self + .Y + .decompress() + .ok_or(SigmaProofVerificationError::Deserialization)?; + + let check = RistrettoPoint::vartime_multiscalar_mul( + vec![&self.z, &(-&c), &(-&Scalar::one())], + vec![&(*H), P, &Y], + ); + + if check.is_identity() { + Ok(()) + } else { + Err(SigmaProofVerificationError::AlgebraicRelation.into()) + } + } + + pub fn to_bytes(&self) -> [u8; PUBKEY_VALIDITY_PROOF_LEN] { + let mut buf = [0_u8; PUBKEY_VALIDITY_PROOF_LEN]; + let mut chunks = buf.chunks_mut(UNIT_LEN); + chunks.next().unwrap().copy_from_slice(self.Y.as_bytes()); + chunks.next().unwrap().copy_from_slice(self.z.as_bytes()); + buf + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + let mut chunks = bytes.chunks(UNIT_LEN); + let Y = ristretto_point_from_optional_slice(chunks.next())?; + let z = canonical_scalar_from_optional_slice(chunks.next())?; + Ok(PubkeyValidityProof { Y, z }) + } +} + +#[cfg(test)] +mod test { + use { + super::*, + solana_sdk::{pubkey::Pubkey, signature::Keypair}, + }; + + #[test] + fn test_pubkey_proof_correctness() { + // random ElGamal keypair + let keypair = ElGamalKeypair::new_rand(); + + let mut prover_transcript = Transcript::new(b"test"); + let mut verifier_transcript = Transcript::new(b"test"); + + let proof = PubkeyValidityProof::new(&keypair, &mut prover_transcript); + assert!(proof + .verify(keypair.pubkey(), &mut verifier_transcript) + .is_ok()); + + // derived ElGamal keypair + let keypair = + ElGamalKeypair::new_from_signer(&Keypair::new(), Pubkey::default().as_ref()).unwrap(); + + let mut prover_transcript = Transcript::new(b"test"); + let mut verifier_transcript = Transcript::new(b"test"); + + let proof = PubkeyValidityProof::new(&keypair, &mut prover_transcript); + assert!(proof + .verify(keypair.pubkey(), &mut verifier_transcript) + .is_ok()); + } +} diff --git a/zk-sdk/src/sigma_proofs/zero_balance_proof.rs b/zk-sdk/src/sigma_proofs/zero_balance_proof.rs new file mode 100644 index 00000000000000..3585978c76c1df --- /dev/null +++ b/zk-sdk/src/sigma_proofs/zero_balance_proof.rs @@ -0,0 +1,302 @@ +//! The zero-balance sigma proof system. +//! +//! The protocol guarantees computationally 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::{ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey}, + pedersen::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, ZeroBalanceProofVerificationError}, + transcript::TranscriptProtocol, + }, + curve25519_dalek::{ + ristretto::{CompressedRistretto, RistrettoPoint}, + scalar::Scalar, + traits::IsIdentity, + }, + merlin::Transcript, +}; + +/// Byte length of a zero-balance proof. +const ZERO_BALANCE_PROOF_LEN: usize = UNIT_LEN * 3; + +/// 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 { + Y_P: CompressedRistretto, + Y_D: CompressedRistretto, + z: Scalar, +} + +#[allow(non_snake_case)] +#[cfg(not(target_os = "solana"))] +impl ZeroBalanceProof { + /// Creates a zero-balance proof. + /// + /// 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, + ciphertext: &ElGamalCiphertext, + transcript: &mut Transcript, + ) -> Self { + transcript.zero_balance_proof_domain_separator(); + + // extract the relevant scalar and Ristretto points from the input + let P = elgamal_keypair.pubkey().get_point(); + let s = elgamal_keypair.secret().get_scalar(); + let D = ciphertext.handle.get_point(); + + // generate a random masking factor that also serves as a nonce + let mut y = Scalar::random(&mut OsRng); + let Y_P = (&y * P).compress(); + let Y_D = (&y * D).compress(); + + // 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; + + // zeroize random scalar + y.zeroize(); + + Self { Y_P, Y_D, z } + } + + /// Verifies a zero-balance proof. + /// + /// * `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, + ciphertext: &ElGamalCiphertext, + transcript: &mut Transcript, + ) -> Result<(), ZeroBalanceProofVerificationError> { + transcript.zero_balance_proof_domain_separator(); + + // extract the relevant scalar and Ristretto points from the input + let P = elgamal_pubkey.get_point(); + 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 batch verification + + let w_negated = -&w; + + // decompress Y or return verification error + let Y_P = self + .Y_P + .decompress() + .ok_or(SigmaProofVerificationError::Deserialization)?; + let Y_D = self + .Y_D + .decompress() + .ok_or(SigmaProofVerificationError::Deserialization)?; + + // check the required algebraic relation + let check = RistrettoPoint::multiscalar_mul( + 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() { + Ok(()) + } else { + Err(SigmaProofVerificationError::AlgebraicRelation.into()) + } + } + + pub fn to_bytes(&self) -> [u8; ZERO_BALANCE_PROOF_LEN] { + let mut buf = [0_u8; ZERO_BALANCE_PROOF_LEN]; + let mut chunks = buf.chunks_mut(UNIT_LEN); + chunks.next().unwrap().copy_from_slice(self.Y_P.as_bytes()); + chunks.next().unwrap().copy_from_slice(self.Y_D.as_bytes()); + chunks.next().unwrap().copy_from_slice(self.z.as_bytes()); + buf + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + let mut chunks = bytes.chunks(UNIT_LEN); + let Y_P = ristretto_point_from_optional_slice(chunks.next())?; + let Y_D = ristretto_point_from_optional_slice(chunks.next())?; + let z = canonical_scalar_from_optional_slice(chunks.next())?; + Ok(ZeroBalanceProof { Y_P, Y_D, z }) + } +} + +#[cfg(test)] +mod test { + use { + super::*, + crate::encryption::{ + elgamal::{DecryptHandle, ElGamalKeypair}, + pedersen::{Pedersen, PedersenCommitment, PedersenOpening}, + }, + }; + + #[test] + fn test_zero_balance_proof_correctness() { + let source_keypair = ElGamalKeypair::new_rand(); + + let mut prover_transcript = Transcript::new(b"test"); + let mut verifier_transcript = Transcript::new(b"test"); + + // general case: encryption of 0 + let elgamal_ciphertext = source_keypair.pubkey().encrypt(0_u64); + let proof = + ZeroBalanceProof::new(&source_keypair, &elgamal_ciphertext, &mut prover_transcript); + assert!(proof + .verify( + source_keypair.pubkey(), + &elgamal_ciphertext, + &mut verifier_transcript + ) + .is_ok()); + + // general case: encryption of > 0 + let elgamal_ciphertext = source_keypair.pubkey().encrypt(1_u64); + let proof = + ZeroBalanceProof::new(&source_keypair, &elgamal_ciphertext, &mut prover_transcript); + assert!(proof + .verify( + source_keypair.pubkey(), + &elgamal_ciphertext, + &mut verifier_transcript + ) + .is_err()); + } + + #[test] + fn test_zero_balance_proof_edge_cases() { + let source_keypair = ElGamalKeypair::new_rand(); + + let mut prover_transcript = Transcript::new(b"test"); + let mut verifier_transcript = Transcript::new(b"test"); + + // 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 prover_transcript); + + assert!(proof + .verify( + source_keypair.pubkey(), + &ciphertext, + &mut verifier_transcript + ) + .is_ok()); + + // if only either commitment or handle is zero, the ciphertext is always invalid and proof + // verification should always reject + let mut prover_transcript = Transcript::new(b"test"); + let mut verifier_transcript = Transcript::new(b"test"); + + let zeroed_commitment = PedersenCommitment::from_bytes(&[0u8; 32]).unwrap(); + let handle = source_keypair + .pubkey() + .decrypt_handle(&PedersenOpening::new_rand()); + + let ciphertext = ElGamalCiphertext { + commitment: zeroed_commitment, + handle, + }; + + let proof = ZeroBalanceProof::new(&source_keypair, &ciphertext, &mut prover_transcript); + + assert!(proof + .verify( + source_keypair.pubkey(), + &ciphertext, + &mut verifier_transcript + ) + .is_err()); + + let mut prover_transcript = Transcript::new(b"test"); + let mut verifier_transcript = 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, &ciphertext, &mut prover_transcript); + + assert!(proof + .verify( + source_keypair.pubkey(), + &ciphertext, + &mut verifier_transcript + ) + .is_err()); + + // if public key is always zero, then the proof should always reject + let mut prover_transcript = Transcript::new(b"test"); + let mut verifier_transcript = Transcript::new(b"test"); + + let public = ElGamalPubkey::try_from([0u8; 32].as_slice()).unwrap(); + let ciphertext = public.encrypt(0_u64); + + let proof = ZeroBalanceProof::new(&source_keypair, &ciphertext, &mut prover_transcript); + + assert!(proof + .verify( + source_keypair.pubkey(), + &ciphertext, + &mut verifier_transcript + ) + .is_err()); + } +} diff --git a/zk-sdk/src/transcript.rs b/zk-sdk/src/transcript.rs new file mode 100644 index 00000000000000..459738ff0beff8 --- /dev/null +++ b/zk-sdk/src/transcript.rs @@ -0,0 +1,143 @@ +use { + crate::errors::TranscriptError, + curve25519_dalek::{ristretto::CompressedRistretto, scalar::Scalar, traits::IsIdentity}, + merlin::Transcript, +}; + +pub trait TranscriptProtocol { + /// Append a domain separator for an `n`-bit rangeproof for ElGamalKeypair + /// ciphertext using a decryption key + fn rangeproof_from_key_domain_separator(&mut self, n: u64); + + /// Append a domain separator for an `n`-bit rangeproof for ElGamalKeypair + /// ciphertext using an opening + fn rangeproof_from_opening_domain_separator(&mut self, n: u64); + + /// Append a domain separator for a length-`n` inner product proof. + fn innerproduct_domain_separator(&mut self, n: u64); + + /// Append a domain separator for close account proof. + fn close_account_proof_domain_separator(&mut self); + + /// Append a domain separator for withdraw proof. + fn withdraw_proof_domain_separator(&mut self); + + /// Append a domain separator for transfer proof. + fn transfer_proof_domain_separator(&mut self); + + /// Append a `scalar` with the given `label`. + fn append_scalar(&mut self, label: &'static [u8], scalar: &Scalar); + + /// Append a `point` with the given `label`. + fn append_point(&mut self, label: &'static [u8], point: &CompressedRistretto); + + /// Append a domain separator for equality proof. + fn equality_proof_domain_separator(&mut self); + + /// Append a domain separator for zero-balance proof. + fn zero_balance_proof_domain_separator(&mut self); + + /// Append a domain separator for grouped ciphertext validity proof. + fn grouped_ciphertext_validity_proof_domain_separator(&mut self); + + /// Append a domain separator for batched grouped ciphertext validity proof. + fn batched_grouped_ciphertext_validity_proof_domain_separator(&mut self); + + /// Append a domain separator for fee sigma proof. + fn fee_sigma_proof_domain_separator(&mut self); + + /// Append a domain separator for public-key proof. + fn pubkey_proof_domain_separator(&mut self); + + /// Check that a point is not the identity, then append it to the + /// transcript. Otherwise, return an error. + fn validate_and_append_point( + &mut self, + label: &'static [u8], + point: &CompressedRistretto, + ) -> Result<(), TranscriptError>; + + /// Compute a `label`ed challenge variable. + fn challenge_scalar(&mut self, label: &'static [u8]) -> Scalar; +} + +impl TranscriptProtocol for Transcript { + fn rangeproof_from_key_domain_separator(&mut self, n: u64) { + self.append_message(b"dom-sep", b"rangeproof from opening v1"); + self.append_u64(b"n", n); + } + + fn rangeproof_from_opening_domain_separator(&mut self, n: u64) { + self.append_message(b"dom-sep", b"rangeproof from opening v1"); + self.append_u64(b"n", n); + } + + fn innerproduct_domain_separator(&mut self, n: u64) { + self.append_message(b"dom-sep", b"ipp v1"); + self.append_u64(b"n", n); + } + + fn close_account_proof_domain_separator(&mut self) { + self.append_message(b"dom-sep", b"CloseAccountProof"); + } + + fn withdraw_proof_domain_separator(&mut self) { + self.append_message(b"dom-sep", b"WithdrawProof"); + } + + fn transfer_proof_domain_separator(&mut self) { + self.append_message(b"dom-sep", b"TransferProof"); + } + + fn append_scalar(&mut self, label: &'static [u8], scalar: &Scalar) { + self.append_message(label, scalar.as_bytes()); + } + + fn append_point(&mut self, label: &'static [u8], point: &CompressedRistretto) { + self.append_message(label, point.as_bytes()); + } + + fn validate_and_append_point( + &mut self, + label: &'static [u8], + point: &CompressedRistretto, + ) -> Result<(), TranscriptError> { + if point.is_identity() { + Err(TranscriptError::ValidationError) + } else { + self.append_message(label, point.as_bytes()); + Ok(()) + } + } + + fn challenge_scalar(&mut self, label: &'static [u8]) -> Scalar { + let mut buf = [0u8; 64]; + self.challenge_bytes(label, &mut buf); + + Scalar::from_bytes_mod_order_wide(&buf) + } + + fn equality_proof_domain_separator(&mut self) { + self.append_message(b"dom-sep", b"equality-proof") + } + + fn zero_balance_proof_domain_separator(&mut self) { + self.append_message(b"dom-sep", b"zero-balance-proof") + } + + fn grouped_ciphertext_validity_proof_domain_separator(&mut self) { + self.append_message(b"dom-sep", b"validity-proof") + } + + fn batched_grouped_ciphertext_validity_proof_domain_separator(&mut self) { + self.append_message(b"dom-sep", b"batched-validity-proof") + } + + fn fee_sigma_proof_domain_separator(&mut self) { + self.append_message(b"dom-sep", b"fee-sigma-proof") + } + + fn pubkey_proof_domain_separator(&mut self) { + self.append_message(b"dom-sep", b"pubkey-proof") + } +} From c2646cf82fe15b27cc05f9d649ea936ecc355018 Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Fri, 26 Apr 2024 16:40:38 +0900 Subject: [PATCH 2/4] remove `_proof` suffix from sigma proof module names --- .../handles_2.rs | 2 +- .../handles_3.rs | 2 +- .../mod.rs | 0 ..._proof.rs => ciphertext_ciphertext_equality.rs} | 0 ..._proof.rs => ciphertext_commitment_equality.rs} | 0 zk-sdk/src/sigma_proofs/{fee_proof.rs => fee.rs} | 0 .../handles_2.rs | 0 .../handles_3.rs | 0 .../mod.rs | 0 zk-sdk/src/sigma_proofs/mod.rs | 14 +++++++------- .../sigma_proofs/{pubkey_proof.rs => pubkey.rs} | 0 .../{zero_balance_proof.rs => zero_balance.rs} | 0 12 files changed, 9 insertions(+), 9 deletions(-) rename zk-sdk/src/sigma_proofs/{batched_grouped_ciphertext_validity_proof => batched_grouped_ciphertext_validity}/handles_2.rs (98%) rename zk-sdk/src/sigma_proofs/{batched_grouped_ciphertext_validity_proof => batched_grouped_ciphertext_validity}/handles_3.rs (98%) rename zk-sdk/src/sigma_proofs/{batched_grouped_ciphertext_validity_proof => batched_grouped_ciphertext_validity}/mod.rs (100%) rename zk-sdk/src/sigma_proofs/{ciphertext_ciphertext_equality_proof.rs => ciphertext_ciphertext_equality.rs} (100%) rename zk-sdk/src/sigma_proofs/{ciphertext_commitment_equality_proof.rs => ciphertext_commitment_equality.rs} (100%) rename zk-sdk/src/sigma_proofs/{fee_proof.rs => fee.rs} (100%) rename zk-sdk/src/sigma_proofs/{grouped_ciphertext_validity_proof => grouped_ciphertext_validity}/handles_2.rs (100%) rename zk-sdk/src/sigma_proofs/{grouped_ciphertext_validity_proof => grouped_ciphertext_validity}/handles_3.rs (100%) rename zk-sdk/src/sigma_proofs/{grouped_ciphertext_validity_proof => grouped_ciphertext_validity}/mod.rs (100%) rename zk-sdk/src/sigma_proofs/{pubkey_proof.rs => pubkey.rs} (100%) rename zk-sdk/src/sigma_proofs/{zero_balance_proof.rs => zero_balance.rs} (100%) diff --git a/zk-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity_proof/handles_2.rs b/zk-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity/handles_2.rs similarity index 98% rename from zk-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity_proof/handles_2.rs rename to zk-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity/handles_2.rs index 7fbe17fb195ddd..9cc9d3ae43b28f 100644 --- a/zk-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity_proof/handles_2.rs +++ b/zk-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity/handles_2.rs @@ -17,7 +17,7 @@ use { crate::{ sigma_proofs::{ errors::ValidityProofVerificationError, - grouped_ciphertext_validity_proof::GroupedCiphertext2HandlesValidityProof, + grouped_ciphertext_validity::GroupedCiphertext2HandlesValidityProof, }, transcript::TranscriptProtocol, }, diff --git a/zk-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity_proof/handles_3.rs b/zk-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity/handles_3.rs similarity index 98% rename from zk-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity_proof/handles_3.rs rename to zk-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity/handles_3.rs index 2faaa81c34421e..de1d42355fe800 100644 --- a/zk-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity_proof/handles_3.rs +++ b/zk-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity/handles_3.rs @@ -21,7 +21,7 @@ use { crate::{ sigma_proofs::{ errors::ValidityProofVerificationError, - grouped_ciphertext_validity_proof::GroupedCiphertext3HandlesValidityProof, + grouped_ciphertext_validity::GroupedCiphertext3HandlesValidityProof, }, transcript::TranscriptProtocol, UNIT_LEN, diff --git a/zk-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity_proof/mod.rs b/zk-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity/mod.rs similarity index 100% rename from zk-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity_proof/mod.rs rename to zk-sdk/src/sigma_proofs/batched_grouped_ciphertext_validity/mod.rs diff --git a/zk-sdk/src/sigma_proofs/ciphertext_ciphertext_equality_proof.rs b/zk-sdk/src/sigma_proofs/ciphertext_ciphertext_equality.rs similarity index 100% rename from zk-sdk/src/sigma_proofs/ciphertext_ciphertext_equality_proof.rs rename to zk-sdk/src/sigma_proofs/ciphertext_ciphertext_equality.rs diff --git a/zk-sdk/src/sigma_proofs/ciphertext_commitment_equality_proof.rs b/zk-sdk/src/sigma_proofs/ciphertext_commitment_equality.rs similarity index 100% rename from zk-sdk/src/sigma_proofs/ciphertext_commitment_equality_proof.rs rename to zk-sdk/src/sigma_proofs/ciphertext_commitment_equality.rs diff --git a/zk-sdk/src/sigma_proofs/fee_proof.rs b/zk-sdk/src/sigma_proofs/fee.rs similarity index 100% rename from zk-sdk/src/sigma_proofs/fee_proof.rs rename to zk-sdk/src/sigma_proofs/fee.rs diff --git a/zk-sdk/src/sigma_proofs/grouped_ciphertext_validity_proof/handles_2.rs b/zk-sdk/src/sigma_proofs/grouped_ciphertext_validity/handles_2.rs similarity index 100% rename from zk-sdk/src/sigma_proofs/grouped_ciphertext_validity_proof/handles_2.rs rename to zk-sdk/src/sigma_proofs/grouped_ciphertext_validity/handles_2.rs diff --git a/zk-sdk/src/sigma_proofs/grouped_ciphertext_validity_proof/handles_3.rs b/zk-sdk/src/sigma_proofs/grouped_ciphertext_validity/handles_3.rs similarity index 100% rename from zk-sdk/src/sigma_proofs/grouped_ciphertext_validity_proof/handles_3.rs rename to zk-sdk/src/sigma_proofs/grouped_ciphertext_validity/handles_3.rs diff --git a/zk-sdk/src/sigma_proofs/grouped_ciphertext_validity_proof/mod.rs b/zk-sdk/src/sigma_proofs/grouped_ciphertext_validity/mod.rs similarity index 100% rename from zk-sdk/src/sigma_proofs/grouped_ciphertext_validity_proof/mod.rs rename to zk-sdk/src/sigma_proofs/grouped_ciphertext_validity/mod.rs diff --git a/zk-sdk/src/sigma_proofs/mod.rs b/zk-sdk/src/sigma_proofs/mod.rs index bad707157a8c33..edf724c3047a94 100644 --- a/zk-sdk/src/sigma_proofs/mod.rs +++ b/zk-sdk/src/sigma_proofs/mod.rs @@ -8,19 +8,19 @@ pub mod errors; #[cfg(not(target_os = "solana"))] -pub mod batched_grouped_ciphertext_validity_proof; +pub mod batched_grouped_ciphertext_validity; #[cfg(not(target_os = "solana"))] -pub mod ciphertext_ciphertext_equality_proof; +pub mod ciphertext_ciphertext_equality; #[cfg(not(target_os = "solana"))] -pub mod ciphertext_commitment_equality_proof; +pub mod ciphertext_commitment_equality; #[cfg(not(target_os = "solana"))] -pub mod fee_proof; +pub mod fee; #[cfg(not(target_os = "solana"))] -pub mod grouped_ciphertext_validity_proof; +pub mod grouped_ciphertext_validity; #[cfg(not(target_os = "solana"))] -pub mod pubkey_proof; +pub mod pubkey; #[cfg(not(target_os = "solana"))] -pub mod zero_balance_proof; +pub mod zero_balance; #[cfg(not(target_os = "solana"))] use { diff --git a/zk-sdk/src/sigma_proofs/pubkey_proof.rs b/zk-sdk/src/sigma_proofs/pubkey.rs similarity index 100% rename from zk-sdk/src/sigma_proofs/pubkey_proof.rs rename to zk-sdk/src/sigma_proofs/pubkey.rs diff --git a/zk-sdk/src/sigma_proofs/zero_balance_proof.rs b/zk-sdk/src/sigma_proofs/zero_balance.rs similarity index 100% rename from zk-sdk/src/sigma_proofs/zero_balance_proof.rs rename to zk-sdk/src/sigma_proofs/zero_balance.rs From a07b08778844ffb8529dd3831bad689f15fdb10e Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Fri, 26 Apr 2024 16:45:50 +0900 Subject: [PATCH 3/4] remove the `sigma_proofs` and `transcript` modules from sbf target --- zk-sdk/src/lib.rs | 3 ++- zk-sdk/src/sigma_proofs/mod.rs | 8 -------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/zk-sdk/src/lib.rs b/zk-sdk/src/lib.rs index d25df67c6e085d..8119c35acfc30c 100644 --- a/zk-sdk/src/lib.rs +++ b/zk-sdk/src/lib.rs @@ -21,9 +21,10 @@ #[cfg(not(target_os = "solana"))] pub mod encryption; - pub mod errors; +#[cfg(not(target_os = "solana"))] mod sigma_proofs; +#[cfg(not(target_os = "solana"))] mod transcript; /// Byte length of a compressed Ristretto point or scalar in Curve255519 diff --git a/zk-sdk/src/sigma_proofs/mod.rs b/zk-sdk/src/sigma_proofs/mod.rs index edf724c3047a94..b04516d5791d72 100644 --- a/zk-sdk/src/sigma_proofs/mod.rs +++ b/zk-sdk/src/sigma_proofs/mod.rs @@ -7,22 +7,14 @@ pub mod errors; -#[cfg(not(target_os = "solana"))] pub mod batched_grouped_ciphertext_validity; -#[cfg(not(target_os = "solana"))] pub mod ciphertext_ciphertext_equality; -#[cfg(not(target_os = "solana"))] pub mod ciphertext_commitment_equality; -#[cfg(not(target_os = "solana"))] pub mod fee; -#[cfg(not(target_os = "solana"))] pub mod grouped_ciphertext_validity; -#[cfg(not(target_os = "solana"))] pub mod pubkey; -#[cfg(not(target_os = "solana"))] pub mod zero_balance; -#[cfg(not(target_os = "solana"))] use { crate::{sigma_proofs::errors::SigmaProofVerificationError, RISTRETTO_POINT_LEN, SCALAR_LEN}, curve25519_dalek::{ristretto::CompressedRistretto, scalar::Scalar}, From 4e594dd4bdf4983b584765f64bd342ef71bc3b49 Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Fri, 26 Apr 2024 16:46:39 +0900 Subject: [PATCH 4/4] allow `dead_code` and `unused_imports` until the `instruction` module is added --- zk-sdk/src/sigma_proofs/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/zk-sdk/src/sigma_proofs/mod.rs b/zk-sdk/src/sigma_proofs/mod.rs index b04516d5791d72..343b1583ac7a3e 100644 --- a/zk-sdk/src/sigma_proofs/mod.rs +++ b/zk-sdk/src/sigma_proofs/mod.rs @@ -5,6 +5,8 @@ //! //! [`ZK Token proof`]: https://docs.solanalabs.com/runtime/zk-token-proof +#![allow(dead_code, unused_imports)] + pub mod errors; pub mod batched_grouped_ciphertext_validity;