solana_zk_sdk/encryption/
grouped_elgamal.rs

1//! The twisted ElGamal group encryption implementation.
2//!
3//! The message space consists of any number that is representable as a scalar (a.k.a. "exponent")
4//! for Curve25519.
5//!
6//! A regular twisted ElGamal ciphertext consists of two components:
7//! - A Pedersen commitment that encodes a message to be encrypted
8//! - A "decryption handle" that binds the Pedersen opening to a specific public key
9//!
10//! The ciphertext can be generalized to hold not a single decryption handle, but multiple handles
11//! pertaining to multiple ElGamal public keys. These ciphertexts are referred to as a "grouped"
12//! ElGamal ciphertext.
13//!
14
15#[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
35/// Algorithm handle for the grouped ElGamal encryption
36pub struct GroupedElGamal<const N: usize>;
37impl<const N: usize> GroupedElGamal<N> {
38    /// Encrypts an amount under an array of ElGamal public keys.
39    ///
40    /// This function is randomized. It internally samples a scalar element using `OsRng`.
41    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    /// Encrypts an amount under an array of ElGamal public keys using a specified Pedersen
60    /// opening.
61    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    /// Converts a grouped ElGamal ciphertext into a regular ElGamal ciphertext using the decrypt
81    /// handle at a specified index.
82    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    /// Decrypts a grouped ElGamal ciphertext using an ElGamal secret key pertaining to a
101    /// decryption handle at a specified index.
102    ///
103    /// The output of this function is of type `DiscreteLog`. To recover the originally encrypted
104    /// amount, use `DiscreteLog::decode`.
105    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    /// Decrypts a grouped ElGamal ciphertext to a number that is interpreted as a positive 32-bit
115    /// number (but still of type `u64`).
116    ///
117    /// If the originally encrypted amount is not a positive 32-bit number, then the function
118    /// Result contains `None`.
119    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/// A grouped ElGamal ciphertext.
130///
131/// The type is defined with a generic constant parameter that specifies the number of
132/// decryption handles that the ciphertext holds.
133#[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    /// Converts a grouped ElGamal ciphertext into a regular ElGamal ciphertext using the decrypt
141    /// handle at a specified index.
142    pub fn to_elgamal_ciphertext(
143        &self,
144        index: usize,
145    ) -> Result<ElGamalCiphertext, GroupedElGamalError> {
146        GroupedElGamal::to_elgamal_ciphertext(self, index)
147    }
148
149    /// The expected length of a serialized grouped ElGamal ciphertext.
150    ///
151    /// A grouped ElGamal ciphertext consists of a Pedersen commitment and an array of decryption
152    /// handles. The commitment and decryption handles are each a single Curve25519 group element
153    /// that is serialized as 32 bytes. Therefore, the total byte length of a grouped ciphertext is
154    /// `(N+1) * 32`.
155    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    /// Decrypts the grouped ElGamal ciphertext using an ElGamal secret key pertaining to a
193    /// specified index.
194    ///
195    /// The output of this function is of type `DiscreteLog`. To recover the originally encrypted
196    /// amount, use `DiscreteLog::decode`.
197    pub fn decrypt(
198        &self,
199        secret: &ElGamalSecretKey,
200        index: usize,
201    ) -> Result<DiscreteLog, GroupedElGamalError> {
202        GroupedElGamal::decrypt(self, secret, index)
203    }
204
205    /// Decrypts the grouped ElGamal ciphertext to a number that is interpreted as a positive 32-bit
206    /// number (but still of type `u64`).
207    ///
208    /// If the originally encrypted amount is not a positive 32-bit number, then the function
209    /// returns `None`.
210    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}