Skip to main content

arcis_compiler/utils/zkp/
zero_ciphertext.rs

1//! Arcis implementation of https://github.com/solana-program/zk-elgamal-proof/blob/main/zk-sdk/src/sigma_proofs/zero_ciphertext.rs
2
3use crate::{
4    core::{
5        circuits::boolean::{boolean_value::BooleanValue, byte::Byte},
6        global_value::{curve_value::CompressedCurveValue, value::FieldValue},
7    },
8    traits::{Reveal, ToLeBytes},
9    utils::{
10        field::ScalarField,
11        zkp::{
12            elgamal::{ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey},
13            transcript::Transcript,
14            util::UNIT_LEN,
15        },
16    },
17};
18use zk_elgamal_proof::encryption::{
19    DECRYPT_HANDLE_LEN,
20    ELGAMAL_CIPHERTEXT_LEN,
21    ELGAMAL_PUBKEY_LEN,
22    PEDERSEN_COMMITMENT_LEN,
23};
24
25pub const ZERO_CIPHERTEXT_PROOF_LEN: usize = 96;
26
27pub const ZERO_CIPHERTEXT_PROOF_CONTEXT_LEN: usize = ELGAMAL_PUBKEY_LEN + ELGAMAL_CIPHERTEXT_LEN;
28
29pub const ZERO_CIPHERTEXT_PROOF_DATA_LEN: usize =
30    ZERO_CIPHERTEXT_PROOF_CONTEXT_LEN + ZERO_CIPHERTEXT_PROOF_LEN;
31
32/// The instruction data that is needed for the `ProofInstruction::ZeroCiphertext` instruction.
33///
34/// It includes the cryptographic proof as well as the context data information needed to verify
35/// the proof.
36#[derive(Clone, Copy)]
37pub struct ZeroCiphertextProofData {
38    pub context: ZeroCiphertextProofContext,
39    pub proof: ZeroCiphertextProof,
40}
41
42/// The context data needed to verify a zero-ciphertext proof.
43#[derive(Clone, Copy)]
44pub struct ZeroCiphertextProofContext {
45    /// The ElGamal pubkey associated with the ElGamal ciphertext
46    pub pubkey: ElGamalPubkey,
47    /// The ElGamal ciphertext that encrypts zero
48    pub ciphertext: ElGamalCiphertext,
49}
50
51impl ZeroCiphertextProofContext {
52    fn new_transcript(&self) -> Transcript<BooleanValue> {
53        let mut transcript = Transcript::new(b"zero-ciphertext-instruction");
54        transcript.append_point(b"pubkey", &self.pubkey.get_point().compress());
55        transcript.append_elgamal_ciphertext(b"ciphertext", &self.ciphertext);
56        transcript
57    }
58
59    pub fn to_bytes(&self) -> [Byte<BooleanValue>; ZERO_CIPHERTEXT_PROOF_CONTEXT_LEN] {
60        let mut bytes = [Byte::<BooleanValue>::from(0); ZERO_CIPHERTEXT_PROOF_CONTEXT_LEN];
61        bytes[..ELGAMAL_PUBKEY_LEN].copy_from_slice(&self.pubkey.get_point().compress().to_bytes());
62        let mut offset = ELGAMAL_PUBKEY_LEN;
63        bytes[offset..offset + PEDERSEN_COMMITMENT_LEN]
64            .copy_from_slice(&self.ciphertext.commitment.get_point().compress().to_bytes());
65        offset += PEDERSEN_COMMITMENT_LEN;
66        bytes[offset..offset + DECRYPT_HANDLE_LEN]
67            .copy_from_slice(&self.ciphertext.handle.get_point().compress().to_bytes());
68        bytes
69    }
70}
71
72impl ZeroCiphertextProofData {
73    pub fn new(keypair: &ElGamalKeypair, ciphertext: &ElGamalCiphertext) -> Self {
74        let context = ZeroCiphertextProofContext {
75            pubkey: *keypair.pubkey(),
76            ciphertext: *ciphertext,
77        };
78        let mut transcript = context.new_transcript();
79        let proof = ZeroCiphertextProof::new(keypair, ciphertext, &mut transcript);
80        ZeroCiphertextProofData { context, proof }
81    }
82
83    pub fn to_bytes(&self) -> [Byte<BooleanValue>; ZERO_CIPHERTEXT_PROOF_DATA_LEN] {
84        let mut bytes = [Byte::<BooleanValue>::from(0); ZERO_CIPHERTEXT_PROOF_DATA_LEN];
85        bytes[..ZERO_CIPHERTEXT_PROOF_CONTEXT_LEN].copy_from_slice(&self.context.to_bytes());
86        bytes[ZERO_CIPHERTEXT_PROOF_CONTEXT_LEN..].copy_from_slice(&self.proof.to_bytes());
87        bytes
88    }
89}
90
91/// Zero-ciphertext proof.
92///
93/// Contains all the elliptic curve and scalar components that make up the sigma protocol.
94#[allow(non_snake_case, dead_code)]
95#[derive(Clone, Copy)]
96pub struct ZeroCiphertextProof {
97    Y_P: CompressedCurveValue,
98    Y_D: CompressedCurveValue,
99    z: FieldValue<ScalarField>,
100}
101
102#[allow(non_snake_case)]
103impl ZeroCiphertextProof {
104    /// Creates a zero-ciphertext proof.
105    ///
106    /// The function does *not* hash the public key and ciphertext into the transcript. For
107    /// security, the caller (the main protocol) should hash these public components prior to
108    /// invoking this constructor.
109    ///
110    /// This function is randomized. It uses random singlets internally to generate random scalars.
111    ///
112    /// * `elgamal_keypair` - The ElGamal keypair associated with the ciphertext to be proved
113    /// * `ciphertext` - The main ElGamal ciphertext to be proved
114    /// * `transcript` - The transcript that does the bookkeeping for the Fiat-Shamir heuristic
115    pub fn new(
116        elgamal_keypair: &ElGamalKeypair,
117        ciphertext: &ElGamalCiphertext,
118        transcript: &mut Transcript<BooleanValue>,
119    ) -> Self {
120        transcript.zero_ciphertext_proof_domain_separator();
121
122        // extract the relevant scalar and Ristretto points from the input
123        let P = elgamal_keypair.pubkey().get_point();
124        let s = elgamal_keypair.secret().get_scalar();
125        let D = ciphertext.handle.get_point();
126
127        // generate a random masking factor that also serves as a nonce
128        let y = FieldValue::<ScalarField>::random();
129        let Y_P = (y * *P).reveal().compress();
130        let Y_D = (y * *D).reveal().compress();
131
132        // record Y in the transcript and receive a challenge scalar
133        transcript.append_point(b"Y_P", &Y_P);
134        transcript.append_point(b"Y_D", &Y_D);
135
136        let c = transcript.challenge_scalar(b"c");
137
138        // compute the masked secret key
139        let z = ((c * *s) + y).reveal();
140
141        transcript.append_scalar(b"z", &z);
142        let _w = transcript.challenge_scalar(b"w");
143
144        Self { Y_P, Y_D, z }
145    }
146
147    pub fn to_bytes(&self) -> [Byte<BooleanValue>; ZERO_CIPHERTEXT_PROOF_LEN] {
148        let mut buf = [Byte::<BooleanValue>::from(0); ZERO_CIPHERTEXT_PROOF_LEN];
149        let mut chunks = buf.chunks_mut(UNIT_LEN);
150        chunks.next().unwrap().copy_from_slice(&self.Y_P.to_bytes());
151        chunks.next().unwrap().copy_from_slice(&self.Y_D.to_bytes());
152        chunks
153            .next()
154            .unwrap()
155            .copy_from_slice(&self.z.to_le_bytes());
156        buf
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use crate::{
163        core::{
164            bounds::FieldBounds,
165            expressions::{
166                curve_expr::{CurveExpr, InputInfo},
167                domain::Domain,
168                expr::EvalValue,
169                field_expr::FieldExpr,
170                InputKind,
171            },
172            global_value::{
173                curve_value::CurveValue,
174                global_expr_store::with_local_expr_store_as_global,
175                value::FieldValue,
176            },
177            ir_builder::{ExprStore, IRBuilder},
178        },
179        utils::{
180            curve_point::CurvePoint,
181            field::ScalarField,
182            zkp::{
183                elgamal::{
184                    DecryptHandle,
185                    ElGamalCiphertext,
186                    ElGamalKeypair,
187                    ElGamalPubkey,
188                    ElGamalSecretKey,
189                },
190                pedersen::PedersenCommitment,
191                zero_ciphertext::ZeroCiphertextProofData,
192            },
193        },
194    };
195    use group::GroupEncoding;
196    use primitives::algebra::elliptic_curve::{Curve as AsyncMPCCurve, Curve25519Ristretto};
197    use rand::{Rng, RngCore};
198    use rustc_hash::FxHashMap;
199    use std::rc::Rc;
200    use zk_elgamal_proof::{
201        encryption::elgamal::ElGamalKeypair as SolanaElGamalKeypair,
202        zk_elgamal_proof_program::proof_data::{
203            ZeroCiphertextProofData as SolanaZeroCiphertextProofData,
204            ZkProofData,
205        },
206    };
207
208    #[test]
209    #[allow(non_snake_case)]
210    fn test_zero_ciphertext_proof() {
211        let rng = &mut crate::utils::test_rng::get();
212
213        // random ElGamal keypair
214        let keypair = SolanaElGamalKeypair::new_rand();
215        let mut ciphertext = keypair.pubkey().encrypt(0u32);
216
217        // we flip a coin and generate an invalid proof if the bit is false
218        let is_valid_proof = rng.gen_bool(0.5);
219        if !is_valid_proof {
220            let rand = rng.next_u32() + 1;
221            ciphertext = ciphertext.add_amount(rand);
222        }
223
224        let solana_proof_data = SolanaZeroCiphertextProofData::new(&keypair, &ciphertext).unwrap();
225        assert_eq!(solana_proof_data.verify_proof().is_ok(), is_valid_proof);
226
227        let mut expr_store = IRBuilder::new(true);
228
229        // add inputs
230        let mut input_vals = FxHashMap::<usize, EvalValue>::default();
231        let _ = <IRBuilder as ExprStore<ScalarField>>::push_curve(
232            &mut expr_store,
233            CurveExpr::Input(0, Rc::new(InputInfo::from(InputKind::Plaintext))),
234        );
235        input_vals.insert(
236            0,
237            EvalValue::Curve(CurvePoint::new(
238                <Curve25519Ristretto as AsyncMPCCurve>::Point::from_bytes(
239                    &keypair.pubkey().get_point().to_bytes(),
240                )
241                .unwrap(),
242            )),
243        );
244        let _ = expr_store.push_field(FieldExpr::Input(
245            1,
246            FieldBounds::<ScalarField>::All.as_input_info(InputKind::Secret),
247        ));
248        input_vals.insert(
249            1,
250            EvalValue::Scalar(ScalarField::from(*keypair.secret().get_scalar())),
251        );
252        let _ = <IRBuilder as ExprStore<ScalarField>>::push_curve(
253            &mut expr_store,
254            CurveExpr::Input(2, Rc::new(InputInfo::from(InputKind::Plaintext))),
255        );
256        input_vals.insert(
257            2,
258            EvalValue::Curve(CurvePoint::new(
259                <Curve25519Ristretto as AsyncMPCCurve>::Point::from_bytes(
260                    &ciphertext.commitment.get_point().to_bytes(),
261                )
262                .unwrap(),
263            )),
264        );
265        let _ = <IRBuilder as ExprStore<ScalarField>>::push_curve(
266            &mut expr_store,
267            CurveExpr::Input(3, Rc::new(InputInfo::from(InputKind::Plaintext))),
268        );
269        input_vals.insert(
270            3,
271            EvalValue::Curve(CurvePoint::new(
272                <Curve25519Ristretto as AsyncMPCCurve>::Point::from_bytes(
273                    &ciphertext.handle.get_point().to_bytes(),
274                )
275                .unwrap(),
276            )),
277        );
278
279        let outputs = with_local_expr_store_as_global(
280            || {
281                let pubkey = CurveValue::new(0);
282                let secret = FieldValue::<ScalarField>::from_id(1);
283                let commitment = CurveValue::new(2);
284                let handle = CurveValue::new(3);
285
286                let arcis_proof_data = ZeroCiphertextProofData::new(
287                    &ElGamalKeypair::new_from_inner(
288                        ElGamalPubkey::new_from_inner(pubkey),
289                        ElGamalSecretKey::new(secret),
290                    ),
291                    &ElGamalCiphertext {
292                        commitment: PedersenCommitment::new(commitment),
293                        handle: DecryptHandle::new_from_inner(handle),
294                    },
295                );
296
297                arcis_proof_data
298                    .to_bytes()
299                    .into_iter()
300                    .map(|byte| FieldValue::<ScalarField>::from(byte).get_id())
301                    .collect::<Vec<usize>>()
302            },
303            &mut expr_store,
304        );
305
306        let ir = expr_store.into_ir(outputs);
307        let result = ir
308            .eval(rng, &mut input_vals)
309            .map(|x| {
310                x.into_iter()
311                    .map(ScalarField::unwrap)
312                    .collect::<Vec<ScalarField>>()
313            })
314            .unwrap();
315
316        let arcis_proof_data_bytes = result
317            .iter()
318            .map(|byte| byte.to_le_bytes()[0])
319            .collect::<Vec<u8>>();
320
321        let arcis_proof_data =
322            SolanaZeroCiphertextProofData::from_bytes(&arcis_proof_data_bytes).unwrap();
323
324        let arcis_verification = arcis_proof_data.verify_proof();
325
326        assert_eq!(arcis_verification.is_ok(), is_valid_proof);
327    }
328}