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(
124 grouped_ciphertext: &GroupedElGamalCiphertext<N>,
125 secret: &ElGamalSecretKey,
126 index: usize,
127 ) -> Result<Option<u64>, GroupedElGamalError> {
128 Self::to_elgamal_ciphertext(grouped_ciphertext, index)
129 .map(|ciphertext| ciphertext.decrypt_u32(secret))
130 }
131}
132
133#[derive(Clone, Copy, Debug, Eq, PartialEq)]
138pub struct GroupedElGamalCiphertext<const N: usize> {
139 pub commitment: PedersenCommitment,
140 pub handles: [DecryptHandle; N],
141}
142
143impl<const N: usize> GroupedElGamalCiphertext<N> {
144 pub fn to_elgamal_ciphertext(
147 &self,
148 index: usize,
149 ) -> Result<ElGamalCiphertext, GroupedElGamalError> {
150 GroupedElGamal::to_elgamal_ciphertext(self, index)
151 }
152
153 fn expected_byte_length() -> usize {
160 N.checked_add(1)
161 .and_then(|length| length.checked_mul(RISTRETTO_POINT_LEN))
162 .unwrap()
163 }
164
165 pub fn to_bytes(&self) -> Vec<u8> {
166 let mut buf = Vec::with_capacity(Self::expected_byte_length());
167 buf.extend_from_slice(&self.commitment.to_bytes());
168 self.handles
169 .iter()
170 .for_each(|handle| buf.extend_from_slice(&handle.to_bytes()));
171 buf
172 }
173
174 pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
175 if bytes.len() != Self::expected_byte_length() {
176 return None;
177 }
178
179 let mut iter = bytes.chunks(RISTRETTO_POINT_LEN);
180 let commitment = PedersenCommitment::from_bytes(iter.next()?)?;
181
182 let mut handles = Vec::with_capacity(N);
183 for handle_bytes in iter {
184 handles.push(DecryptHandle::from_bytes(handle_bytes)?);
185 }
186
187 Some(Self {
188 commitment,
189 handles: handles.try_into().unwrap(),
190 })
191 }
192}
193
194#[cfg(not(target_arch = "wasm32"))]
195impl<const N: usize> GroupedElGamalCiphertext<N> {
196 pub fn decrypt(
202 &self,
203 secret: &ElGamalSecretKey,
204 index: usize,
205 ) -> Result<DiscreteLog, GroupedElGamalError> {
206 GroupedElGamal::decrypt(self, secret, index)
207 }
208
209 pub fn decrypt_u32(
215 &self,
216 secret: &ElGamalSecretKey,
217 index: usize,
218 ) -> Result<Option<u64>, GroupedElGamalError> {
219 GroupedElGamal::decrypt_u32(self, secret, index)
220 }
221}
222
223#[cfg(target_arch = "wasm32")]
227mod grouped_elgamal_wasm {
228 use super::*;
229
230 #[wasm_bindgen]
231 pub struct GroupedElGamalCiphertext2Handles(pub(crate) GroupedElGamalCiphertext<2>);
232
233 #[wasm_bindgen]
234 impl GroupedElGamalCiphertext2Handles {
235 #[wasm_bindgen(js_name = encryptU64)]
236 pub fn encrypt_u64(
237 first_pubkey: &ElGamalPubkey,
238 second_pubkey: &ElGamalPubkey,
239 amount: u64,
240 ) -> Self {
241 Self(GroupedElGamal::<2>::encrypt(
242 [first_pubkey, second_pubkey],
243 amount,
244 ))
245 }
246
247 #[wasm_bindgen(js_name = encryptionWithU64)]
248 pub fn encryption_with_u64(
249 first_pubkey: &ElGamalPubkey,
250 second_pubkey: &ElGamalPubkey,
251 amount: u64,
252 opening: &PedersenOpening,
253 ) -> Self {
254 Self(GroupedElGamal::<2>::encrypt_with(
255 [first_pubkey, second_pubkey],
256 amount,
257 opening,
258 ))
259 }
260 }
261
262 #[wasm_bindgen]
263 pub struct GroupedElGamalCiphertext3Handles(pub(crate) GroupedElGamalCiphertext<3>);
264
265 #[wasm_bindgen]
266 impl GroupedElGamalCiphertext3Handles {
267 #[wasm_bindgen(js_name = encryptU64)]
268 pub fn encrypt_u64(
269 first_pubkey: &ElGamalPubkey,
270 second_pubkey: &ElGamalPubkey,
271 third_pubkey: &ElGamalPubkey,
272 amount: u64,
273 ) -> Self {
274 Self(GroupedElGamal::<3>::encrypt(
275 [first_pubkey, second_pubkey, third_pubkey],
276 amount,
277 ))
278 }
279
280 #[wasm_bindgen(js_name = encryptionWithU64)]
281 pub fn encryption_with_u64(
282 first_pubkey: &ElGamalPubkey,
283 second_pubkey: &ElGamalPubkey,
284 third_pubkey: &ElGamalPubkey,
285 amount: u64,
286 opening: &PedersenOpening,
287 ) -> Self {
288 Self(GroupedElGamal::<3>::encrypt_with(
289 [first_pubkey, second_pubkey, third_pubkey],
290 amount,
291 opening,
292 ))
293 }
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use {super::*, crate::encryption::elgamal::ElGamalKeypair};
300
301 #[test]
302 fn test_grouped_elgamal_encrypt_decrypt_correctness() {
303 let elgamal_keypair_0 = ElGamalKeypair::new_rand();
304 let elgamal_keypair_1 = ElGamalKeypair::new_rand();
305 let elgamal_keypair_2 = ElGamalKeypair::new_rand();
306
307 let amount: u64 = 10;
308 let grouped_ciphertext = GroupedElGamal::encrypt(
309 [
310 elgamal_keypair_0.pubkey(),
311 elgamal_keypair_1.pubkey(),
312 elgamal_keypair_2.pubkey(),
313 ],
314 amount,
315 );
316
317 assert_eq!(
318 Some(amount),
319 grouped_ciphertext
320 .decrypt_u32(elgamal_keypair_0.secret(), 0)
321 .unwrap()
322 );
323
324 assert_eq!(
325 Some(amount),
326 grouped_ciphertext
327 .decrypt_u32(elgamal_keypair_1.secret(), 1)
328 .unwrap()
329 );
330
331 assert_eq!(
332 Some(amount),
333 grouped_ciphertext
334 .decrypt_u32(elgamal_keypair_2.secret(), 2)
335 .unwrap()
336 );
337
338 assert_eq!(
339 GroupedElGamalError::IndexOutOfBounds,
340 grouped_ciphertext
341 .decrypt_u32(elgamal_keypair_0.secret(), 3)
342 .unwrap_err()
343 );
344 }
345
346 #[test]
347 fn test_grouped_ciphertext_bytes() {
348 let elgamal_keypair_0 = ElGamalKeypair::new_rand();
349 let elgamal_keypair_1 = ElGamalKeypair::new_rand();
350 let elgamal_keypair_2 = ElGamalKeypair::new_rand();
351
352 let amount: u64 = 10;
353 let grouped_ciphertext = GroupedElGamal::encrypt(
354 [
355 elgamal_keypair_0.pubkey(),
356 elgamal_keypair_1.pubkey(),
357 elgamal_keypair_2.pubkey(),
358 ],
359 amount,
360 );
361
362 let produced_bytes = grouped_ciphertext.to_bytes();
363 assert_eq!(produced_bytes.len(), 128);
364
365 let decoded_grouped_ciphertext =
366 GroupedElGamalCiphertext::<3>::from_bytes(&produced_bytes).unwrap();
367 assert_eq!(
368 Some(amount),
369 decoded_grouped_ciphertext
370 .decrypt_u32(elgamal_keypair_0.secret(), 0)
371 .unwrap()
372 );
373
374 assert_eq!(
375 Some(amount),
376 decoded_grouped_ciphertext
377 .decrypt_u32(elgamal_keypair_1.secret(), 1)
378 .unwrap()
379 );
380
381 assert_eq!(
382 Some(amount),
383 decoded_grouped_ciphertext
384 .decrypt_u32(elgamal_keypair_2.secret(), 2)
385 .unwrap()
386 );
387 }
388}