solana_zk_sdk/encryption/
grouped_elgamal.rs1#[cfg(not(target_arch = "wasm32"))]
16use crate::encryption::{discrete_log::DiscreteLog, elgamal::ElGamalSecretKey};
17#[cfg(target_arch = "wasm32")]
18pub use grouped_elgamal_wasm::*;
19#[cfg(target_arch = "wasm32")]
20use wasm_bindgen::prelude::*;
21use {
22 crate::{
23 encryption::{
24 elgamal::{DecryptHandle, ElGamalCiphertext, ElGamalPubkey},
25 pedersen::{Pedersen, PedersenCommitment, PedersenOpening},
26 },
27 RISTRETTO_POINT_LEN,
28 },
29 curve25519_dalek::scalar::Scalar,
30 thiserror::Error,
31};
32
33#[derive(Error, Clone, Debug, Eq, PartialEq)]
34pub enum GroupedElGamalError {
35 #[error("index out of bounds")]
36 IndexOutOfBounds,
37}
38
39pub struct GroupedElGamal<const N: usize>;
41impl<const N: usize> GroupedElGamal<N> {
42 pub fn encrypt<T: Into<Scalar>>(
46 pubkeys: [&ElGamalPubkey; N],
47 amount: T,
48 ) -> GroupedElGamalCiphertext<N> {
49 let (commitment, opening) = Pedersen::new(amount);
50 let handles: [DecryptHandle; N] = pubkeys
51 .iter()
52 .map(|handle| handle.decrypt_handle(&opening))
53 .collect::<Vec<DecryptHandle>>()
54 .try_into()
55 .unwrap();
56
57 GroupedElGamalCiphertext {
58 commitment,
59 handles,
60 }
61 }
62
63 pub fn encrypt_with<T: Into<Scalar>>(
66 pubkeys: [&ElGamalPubkey; N],
67 amount: T,
68 opening: &PedersenOpening,
69 ) -> GroupedElGamalCiphertext<N> {
70 let commitment = Pedersen::with(amount, opening);
71 let handles: [DecryptHandle; N] = pubkeys
72 .iter()
73 .map(|handle| handle.decrypt_handle(opening))
74 .collect::<Vec<DecryptHandle>>()
75 .try_into()
76 .unwrap();
77
78 GroupedElGamalCiphertext {
79 commitment,
80 handles,
81 }
82 }
83
84 fn to_elgamal_ciphertext(
87 grouped_ciphertext: &GroupedElGamalCiphertext<N>,
88 index: usize,
89 ) -> Result<ElGamalCiphertext, GroupedElGamalError> {
90 let handle = grouped_ciphertext
91 .handles
92 .get(index)
93 .ok_or(GroupedElGamalError::IndexOutOfBounds)?;
94
95 Ok(ElGamalCiphertext {
96 commitment: grouped_ciphertext.commitment,
97 handle: *handle,
98 })
99 }
100}
101
102#[cfg(not(target_arch = "wasm32"))]
103impl<const N: usize> GroupedElGamal<N> {
104 fn decrypt(
110 grouped_ciphertext: &GroupedElGamalCiphertext<N>,
111 secret: &ElGamalSecretKey,
112 index: usize,
113 ) -> Result<DiscreteLog, GroupedElGamalError> {
114 Self::to_elgamal_ciphertext(grouped_ciphertext, index)
115 .map(|ciphertext| ciphertext.decrypt(secret))
116 }
117
118 fn decrypt_u32(
126 grouped_ciphertext: &GroupedElGamalCiphertext<N>,
127 secret: &ElGamalSecretKey,
128 index: usize,
129 ) -> Result<Option<u64>, GroupedElGamalError> {
130 Self::to_elgamal_ciphertext(grouped_ciphertext, index)
131 .map(|ciphertext| ciphertext.decrypt_u32(secret))
132 }
133}
134
135#[derive(Clone, Copy, Debug, Eq, PartialEq)]
140pub struct GroupedElGamalCiphertext<const N: usize> {
141 pub commitment: PedersenCommitment,
142 pub handles: [DecryptHandle; N],
143}
144
145impl<const N: usize> GroupedElGamalCiphertext<N> {
146 pub fn to_elgamal_ciphertext(
149 &self,
150 index: usize,
151 ) -> Result<ElGamalCiphertext, GroupedElGamalError> {
152 GroupedElGamal::to_elgamal_ciphertext(self, index)
153 }
154
155 fn expected_byte_length() -> usize {
162 N.checked_add(1)
163 .and_then(|length| length.checked_mul(RISTRETTO_POINT_LEN))
164 .unwrap()
165 }
166
167 pub fn to_bytes(&self) -> Vec<u8> {
168 let mut buf = Vec::with_capacity(Self::expected_byte_length());
169 buf.extend_from_slice(&self.commitment.to_bytes());
170 self.handles
171 .iter()
172 .for_each(|handle| buf.extend_from_slice(&handle.to_bytes()));
173 buf
174 }
175
176 pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
177 if bytes.len() != Self::expected_byte_length() {
178 return None;
179 }
180
181 let mut iter = bytes.chunks(RISTRETTO_POINT_LEN);
182 let commitment = PedersenCommitment::from_bytes(iter.next()?)?;
183
184 let mut handles = Vec::with_capacity(N);
185 for handle_bytes in iter {
186 handles.push(DecryptHandle::from_bytes(handle_bytes)?);
187 }
188
189 Some(Self {
190 commitment,
191 handles: handles.try_into().unwrap(),
192 })
193 }
194}
195
196#[cfg(not(target_arch = "wasm32"))]
197impl<const N: usize> GroupedElGamalCiphertext<N> {
198 pub fn decrypt(
204 &self,
205 secret: &ElGamalSecretKey,
206 index: usize,
207 ) -> Result<DiscreteLog, GroupedElGamalError> {
208 GroupedElGamal::decrypt(self, secret, index)
209 }
210
211 pub fn decrypt_u32(
219 &self,
220 secret: &ElGamalSecretKey,
221 index: usize,
222 ) -> Result<Option<u64>, GroupedElGamalError> {
223 GroupedElGamal::decrypt_u32(self, secret, index)
224 }
225}
226
227#[cfg(target_arch = "wasm32")]
231mod grouped_elgamal_wasm {
232 use super::*;
233
234 #[wasm_bindgen]
235 pub struct GroupedElGamalCiphertext2Handles(pub(crate) GroupedElGamalCiphertext<2>);
236
237 #[wasm_bindgen]
238 impl GroupedElGamalCiphertext2Handles {
239 #[wasm_bindgen(js_name = encryptU64)]
240 pub fn encrypt_u64(
241 first_pubkey: &ElGamalPubkey,
242 second_pubkey: &ElGamalPubkey,
243 amount: u64,
244 ) -> Self {
245 Self(GroupedElGamal::<2>::encrypt(
246 [first_pubkey, second_pubkey],
247 amount,
248 ))
249 }
250
251 #[wasm_bindgen(js_name = encryptWithU64)]
252 pub fn encrypt_with_u64(
253 first_pubkey: &ElGamalPubkey,
254 second_pubkey: &ElGamalPubkey,
255 amount: u64,
256 opening: &PedersenOpening,
257 ) -> Self {
258 Self(GroupedElGamal::<2>::encrypt_with(
259 [first_pubkey, second_pubkey],
260 amount,
261 opening,
262 ))
263 }
264 }
265
266 #[wasm_bindgen]
267 pub struct GroupedElGamalCiphertext3Handles(pub(crate) GroupedElGamalCiphertext<3>);
268
269 #[wasm_bindgen]
270 impl GroupedElGamalCiphertext3Handles {
271 #[wasm_bindgen(js_name = encryptU64)]
272 pub fn encrypt_u64(
273 first_pubkey: &ElGamalPubkey,
274 second_pubkey: &ElGamalPubkey,
275 third_pubkey: &ElGamalPubkey,
276 amount: u64,
277 ) -> Self {
278 Self(GroupedElGamal::<3>::encrypt(
279 [first_pubkey, second_pubkey, third_pubkey],
280 amount,
281 ))
282 }
283
284 #[wasm_bindgen(js_name = encryptWithU64)]
285 pub fn encrypt_with_u64(
286 first_pubkey: &ElGamalPubkey,
287 second_pubkey: &ElGamalPubkey,
288 third_pubkey: &ElGamalPubkey,
289 amount: u64,
290 opening: &PedersenOpening,
291 ) -> Self {
292 Self(GroupedElGamal::<3>::encrypt_with(
293 [first_pubkey, second_pubkey, third_pubkey],
294 amount,
295 opening,
296 ))
297 }
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use {super::*, crate::encryption::elgamal::ElGamalKeypair};
304
305 #[test]
306 fn test_grouped_elgamal_encrypt_decrypt_correctness() {
307 let elgamal_keypair_0 = ElGamalKeypair::new_rand();
308 let elgamal_keypair_1 = ElGamalKeypair::new_rand();
309 let elgamal_keypair_2 = ElGamalKeypair::new_rand();
310
311 let amount: u64 = 10;
312 let grouped_ciphertext = GroupedElGamal::encrypt(
313 [
314 elgamal_keypair_0.pubkey(),
315 elgamal_keypair_1.pubkey(),
316 elgamal_keypair_2.pubkey(),
317 ],
318 amount,
319 );
320
321 assert_eq!(
322 Some(amount),
323 grouped_ciphertext
324 .decrypt_u32(elgamal_keypair_0.secret(), 0)
325 .unwrap()
326 );
327
328 assert_eq!(
329 Some(amount),
330 grouped_ciphertext
331 .decrypt_u32(elgamal_keypair_1.secret(), 1)
332 .unwrap()
333 );
334
335 assert_eq!(
336 Some(amount),
337 grouped_ciphertext
338 .decrypt_u32(elgamal_keypair_2.secret(), 2)
339 .unwrap()
340 );
341
342 assert_eq!(
343 GroupedElGamalError::IndexOutOfBounds,
344 grouped_ciphertext
345 .decrypt_u32(elgamal_keypair_0.secret(), 3)
346 .unwrap_err()
347 );
348 }
349
350 #[test]
351 fn test_grouped_ciphertext_bytes() {
352 let elgamal_keypair_0 = ElGamalKeypair::new_rand();
353 let elgamal_keypair_1 = ElGamalKeypair::new_rand();
354 let elgamal_keypair_2 = ElGamalKeypair::new_rand();
355
356 let amount: u64 = 10;
357 let grouped_ciphertext = GroupedElGamal::encrypt(
358 [
359 elgamal_keypair_0.pubkey(),
360 elgamal_keypair_1.pubkey(),
361 elgamal_keypair_2.pubkey(),
362 ],
363 amount,
364 );
365
366 let produced_bytes = grouped_ciphertext.to_bytes();
367 assert_eq!(produced_bytes.len(), 128);
368
369 let decoded_grouped_ciphertext =
370 GroupedElGamalCiphertext::<3>::from_bytes(&produced_bytes).unwrap();
371 assert_eq!(
372 Some(amount),
373 decoded_grouped_ciphertext
374 .decrypt_u32(elgamal_keypair_0.secret(), 0)
375 .unwrap()
376 );
377
378 assert_eq!(
379 Some(amount),
380 decoded_grouped_ciphertext
381 .decrypt_u32(elgamal_keypair_1.secret(), 1)
382 .unwrap()
383 );
384
385 assert_eq!(
386 Some(amount),
387 decoded_grouped_ciphertext
388 .decrypt_u32(elgamal_keypair_2.secret(), 2)
389 .unwrap()
390 );
391 }
392
393 #[test]
394 fn test_decrypt_with_wrong_key_at_valid_index() {
395 let keypair_0 = ElGamalKeypair::new_rand();
396 let keypair_1 = ElGamalKeypair::new_rand();
397 let amount: u64 = 50;
398
399 let grouped_ciphertext =
400 GroupedElGamal::encrypt([keypair_0.pubkey(), keypair_1.pubkey()], amount);
401
402 let result = grouped_ciphertext
404 .decrypt_u32(keypair_0.secret(), 1)
405 .unwrap();
406 assert!(result.is_none());
407 }
408
409 #[test]
410 fn test_zero_sized_group() {
411 let amount: u64 = 42;
412 let grouped_ciphertext = GroupedElGamal::<0>::encrypt([], amount);
413
414 let bytes = grouped_ciphertext.to_bytes();
416 assert_eq!(bytes.len(), 32); let decoded_ciphertext = GroupedElGamalCiphertext::<0>::from_bytes(&bytes).unwrap();
420 assert_eq!(grouped_ciphertext, decoded_ciphertext);
421
422 let keypair = ElGamalKeypair::new_rand();
424 assert_eq!(
425 grouped_ciphertext
426 .decrypt_u32(keypair.secret(), 0)
427 .unwrap_err(),
428 GroupedElGamalError::IndexOutOfBounds
429 );
430 }
431
432 #[test]
433 fn test_malformed_bytes_deserialization() {
434 let amount: u64 = 42;
435
436 let short_bytes = vec![0; 63]; assert!(GroupedElGamalCiphertext::<1>::from_bytes(&short_bytes).is_none());
439
440 let long_bytes = vec![0; 65]; assert!(GroupedElGamalCiphertext::<1>::from_bytes(&long_bytes).is_none());
443
444 let mut malformed_commitment = vec![0; 64];
446 malformed_commitment[0] = 1;
448 assert!(GroupedElGamalCiphertext::<1>::from_bytes(&malformed_commitment).is_none());
449
450 let keypair = ElGamalKeypair::new_rand();
452 let ciphertext = GroupedElGamal::<1>::encrypt([keypair.pubkey()], amount);
453 let mut bytes = ciphertext.to_bytes();
454 bytes[32] = 1;
456 assert!(GroupedElGamalCiphertext::<1>::from_bytes(&bytes).is_none());
457 }
458}