solana_zk_sdk/encryption/
grouped_elgamal.rs1#[cfg(not(target_arch = "wasm32"))]
16use crate::encryption::{discrete_log::DiscreteLog, elgamal::ElGamalSecretKey};
17use {
18 crate::{
19 encryption::{
20 elgamal::{DecryptHandle, ElGamalCiphertext, ElGamalPubkey},
21 pedersen::{Pedersen, PedersenCommitment, PedersenOpening},
22 },
23 RISTRETTO_POINT_LEN,
24 },
25 curve25519_dalek::scalar::Scalar,
26 thiserror::Error,
27};
28
29#[derive(Error, Clone, Debug, Eq, PartialEq)]
30pub enum GroupedElGamalError {
31 #[error("index out of bounds")]
32 IndexOutOfBounds,
33}
34
35pub struct GroupedElGamal<const N: usize>;
37impl<const N: usize> GroupedElGamal<N> {
38 pub fn encrypt<T: Into<Scalar>>(
42 pubkeys: [&ElGamalPubkey; N],
43 amount: T,
44 ) -> GroupedElGamalCiphertext<N> {
45 let (commitment, opening) = Pedersen::new(amount);
46 let handles: [DecryptHandle; N] = pubkeys
47 .iter()
48 .map(|handle| handle.decrypt_handle(&opening))
49 .collect::<Vec<DecryptHandle>>()
50 .try_into()
51 .unwrap();
52
53 GroupedElGamalCiphertext {
54 commitment,
55 handles,
56 }
57 }
58
59 pub fn encrypt_with<T: Into<Scalar>>(
62 pubkeys: [&ElGamalPubkey; N],
63 amount: T,
64 opening: &PedersenOpening,
65 ) -> GroupedElGamalCiphertext<N> {
66 let commitment = Pedersen::with(amount, opening);
67 let handles: [DecryptHandle; N] = pubkeys
68 .iter()
69 .map(|handle| handle.decrypt_handle(opening))
70 .collect::<Vec<DecryptHandle>>()
71 .try_into()
72 .unwrap();
73
74 GroupedElGamalCiphertext {
75 commitment,
76 handles,
77 }
78 }
79
80 fn to_elgamal_ciphertext(
83 grouped_ciphertext: &GroupedElGamalCiphertext<N>,
84 index: usize,
85 ) -> Result<ElGamalCiphertext, GroupedElGamalError> {
86 let handle = grouped_ciphertext
87 .handles
88 .get(index)
89 .ok_or(GroupedElGamalError::IndexOutOfBounds)?;
90
91 Ok(ElGamalCiphertext {
92 commitment: grouped_ciphertext.commitment,
93 handle: *handle,
94 })
95 }
96}
97
98#[cfg(not(target_arch = "wasm32"))]
99impl<const N: usize> GroupedElGamal<N> {
100 fn decrypt(
106 grouped_ciphertext: &GroupedElGamalCiphertext<N>,
107 secret: &ElGamalSecretKey,
108 index: usize,
109 ) -> Result<DiscreteLog, GroupedElGamalError> {
110 Self::to_elgamal_ciphertext(grouped_ciphertext, index)
111 .map(|ciphertext| ciphertext.decrypt(secret))
112 }
113
114 fn decrypt_u32(
120 grouped_ciphertext: &GroupedElGamalCiphertext<N>,
121 secret: &ElGamalSecretKey,
122 index: usize,
123 ) -> Result<Option<u64>, GroupedElGamalError> {
124 Self::to_elgamal_ciphertext(grouped_ciphertext, index)
125 .map(|ciphertext| ciphertext.decrypt_u32(secret))
126 }
127}
128
129#[derive(Clone, Copy, Debug, Eq, PartialEq)]
134pub struct GroupedElGamalCiphertext<const N: usize> {
135 pub commitment: PedersenCommitment,
136 pub handles: [DecryptHandle; N],
137}
138
139impl<const N: usize> GroupedElGamalCiphertext<N> {
140 pub fn to_elgamal_ciphertext(
143 &self,
144 index: usize,
145 ) -> Result<ElGamalCiphertext, GroupedElGamalError> {
146 GroupedElGamal::to_elgamal_ciphertext(self, index)
147 }
148
149 fn expected_byte_length() -> usize {
156 N.checked_add(1)
157 .and_then(|length| length.checked_mul(RISTRETTO_POINT_LEN))
158 .unwrap()
159 }
160
161 pub fn to_bytes(&self) -> Vec<u8> {
162 let mut buf = Vec::with_capacity(Self::expected_byte_length());
163 buf.extend_from_slice(&self.commitment.to_bytes());
164 self.handles
165 .iter()
166 .for_each(|handle| buf.extend_from_slice(&handle.to_bytes()));
167 buf
168 }
169
170 pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
171 if bytes.len() != Self::expected_byte_length() {
172 return None;
173 }
174
175 let mut iter = bytes.chunks(RISTRETTO_POINT_LEN);
176 let commitment = PedersenCommitment::from_bytes(iter.next()?)?;
177
178 let mut handles = Vec::with_capacity(N);
179 for handle_bytes in iter {
180 handles.push(DecryptHandle::from_bytes(handle_bytes)?);
181 }
182
183 Some(Self {
184 commitment,
185 handles: handles.try_into().unwrap(),
186 })
187 }
188}
189
190#[cfg(not(target_arch = "wasm32"))]
191impl<const N: usize> GroupedElGamalCiphertext<N> {
192 pub fn decrypt(
198 &self,
199 secret: &ElGamalSecretKey,
200 index: usize,
201 ) -> Result<DiscreteLog, GroupedElGamalError> {
202 GroupedElGamal::decrypt(self, secret, index)
203 }
204
205 pub fn decrypt_u32(
211 &self,
212 secret: &ElGamalSecretKey,
213 index: usize,
214 ) -> Result<Option<u64>, GroupedElGamalError> {
215 GroupedElGamal::decrypt_u32(self, secret, index)
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use {super::*, crate::encryption::elgamal::ElGamalKeypair};
222
223 #[test]
224 fn test_grouped_elgamal_encrypt_decrypt_correctness() {
225 let elgamal_keypair_0 = ElGamalKeypair::new_rand();
226 let elgamal_keypair_1 = ElGamalKeypair::new_rand();
227 let elgamal_keypair_2 = ElGamalKeypair::new_rand();
228
229 let amount: u64 = 10;
230 let grouped_ciphertext = GroupedElGamal::encrypt(
231 [
232 elgamal_keypair_0.pubkey(),
233 elgamal_keypair_1.pubkey(),
234 elgamal_keypair_2.pubkey(),
235 ],
236 amount,
237 );
238
239 assert_eq!(
240 Some(amount),
241 grouped_ciphertext
242 .decrypt_u32(elgamal_keypair_0.secret(), 0)
243 .unwrap()
244 );
245
246 assert_eq!(
247 Some(amount),
248 grouped_ciphertext
249 .decrypt_u32(elgamal_keypair_1.secret(), 1)
250 .unwrap()
251 );
252
253 assert_eq!(
254 Some(amount),
255 grouped_ciphertext
256 .decrypt_u32(elgamal_keypair_2.secret(), 2)
257 .unwrap()
258 );
259
260 assert_eq!(
261 GroupedElGamalError::IndexOutOfBounds,
262 grouped_ciphertext
263 .decrypt_u32(elgamal_keypair_0.secret(), 3)
264 .unwrap_err()
265 );
266 }
267
268 #[test]
269 fn test_grouped_ciphertext_bytes() {
270 let elgamal_keypair_0 = ElGamalKeypair::new_rand();
271 let elgamal_keypair_1 = ElGamalKeypair::new_rand();
272 let elgamal_keypair_2 = ElGamalKeypair::new_rand();
273
274 let amount: u64 = 10;
275 let grouped_ciphertext = GroupedElGamal::encrypt(
276 [
277 elgamal_keypair_0.pubkey(),
278 elgamal_keypair_1.pubkey(),
279 elgamal_keypair_2.pubkey(),
280 ],
281 amount,
282 );
283
284 let produced_bytes = grouped_ciphertext.to_bytes();
285 assert_eq!(produced_bytes.len(), 128);
286
287 let decoded_grouped_ciphertext =
288 GroupedElGamalCiphertext::<3>::from_bytes(&produced_bytes).unwrap();
289 assert_eq!(
290 Some(amount),
291 decoded_grouped_ciphertext
292 .decrypt_u32(elgamal_keypair_0.secret(), 0)
293 .unwrap()
294 );
295
296 assert_eq!(
297 Some(amount),
298 decoded_grouped_ciphertext
299 .decrypt_u32(elgamal_keypair_1.secret(), 1)
300 .unwrap()
301 );
302
303 assert_eq!(
304 Some(amount),
305 decoded_grouped_ciphertext
306 .decrypt_u32(elgamal_keypair_2.secret(), 2)
307 .unwrap()
308 );
309 }
310}