solana_zk_sdk/sigma_proofs/
ciphertext_commitment_equality.rs

1//! The ciphertext-commitment equality sigma proof system.
2//!
3//! A ciphertext-commitment equality proof is defined with respect to a twisted ElGamal ciphertext
4//! and a Pedersen commitment. The proof certifies that a given ElGamal ciphertext and Pedersen
5//! commitment pair encrypt and encode the same message. To generate the proof, a prover must provide
6//! the decryption key for the first ciphertext and the Pedersen opening for the commitment.
7//!
8//! The protocol guarantees computational soundness (by the hardness of discrete log) and perfect
9//! zero-knowledge in the random oracle model.
10
11#[cfg(target_arch = "wasm32")]
12use wasm_bindgen::prelude::*;
13#[cfg(not(target_os = "solana"))]
14use {
15    crate::{
16        encryption::{
17            elgamal::{ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey},
18            pedersen::{PedersenCommitment, PedersenOpening, G, H},
19        },
20        sigma_proofs::{canonical_scalar_from_optional_slice, ristretto_point_from_optional_slice},
21        UNIT_LEN,
22    },
23    curve25519_dalek::traits::MultiscalarMul,
24    rand::rngs::OsRng,
25    zeroize::Zeroize,
26};
27use {
28    crate::{
29        sigma_proofs::errors::{EqualityProofVerificationError, SigmaProofVerificationError},
30        transcript::TranscriptProtocol,
31    },
32    curve25519_dalek::{
33        ristretto::{CompressedRistretto, RistrettoPoint},
34        scalar::Scalar,
35        traits::{IsIdentity, VartimeMultiscalarMul},
36    },
37    merlin::Transcript,
38};
39
40/// Byte length of a ciphertext-commitment equality proof.
41const CIPHERTEXT_COMMITMENT_EQUALITY_PROOF_LEN: usize = UNIT_LEN * 6;
42
43/// Equality proof.
44///
45/// Contains all the elliptic curve and scalar components that make up the sigma protocol.
46#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
47#[allow(non_snake_case)]
48#[derive(Clone)]
49pub struct CiphertextCommitmentEqualityProof {
50    Y_0: CompressedRistretto,
51    Y_1: CompressedRistretto,
52    Y_2: CompressedRistretto,
53    z_s: Scalar,
54    z_x: Scalar,
55    z_r: Scalar,
56}
57
58#[allow(non_snake_case)]
59#[cfg(not(target_os = "solana"))]
60impl CiphertextCommitmentEqualityProof {
61    /// Creates a ciphertext-commitment equality proof.
62    ///
63    /// The function does *not* hash the public key, ciphertext, or commitment into the transcript.
64    /// For security, the caller (the main protocol) should hash these public components prior to
65    /// invoking this constructor.
66    ///
67    /// This function is randomized. It uses `OsRng` internally to generate random scalars.
68    ///
69    /// Note that the proof constructor does not take the actual Pedersen commitment as input; it
70    /// takes the associated Pedersen opening instead.
71    ///
72    /// * `keypair` - The ElGamal keypair associated with the first to be proved
73    /// * `ciphertext` - The main ElGamal ciphertext to be proved
74    /// * `opening` - The opening associated with the main Pedersen commitment to be proved
75    /// * `amount` - The message associated with the ElGamal ciphertext and Pedersen commitment
76    /// * `transcript` - The transcript that does the bookkeeping for the Fiat-Shamir heuristic
77    pub fn new(
78        keypair: &ElGamalKeypair,
79        ciphertext: &ElGamalCiphertext,
80        opening: &PedersenOpening,
81        amount: u64,
82        transcript: &mut Transcript,
83    ) -> Self {
84        transcript.ciphertext_commitment_equality_proof_domain_separator();
85
86        // extract the relevant scalar and Ristretto points from the inputs
87        let P = keypair.pubkey().get_point();
88        let D = ciphertext.handle.get_point();
89
90        let s = keypair.secret().get_scalar();
91        let mut x = Scalar::from(amount);
92        let r = opening.get_scalar();
93
94        // generate random masking factors that also serves as nonces
95        let mut y_s = Scalar::random(&mut OsRng);
96        let mut y_x = Scalar::random(&mut OsRng);
97        let mut y_r = Scalar::random(&mut OsRng);
98
99        let Y_0 = (&y_s * P).compress();
100        let Y_1 = RistrettoPoint::multiscalar_mul(vec![&y_x, &y_s], vec![&G, D]).compress();
101        let Y_2 = RistrettoPoint::multiscalar_mul(vec![&y_x, &y_r], vec![&G, &(*H)]).compress();
102
103        // record masking factors in the transcript
104        transcript.append_point(b"Y_0", &Y_0);
105        transcript.append_point(b"Y_1", &Y_1);
106        transcript.append_point(b"Y_2", &Y_2);
107
108        let c = transcript.challenge_scalar(b"c");
109
110        // compute the masked values
111        let z_s = &(&c * s) + &y_s;
112        let z_x = &(&c * &x) + &y_x;
113        let z_r = &(&c * r) + &y_r;
114
115        // compute challenge `w` for consistency with verification
116        transcript.append_scalar(b"z_s", &z_s);
117        transcript.append_scalar(b"z_x", &z_x);
118        transcript.append_scalar(b"z_r", &z_r);
119        let _w = transcript.challenge_scalar(b"w");
120
121        // zeroize random scalars
122        x.zeroize();
123        y_s.zeroize();
124        y_x.zeroize();
125        y_r.zeroize();
126
127        CiphertextCommitmentEqualityProof {
128            Y_0,
129            Y_1,
130            Y_2,
131            z_s,
132            z_x,
133            z_r,
134        }
135    }
136
137    /// Verifies a ciphertext-commitment equality proof.
138    ///
139    /// * `pubkey` - The ElGamal pubkey associated with the ciphertext to be proved
140    /// * `ciphertext` - The main ElGamal ciphertext to be proved
141    /// * `commitment` - The main Pedersen commitment to be proved
142    /// * `transcript` - The transcript that does the bookkeeping for the Fiat-Shamir heuristic
143    pub fn verify(
144        self,
145        pubkey: &ElGamalPubkey,
146        ciphertext: &ElGamalCiphertext,
147        commitment: &PedersenCommitment,
148        transcript: &mut Transcript,
149    ) -> Result<(), EqualityProofVerificationError> {
150        transcript.ciphertext_commitment_equality_proof_domain_separator();
151
152        // extract the relevant scalar and Ristretto points from the inputs
153        let P = pubkey.get_point();
154        let C_ciphertext = ciphertext.commitment.get_point();
155        let D = ciphertext.handle.get_point();
156        let C_commitment = commitment.get_point();
157
158        // include Y_0, Y_1, Y_2 to transcript and extract challenges
159        transcript.validate_and_append_point(b"Y_0", &self.Y_0)?;
160        transcript.validate_and_append_point(b"Y_1", &self.Y_1)?;
161        transcript.validate_and_append_point(b"Y_2", &self.Y_2)?;
162
163        let c = transcript.challenge_scalar(b"c");
164
165        transcript.append_scalar(b"z_s", &self.z_s);
166        transcript.append_scalar(b"z_x", &self.z_x);
167        transcript.append_scalar(b"z_r", &self.z_r);
168        let w = transcript.challenge_scalar(b"w"); // w used for batch verification
169        let ww = &w * &w;
170
171        let w_negated = -&w;
172        let ww_negated = -&ww;
173
174        // check that the required algebraic condition holds
175        let Y_0 = self
176            .Y_0
177            .decompress()
178            .ok_or(SigmaProofVerificationError::Deserialization)?;
179        let Y_1 = self
180            .Y_1
181            .decompress()
182            .ok_or(SigmaProofVerificationError::Deserialization)?;
183        let Y_2 = self
184            .Y_2
185            .decompress()
186            .ok_or(SigmaProofVerificationError::Deserialization)?;
187
188        let check = RistrettoPoint::vartime_multiscalar_mul(
189            vec![
190                &self.z_s,           // z_s
191                &(-&c),              // -c
192                &(-&Scalar::ONE),    // -identity
193                &(&w * &self.z_x),   // w * z_x
194                &(&w * &self.z_s),   // w * z_s
195                &(&w_negated * &c),  // -w * c
196                &w_negated,          // -w
197                &(&ww * &self.z_x),  // ww * z_x
198                &(&ww * &self.z_r),  // ww * z_r
199                &(&ww_negated * &c), // -ww * c
200                &ww_negated,         // -ww
201            ],
202            vec![
203                P,            // P
204                &(*H),        // H
205                &Y_0,         // Y_0
206                &G,           // G
207                D,            // D
208                C_ciphertext, // C_ciphertext
209                &Y_1,         // Y_1
210                &G,           // G
211                &(*H),        // H
212                C_commitment, // C_commitment
213                &Y_2,         // Y_2
214            ],
215        );
216
217        if check.is_identity() {
218            Ok(())
219        } else {
220            Err(SigmaProofVerificationError::AlgebraicRelation.into())
221        }
222    }
223
224    pub fn to_bytes(&self) -> [u8; CIPHERTEXT_COMMITMENT_EQUALITY_PROOF_LEN] {
225        let mut buf = [0_u8; CIPHERTEXT_COMMITMENT_EQUALITY_PROOF_LEN];
226        let mut chunks = buf.chunks_mut(UNIT_LEN);
227        chunks.next().unwrap().copy_from_slice(self.Y_0.as_bytes());
228        chunks.next().unwrap().copy_from_slice(self.Y_1.as_bytes());
229        chunks.next().unwrap().copy_from_slice(self.Y_2.as_bytes());
230        chunks.next().unwrap().copy_from_slice(self.z_s.as_bytes());
231        chunks.next().unwrap().copy_from_slice(self.z_x.as_bytes());
232        chunks.next().unwrap().copy_from_slice(self.z_r.as_bytes());
233        buf
234    }
235
236    pub fn from_bytes(bytes: &[u8]) -> Result<Self, EqualityProofVerificationError> {
237        let mut chunks = bytes.chunks(UNIT_LEN);
238        let Y_0 = ristretto_point_from_optional_slice(chunks.next())?;
239        let Y_1 = ristretto_point_from_optional_slice(chunks.next())?;
240        let Y_2 = ristretto_point_from_optional_slice(chunks.next())?;
241        let z_s = canonical_scalar_from_optional_slice(chunks.next())?;
242        let z_x = canonical_scalar_from_optional_slice(chunks.next())?;
243        let z_r = canonical_scalar_from_optional_slice(chunks.next())?;
244
245        Ok(CiphertextCommitmentEqualityProof {
246            Y_0,
247            Y_1,
248            Y_2,
249            z_s,
250            z_x,
251            z_r,
252        })
253    }
254}
255
256#[cfg(test)]
257mod test {
258    use {
259        super::*,
260        crate::{
261            encryption::{
262                elgamal::ElGamalSecretKey,
263                pedersen::Pedersen,
264                pod::{
265                    elgamal::{PodElGamalCiphertext, PodElGamalPubkey},
266                    pedersen::PodPedersenCommitment,
267                },
268            },
269            sigma_proofs::pod::PodCiphertextCommitmentEqualityProof,
270        },
271        std::str::FromStr,
272    };
273
274    #[test]
275    fn test_ciphertext_commitment_equality_proof_correctness() {
276        // success case
277        let keypair = ElGamalKeypair::new_rand();
278        let message: u64 = 55;
279
280        let ciphertext = keypair.pubkey().encrypt(message);
281        let (commitment, opening) = Pedersen::new(message);
282
283        let mut prover_transcript = Transcript::new(b"Test");
284        let mut verifier_transcript = Transcript::new(b"Test");
285
286        let proof = CiphertextCommitmentEqualityProof::new(
287            &keypair,
288            &ciphertext,
289            &opening,
290            message,
291            &mut prover_transcript,
292        );
293
294        proof
295            .verify(
296                keypair.pubkey(),
297                &ciphertext,
298                &commitment,
299                &mut verifier_transcript,
300            )
301            .unwrap();
302
303        // fail case: encrypted and committed messages are different
304        let keypair = ElGamalKeypair::new_rand();
305        let encrypted_message: u64 = 55;
306        let committed_message: u64 = 77;
307
308        let ciphertext = keypair.pubkey().encrypt(encrypted_message);
309        let (commitment, opening) = Pedersen::new(committed_message);
310
311        let mut prover_transcript = Transcript::new(b"Test");
312        let mut verifier_transcript = Transcript::new(b"Test");
313
314        let proof = CiphertextCommitmentEqualityProof::new(
315            &keypair,
316            &ciphertext,
317            &opening,
318            encrypted_message,
319            &mut prover_transcript,
320        );
321
322        assert!(proof
323            .verify(
324                keypair.pubkey(),
325                &ciphertext,
326                &commitment,
327                &mut verifier_transcript
328            )
329            .is_err());
330
331        assert_eq!(
332            prover_transcript.challenge_scalar(b"test"),
333            verifier_transcript.challenge_scalar(b"test"),
334        )
335    }
336
337    #[test]
338    fn test_ciphertext_commitment_equality_proof_edge_cases() {
339        // if ElGamal public key zero (public key is invalid), then the proof should always reject
340        let public = ElGamalPubkey::try_from([0u8; 32].as_slice()).unwrap();
341        let secret = ElGamalSecretKey::new_rand();
342
343        let elgamal_keypair = ElGamalKeypair::new_for_tests(public, secret);
344
345        let message: u64 = 55;
346        let ciphertext = elgamal_keypair.pubkey().encrypt(message);
347        let (commitment, opening) = Pedersen::new(message);
348
349        let mut prover_transcript = Transcript::new(b"Test");
350        let mut verifier_transcript = Transcript::new(b"Test");
351
352        let proof = CiphertextCommitmentEqualityProof::new(
353            &elgamal_keypair,
354            &ciphertext,
355            &opening,
356            message,
357            &mut prover_transcript,
358        );
359
360        assert!(proof
361            .verify(
362                elgamal_keypair.pubkey(),
363                &ciphertext,
364                &commitment,
365                &mut verifier_transcript
366            )
367            .is_err());
368
369        // if ciphertext is all-zero (valid commitment of 0) and commitment is also all-zero, then
370        // the proof should still accept
371        let elgamal_keypair = ElGamalKeypair::new_rand();
372
373        let message: u64 = 0;
374        let ciphertext = ElGamalCiphertext::from_bytes(&[0u8; 64]).unwrap();
375        let commitment = PedersenCommitment::from_bytes(&[0u8; 32]).unwrap();
376        let opening = PedersenOpening::from_bytes(&[0u8; 32]).unwrap();
377
378        let mut prover_transcript = Transcript::new(b"Test");
379        let mut verifier_transcript = Transcript::new(b"Test");
380
381        let proof = CiphertextCommitmentEqualityProof::new(
382            &elgamal_keypair,
383            &ciphertext,
384            &opening,
385            message,
386            &mut prover_transcript,
387        );
388
389        proof
390            .verify(
391                elgamal_keypair.pubkey(),
392                &ciphertext,
393                &commitment,
394                &mut verifier_transcript,
395            )
396            .unwrap();
397
398        // if commitment is all-zero and the ciphertext is a correct encryption of 0, then the
399        // proof should still accept
400        let elgamal_keypair = ElGamalKeypair::new_rand();
401
402        let message: u64 = 0;
403        let ciphertext = elgamal_keypair.pubkey().encrypt(message);
404        let commitment = PedersenCommitment::from_bytes(&[0u8; 32]).unwrap();
405        let opening = PedersenOpening::from_bytes(&[0u8; 32]).unwrap();
406
407        let mut prover_transcript = Transcript::new(b"Test");
408        let mut verifier_transcript = Transcript::new(b"Test");
409
410        let proof = CiphertextCommitmentEqualityProof::new(
411            &elgamal_keypair,
412            &ciphertext,
413            &opening,
414            message,
415            &mut prover_transcript,
416        );
417
418        proof
419            .verify(
420                elgamal_keypair.pubkey(),
421                &ciphertext,
422                &commitment,
423                &mut verifier_transcript,
424            )
425            .unwrap();
426
427        // if ciphertext is all zero and commitment correctly encodes 0, then the proof should
428        // still accept
429        let elgamal_keypair = ElGamalKeypair::new_rand();
430
431        let message: u64 = 0;
432        let ciphertext = ElGamalCiphertext::from_bytes(&[0u8; 64]).unwrap();
433        let (commitment, opening) = Pedersen::new(message);
434
435        let mut prover_transcript = Transcript::new(b"Test");
436        let mut verifier_transcript = Transcript::new(b"Test");
437
438        let proof = CiphertextCommitmentEqualityProof::new(
439            &elgamal_keypair,
440            &ciphertext,
441            &opening,
442            message,
443            &mut prover_transcript,
444        );
445
446        proof
447            .verify(
448                elgamal_keypair.pubkey(),
449                &ciphertext,
450                &commitment,
451                &mut verifier_transcript,
452            )
453            .unwrap();
454    }
455
456    #[test]
457    fn test_ciphertext_commitment_equality_proof_string() {
458        let pubkey_str = "JNa7rRrDm35laU7f8HPds1PmHoZEPSHFK/M+aTtEhAk=";
459        let pod_pubkey = PodElGamalPubkey::from_str(pubkey_str).unwrap();
460        let pubkey: ElGamalPubkey = pod_pubkey.try_into().unwrap();
461
462        let ciphertext_str = "RAXnbQ/DPRlYAWmD+iHRNqMDv7oQcPgQ7OejRzj4bxVy2qOJNziqqDOC7VP3iTW1+z/jckW4smA3EUF7i/r8Rw==";
463        let pod_ciphertext = PodElGamalCiphertext::from_str(ciphertext_str).unwrap();
464        let ciphertext: ElGamalCiphertext = pod_ciphertext.try_into().unwrap();
465
466        let commitment_str = "ngPTYvbY9P5l6aOfr7bLQiI+0HZsw8GBgiumdW3tNzw=";
467        let pod_commitment = PodPedersenCommitment::from_str(commitment_str).unwrap();
468        let commitment: PedersenCommitment = pod_commitment.try_into().unwrap();
469
470        let proof_str = "cCZySLxB2XJdGyDvckVBm2OWiXqf7Jf54IFoDuLJ4G+ySj+lh5DbaDMHDhuozQC9tDWtk2mFITuaXOc5Zw3nZ2oEvVYpqv5hN+k5dx9k8/nZKabUCkZwx310z7x4fE4Np5SY9PYia1hkrq9AWq0b3v97XvW1+XCSSxuflvBk5wsdaQQ+ZgcmPnKWKjHfRwmU2k5iVgYzs2VmvZa5E3OWBoM/M2yFNvukY+FCC2YMnspO0c4lNBr/vDFQuHdW0OgJ";
471        let pod_proof = PodCiphertextCommitmentEqualityProof::from_str(proof_str).unwrap();
472        let proof: CiphertextCommitmentEqualityProof = pod_proof.try_into().unwrap();
473
474        let mut verifier_transcript = Transcript::new(b"Test");
475
476        proof
477            .verify(&pubkey, &ciphertext, &commitment, &mut verifier_transcript)
478            .unwrap();
479    }
480}