arcis_compiler/utils/zkp/
ciphertext_commitment_equality.rs

1//! Arcis implementation of https://github.com/solana-program/zk-elgamal-proof/blob/main/zk-sdk/src/sigma_proofs/ciphertext_commitment_equality.rs
2
3use crate::{
4    core::{
5        circuits::boolean::{boolean_value::BooleanValue, byte::Byte},
6        global_value::{
7            curve_value::{CompressedCurveValue, CurveValue},
8            value::FieldValue,
9        },
10    },
11    traits::{Reveal, ToLeBytes},
12    utils::{
13        field::ScalarField,
14        zkp::{
15            elgamal::{ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey},
16            pedersen::{PedersenCommitment, PedersenOpening},
17            transcript::Transcript,
18            util::UNIT_LEN,
19        },
20    },
21};
22use std::sync::LazyLock;
23use zk_elgamal_proof::encryption::{
24    pedersen::H,
25    DECRYPT_HANDLE_LEN,
26    ELGAMAL_CIPHERTEXT_LEN,
27    ELGAMAL_PUBKEY_LEN,
28    PEDERSEN_COMMITMENT_LEN,
29};
30
31pub const CIPHERTEXT_COMMITMENT_EQUALITY_PROOF_LEN: usize = 192;
32
33pub const CIPHERTEXT_COMMITMENT_EQUALITY_PROOF_CONTEXT_LEN: usize =
34    ELGAMAL_PUBKEY_LEN + ELGAMAL_CIPHERTEXT_LEN + PEDERSEN_COMMITMENT_LEN;
35
36pub const CIPHERTEXT_COMMITMENT_EQUALITY_PROOF_DATA_LEN: usize =
37    CIPHERTEXT_COMMITMENT_EQUALITY_PROOF_CONTEXT_LEN + CIPHERTEXT_COMMITMENT_EQUALITY_PROOF_LEN;
38
39/// The instruction data that is needed for the
40/// `ProofInstruction::VerifyCiphertextCommitmentEquality` instruction.
41///
42/// It includes the cryptographic proof as well as the context data information needed to verify
43/// the proof.
44#[derive(Clone, Copy)]
45pub struct CiphertextCommitmentEqualityProofData {
46    pub context: CiphertextCommitmentEqualityProofContext,
47    pub proof: CiphertextCommitmentEqualityProof,
48}
49
50/// The context data needed to verify a ciphertext-commitment equality proof.
51#[derive(Clone, Copy)]
52pub struct CiphertextCommitmentEqualityProofContext {
53    /// The ElGamal pubkey
54    pub pubkey: ElGamalPubkey,
55    /// The ciphertext encrypted under the ElGamal pubkey
56    pub ciphertext: ElGamalCiphertext,
57    /// The Pedersen commitment
58    pub commitment: PedersenCommitment,
59}
60
61impl CiphertextCommitmentEqualityProofContext {
62    pub fn new_transcript(&self) -> Transcript<BooleanValue> {
63        let mut transcript = Transcript::new(b"ciphertext-commitment-equality-instruction");
64        transcript.append_point(b"pubkey", &self.pubkey.get_point().compress());
65        transcript.append_elgamal_ciphertext(b"ciphertext", &self.ciphertext);
66        transcript.append_point(b"commitment", &self.commitment.get_point().compress());
67        transcript
68    }
69
70    pub fn to_bytes(
71        &self,
72    ) -> [Byte<BooleanValue>; CIPHERTEXT_COMMITMENT_EQUALITY_PROOF_CONTEXT_LEN] {
73        let mut bytes =
74            [Byte::<BooleanValue>::from(0); CIPHERTEXT_COMMITMENT_EQUALITY_PROOF_CONTEXT_LEN];
75        bytes[..ELGAMAL_PUBKEY_LEN].copy_from_slice(&self.pubkey.get_point().compress().to_bytes());
76        let mut offset = ELGAMAL_PUBKEY_LEN;
77        bytes[offset..offset + PEDERSEN_COMMITMENT_LEN]
78            .copy_from_slice(&self.ciphertext.commitment.get_point().compress().to_bytes());
79        offset += PEDERSEN_COMMITMENT_LEN;
80        bytes[offset..offset + DECRYPT_HANDLE_LEN]
81            .copy_from_slice(&self.ciphertext.handle.get_point().compress().to_bytes());
82        offset += DECRYPT_HANDLE_LEN;
83        bytes[offset..offset + PEDERSEN_COMMITMENT_LEN]
84            .copy_from_slice(&self.commitment.get_point().compress().to_bytes());
85        bytes
86    }
87}
88
89impl CiphertextCommitmentEqualityProofData {
90    pub fn new(
91        keypair: &ElGamalKeypair,
92        ciphertext: &ElGamalCiphertext,
93        commitment: &PedersenCommitment,
94        opening: &PedersenOpening,
95        amount: FieldValue<ScalarField>,
96    ) -> Self {
97        let context = CiphertextCommitmentEqualityProofContext {
98            pubkey: *keypair.pubkey(),
99            ciphertext: *ciphertext,
100            commitment: *commitment,
101        };
102        let mut transcript = context.new_transcript();
103        let proof = CiphertextCommitmentEqualityProof::new(
104            keypair,
105            ciphertext,
106            opening,
107            amount,
108            &mut transcript,
109        );
110        CiphertextCommitmentEqualityProofData { context, proof }
111    }
112
113    pub fn to_bytes(&self) -> [Byte<BooleanValue>; CIPHERTEXT_COMMITMENT_EQUALITY_PROOF_DATA_LEN] {
114        let mut bytes =
115            [Byte::<BooleanValue>::from(0); CIPHERTEXT_COMMITMENT_EQUALITY_PROOF_DATA_LEN];
116        bytes[..CIPHERTEXT_COMMITMENT_EQUALITY_PROOF_CONTEXT_LEN]
117            .copy_from_slice(&self.context.to_bytes());
118        bytes[CIPHERTEXT_COMMITMENT_EQUALITY_PROOF_CONTEXT_LEN..]
119            .copy_from_slice(&self.proof.to_bytes());
120        bytes
121    }
122}
123
124/// Equality proof.
125///
126/// Contains all the elliptic curve and scalar components that make up the sigma protocol.
127#[allow(non_snake_case, dead_code)]
128#[derive(Clone, Copy)]
129pub struct CiphertextCommitmentEqualityProof {
130    Y_0: CompressedCurveValue,
131    Y_1: CompressedCurveValue,
132    Y_2: CompressedCurveValue,
133    z_s: FieldValue<ScalarField>,
134    z_x: FieldValue<ScalarField>,
135    z_r: FieldValue<ScalarField>,
136}
137
138#[allow(non_snake_case)]
139impl CiphertextCommitmentEqualityProof {
140    /// Creates a ciphertext-commitment equality proof.
141    ///
142    /// The function does *not* hash the public key, ciphertext, or commitment into the transcript.
143    /// For security, the caller (the main protocol) should hash these public components prior to
144    /// invoking this constructor.
145    ///
146    /// This function is randomized. It uses random singlets internally to generate random scalars.
147    ///
148    /// Note that the proof constructor does not take the actual Pedersen commitment as input; it
149    /// takes the associated Pedersen opening instead.
150    ///
151    /// * `keypair` - The ElGamal keypair associated with the first to be proved
152    /// * `ciphertext` - The main ElGamal ciphertext to be proved
153    /// * `opening` - The opening associated with the main Pedersen commitment to be proved
154    /// * `amount` - The message associated with the ElGamal ciphertext and Pedersen commitment
155    /// * `transcript` - The transcript that does the bookkeeping for the Fiat-Shamir heuristic
156    pub fn new(
157        keypair: &ElGamalKeypair,
158        ciphertext: &ElGamalCiphertext,
159        opening: &PedersenOpening,
160        amount: FieldValue<ScalarField>,
161        transcript: &mut Transcript<BooleanValue>,
162    ) -> Self {
163        transcript.ciphertext_commitment_equality_proof_domain_separator();
164
165        // extract the relevant scalar and Ristretto points from the inputs
166        let P = keypair.pubkey().get_point();
167        let D = ciphertext.handle.get_point();
168
169        let s = keypair.secret().get_scalar();
170        let x = amount;
171        let r = opening.get_scalar();
172
173        // generate random masking factors that also serves as nonces
174        let y_s = FieldValue::<ScalarField>::random();
175        let y_x = FieldValue::<ScalarField>::random();
176        let y_r = FieldValue::<ScalarField>::random();
177
178        let Y_0 = (y_s * *P).reveal().compress();
179        let Y_1 = CurveValue::multiscalar_mul(vec![y_x, y_s], vec![CurveValue::generator(), *D])
180            .reveal()
181            .compress();
182        let Y_2 = CurveValue::multiscalar_mul(
183            vec![y_x, y_r],
184            vec![
185                CurveValue::generator(),
186                CurveValue::from(*LazyLock::force(&H)),
187            ],
188        )
189        .reveal()
190        .compress();
191
192        // record masking factors in the transcript
193        transcript.append_point(b"Y_0", &Y_0);
194        transcript.append_point(b"Y_1", &Y_1);
195        transcript.append_point(b"Y_2", &Y_2);
196
197        let c = transcript.challenge_scalar(b"c");
198
199        // compute the masked values
200        let z_s = ((c * *s) + y_s).reveal();
201        let z_x = ((c * x) + y_x).reveal();
202        let z_r = ((c * *r) + y_r).reveal();
203
204        // compute challenge `w` for consistency with verification
205        transcript.append_scalar(b"z_s", &z_s);
206        transcript.append_scalar(b"z_x", &z_x);
207        transcript.append_scalar(b"z_r", &z_r);
208        let _w = transcript.challenge_scalar(b"w");
209
210        CiphertextCommitmentEqualityProof {
211            Y_0,
212            Y_1,
213            Y_2,
214            z_s,
215            z_x,
216            z_r,
217        }
218    }
219
220    pub fn to_bytes(&self) -> [Byte<BooleanValue>; CIPHERTEXT_COMMITMENT_EQUALITY_PROOF_LEN] {
221        let mut buf = [Byte::<BooleanValue>::from(0); CIPHERTEXT_COMMITMENT_EQUALITY_PROOF_LEN];
222        let mut chunks = buf.chunks_mut(UNIT_LEN);
223        chunks.next().unwrap().copy_from_slice(&self.Y_0.to_bytes());
224        chunks.next().unwrap().copy_from_slice(&self.Y_1.to_bytes());
225        chunks.next().unwrap().copy_from_slice(&self.Y_2.to_bytes());
226        chunks
227            .next()
228            .unwrap()
229            .copy_from_slice(&self.z_s.to_le_bytes());
230        chunks
231            .next()
232            .unwrap()
233            .copy_from_slice(&self.z_x.to_le_bytes());
234        chunks
235            .next()
236            .unwrap()
237            .copy_from_slice(&self.z_r.to_le_bytes());
238        buf
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use crate::{
245        core::{
246            bounds::FieldBounds,
247            expressions::{
248                curve_expr::{CurveExpr, InputInfo},
249                domain::Domain,
250                expr::EvalValue,
251                field_expr::FieldExpr,
252                InputKind,
253            },
254            global_value::{
255                curve_value::CurveValue,
256                global_expr_store::with_local_expr_store_as_global,
257                value::FieldValue,
258            },
259            ir_builder::{ExprStore, IRBuilder},
260        },
261        utils::{
262            curve_point::CurvePoint,
263            field::ScalarField,
264            used_field::UsedField,
265            zkp::{
266                ciphertext_commitment_equality::CiphertextCommitmentEqualityProofData,
267                elgamal::{
268                    DecryptHandle,
269                    ElGamalCiphertext,
270                    ElGamalKeypair,
271                    ElGamalPubkey,
272                    ElGamalSecretKey,
273                },
274                pedersen::{PedersenCommitment, PedersenOpening},
275            },
276        },
277    };
278    use group::GroupEncoding;
279    use primitives::algebra::elliptic_curve::{Curve as AsyncMPCCurve, Curve25519Ristretto};
280    use rand::{Rng, RngCore};
281    use rustc_hash::FxHashMap;
282    use std::rc::Rc;
283    use zk_elgamal_proof::{
284        encryption::{
285            elgamal::ElGamalKeypair as SolanaElGamalKeypair,
286            pedersen::Pedersen as SolanaPedersen,
287        },
288        zk_elgamal_proof_program::proof_data::{
289            CiphertextCommitmentEqualityProofData as SolanaCiphertextCommitmentEqualityProofData,
290            ZkProofData,
291        },
292    };
293
294    #[test]
295    #[allow(non_snake_case)]
296    fn test_ciphertext_commitment_equality() {
297        let rng = &mut crate::utils::test_rng::get();
298
299        // random ElGamal keypair and amount
300        let keypair = SolanaElGamalKeypair::new_rand();
301        let mut amount = rng.next_u64();
302        let ciphertext = keypair.pubkey().encrypt(amount);
303        let (mut commitment, mut opening) = SolanaPedersen::new(amount);
304
305        // we flip a coin and generate an invalid proof if the bit is false
306        let is_valid_proof = rng.gen_bool(0.5);
307        if !is_valid_proof {
308            amount = rng.next_u64();
309            (commitment, opening) = SolanaPedersen::new(amount);
310        }
311
312        let solana_proof_data = SolanaCiphertextCommitmentEqualityProofData::new(
313            &keypair,
314            &ciphertext,
315            &commitment,
316            &opening,
317            amount,
318        )
319        .unwrap();
320        assert_eq!(solana_proof_data.verify_proof().is_ok(), is_valid_proof);
321
322        let mut expr_store = IRBuilder::new(true);
323
324        // add inputs
325        let mut input_vals = FxHashMap::<usize, EvalValue>::default();
326        let _ = <IRBuilder as ExprStore<ScalarField>>::push_curve(
327            &mut expr_store,
328            CurveExpr::Input(0, Rc::new(InputInfo::from(InputKind::Plaintext))),
329        );
330        input_vals.insert(
331            0,
332            EvalValue::Curve(CurvePoint::new(
333                <Curve25519Ristretto as AsyncMPCCurve>::Point::from_bytes(
334                    &keypair.pubkey().get_point().to_bytes(),
335                )
336                .unwrap(),
337            )),
338        );
339        let _ = expr_store.push_field(FieldExpr::Input(
340            1,
341            FieldBounds::<ScalarField>::All.as_input_info(InputKind::Secret),
342        ));
343        input_vals.insert(
344            1,
345            EvalValue::Scalar(ScalarField::from(*keypair.secret().get_scalar())),
346        );
347        let _ = <IRBuilder as ExprStore<ScalarField>>::push_curve(
348            &mut expr_store,
349            CurveExpr::Input(2, Rc::new(InputInfo::from(InputKind::Plaintext))),
350        );
351        input_vals.insert(
352            2,
353            EvalValue::Curve(CurvePoint::new(
354                <Curve25519Ristretto as AsyncMPCCurve>::Point::from_bytes(
355                    &ciphertext.commitment.get_point().to_bytes(),
356                )
357                .unwrap(),
358            )),
359        );
360        let _ = <IRBuilder as ExprStore<ScalarField>>::push_curve(
361            &mut expr_store,
362            CurveExpr::Input(3, Rc::new(InputInfo::from(InputKind::Plaintext))),
363        );
364        input_vals.insert(
365            3,
366            EvalValue::Curve(CurvePoint::new(
367                <Curve25519Ristretto as AsyncMPCCurve>::Point::from_bytes(
368                    &ciphertext.handle.get_point().to_bytes(),
369                )
370                .unwrap(),
371            )),
372        );
373        let _ = <IRBuilder as ExprStore<ScalarField>>::push_curve(
374            &mut expr_store,
375            CurveExpr::Input(4, Rc::new(InputInfo::from(InputKind::Plaintext))),
376        );
377        input_vals.insert(
378            4,
379            EvalValue::Curve(CurvePoint::new(
380                <Curve25519Ristretto as AsyncMPCCurve>::Point::from_bytes(
381                    &commitment.get_point().to_bytes(),
382                )
383                .unwrap(),
384            )),
385        );
386        let _ = expr_store.push_field(FieldExpr::Input(
387            5,
388            FieldBounds::<ScalarField>::All.as_input_info(InputKind::Secret),
389        ));
390        input_vals.insert(
391            5,
392            EvalValue::Scalar(ScalarField::from(*opening.get_scalar())),
393        );
394        let _ = expr_store.push_field(FieldExpr::Input(
395            6,
396            FieldBounds::new(
397                ScalarField::from(0),
398                ScalarField::power_of_two(64) - ScalarField::from(1),
399            )
400            .as_input_info(InputKind::Secret),
401        ));
402        input_vals.insert(6, EvalValue::Scalar(ScalarField::from(amount)));
403
404        let outputs = with_local_expr_store_as_global(
405            || {
406                let pubkey = CurveValue::new(0);
407                let secret = FieldValue::<ScalarField>::from_id(1);
408                let ciphertext_commitment = CurveValue::new(2);
409                let ciphertext_handle = CurveValue::new(3);
410                let commitment = CurveValue::new(4);
411                let opening = FieldValue::<ScalarField>::from_id(5);
412                let amount = FieldValue::<ScalarField>::from_id(6);
413
414                let arcis_proof_data = CiphertextCommitmentEqualityProofData::new(
415                    &ElGamalKeypair::new_from_inner(
416                        ElGamalPubkey::new_from_inner(pubkey),
417                        ElGamalSecretKey::new(secret),
418                    ),
419                    &ElGamalCiphertext {
420                        commitment: PedersenCommitment::new(ciphertext_commitment),
421                        handle: DecryptHandle::new_from_inner(ciphertext_handle),
422                    },
423                    &PedersenCommitment::new(commitment),
424                    &PedersenOpening::new(opening),
425                    amount,
426                );
427
428                arcis_proof_data
429                    .to_bytes()
430                    .into_iter()
431                    .map(|byte| FieldValue::<ScalarField>::from(byte).get_id())
432                    .collect::<Vec<usize>>()
433            },
434            &mut expr_store,
435        );
436
437        let ir = expr_store.into_ir(outputs);
438        let result = ir
439            .eval(rng, &mut input_vals)
440            .map(|x| {
441                x.into_iter()
442                    .map(ScalarField::unwrap)
443                    .collect::<Vec<ScalarField>>()
444            })
445            .unwrap();
446
447        let arcis_proof_data_bytes = result
448            .iter()
449            .map(|byte| byte.to_le_bytes()[0])
450            .collect::<Vec<u8>>();
451
452        let arcis_proof_data =
453            SolanaCiphertextCommitmentEqualityProofData::from_bytes(&arcis_proof_data_bytes)
454                .unwrap();
455
456        let arcis_verification = arcis_proof_data.verify_proof();
457
458        assert_eq!(arcis_verification.is_ok(), is_valid_proof);
459    }
460}