solana_zk_sdk/encryption/pod/
grouped_elgamal.rs

1//! Plain Old Data types for the Grouped ElGamal encryption scheme.
2
3#[cfg(not(target_os = "solana"))]
4use crate::encryption::grouped_elgamal::GroupedElGamalCiphertext;
5#[cfg(target_arch = "wasm32")]
6use wasm_bindgen::prelude::*;
7use {
8    crate::{
9        encryption::{
10            pod::{elgamal::PodElGamalCiphertext, pedersen::PodPedersenCommitment},
11            DECRYPT_HANDLE_LEN, ELGAMAL_CIPHERTEXT_LEN, PEDERSEN_COMMITMENT_LEN,
12        },
13        errors::ElGamalError,
14        pod::{impl_from_bytes, impl_from_str},
15    },
16    base64::{prelude::BASE64_STANDARD, Engine},
17    bytemuck::Zeroable,
18    std::fmt,
19};
20
21/// Maximum length of a base64 encoded grouped ElGamal ciphertext with 2 handles
22const GROUPED_ELGAMAL_CIPHERTEXT_2_HANDLES_MAX_BASE64_LEN: usize = 132;
23
24/// Maximum length of a base64 encoded grouped ElGamal ciphertext with 3 handles
25const GROUPED_ELGAMAL_CIPHERTEXT_3_HANDLES_MAX_BASE64_LEN: usize = 176;
26
27macro_rules! impl_extract {
28    (TYPE = $type:ident) => {
29        impl $type {
30            /// Extract the commitment component from a grouped ciphertext
31            pub fn extract_commitment(&self) -> PodPedersenCommitment {
32                // `GROUPED_ELGAMAL_CIPHERTEXT_2_HANDLES` guaranteed to be at least `PEDERSEN_COMMITMENT_LEN`
33                let commitment = self.0[..PEDERSEN_COMMITMENT_LEN].try_into().unwrap();
34                PodPedersenCommitment(commitment)
35            }
36
37            /// Extract a regular ElGamal ciphertext using the decrypt handle at a specified index.
38            pub fn try_extract_ciphertext(
39                &self,
40                index: usize,
41            ) -> Result<PodElGamalCiphertext, ElGamalError> {
42                let mut ciphertext_bytes = [0u8; ELGAMAL_CIPHERTEXT_LEN];
43                ciphertext_bytes[..PEDERSEN_COMMITMENT_LEN]
44                    .copy_from_slice(&self.0[..PEDERSEN_COMMITMENT_LEN]);
45
46                let handle_start = DECRYPT_HANDLE_LEN
47                    .checked_mul(index)
48                    .and_then(|n| n.checked_add(PEDERSEN_COMMITMENT_LEN))
49                    .ok_or(ElGamalError::CiphertextDeserialization)?;
50                let handle_end = handle_start
51                    .checked_add(DECRYPT_HANDLE_LEN)
52                    .ok_or(ElGamalError::CiphertextDeserialization)?;
53                ciphertext_bytes[PEDERSEN_COMMITMENT_LEN..].copy_from_slice(
54                    self.0
55                        .get(handle_start..handle_end)
56                        .ok_or(ElGamalError::CiphertextDeserialization)?,
57                );
58
59                Ok(PodElGamalCiphertext(ciphertext_bytes))
60            }
61        }
62    };
63}
64
65/// Byte length of a grouped ElGamal ciphertext with 2 handles
66const GROUPED_ELGAMAL_CIPHERTEXT_2_HANDLES: usize =
67    PEDERSEN_COMMITMENT_LEN + DECRYPT_HANDLE_LEN + DECRYPT_HANDLE_LEN;
68
69/// Byte length of a grouped ElGamal ciphertext with 3 handles
70const GROUPED_ELGAMAL_CIPHERTEXT_3_HANDLES: usize =
71    PEDERSEN_COMMITMENT_LEN + DECRYPT_HANDLE_LEN + DECRYPT_HANDLE_LEN + DECRYPT_HANDLE_LEN;
72
73/// The `GroupedElGamalCiphertext` type with two decryption handles as a `Pod`
74#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
75#[derive(Clone, Copy, bytemuck_derive::Pod, bytemuck_derive::Zeroable, PartialEq, Eq)]
76#[repr(transparent)]
77pub struct PodGroupedElGamalCiphertext2Handles(
78    pub(crate) [u8; GROUPED_ELGAMAL_CIPHERTEXT_2_HANDLES],
79);
80
81impl fmt::Debug for PodGroupedElGamalCiphertext2Handles {
82    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
83        write!(f, "{:?}", self.0)
84    }
85}
86
87impl Default for PodGroupedElGamalCiphertext2Handles {
88    fn default() -> Self {
89        Self::zeroed()
90    }
91}
92
93impl fmt::Display for PodGroupedElGamalCiphertext2Handles {
94    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
95        write!(f, "{}", BASE64_STANDARD.encode(self.0))
96    }
97}
98
99impl_from_str!(
100    TYPE = PodGroupedElGamalCiphertext2Handles,
101    BYTES_LEN = GROUPED_ELGAMAL_CIPHERTEXT_2_HANDLES,
102    BASE64_LEN = GROUPED_ELGAMAL_CIPHERTEXT_2_HANDLES_MAX_BASE64_LEN
103);
104
105impl_from_bytes!(
106    TYPE = PodGroupedElGamalCiphertext2Handles,
107    BYTES_LEN = GROUPED_ELGAMAL_CIPHERTEXT_2_HANDLES
108);
109
110#[cfg(not(target_os = "solana"))]
111impl From<GroupedElGamalCiphertext<2>> for PodGroupedElGamalCiphertext2Handles {
112    fn from(decoded_ciphertext: GroupedElGamalCiphertext<2>) -> Self {
113        Self(decoded_ciphertext.to_bytes().try_into().unwrap())
114    }
115}
116
117#[cfg(not(target_os = "solana"))]
118impl TryFrom<PodGroupedElGamalCiphertext2Handles> for GroupedElGamalCiphertext<2> {
119    type Error = ElGamalError;
120
121    fn try_from(pod_ciphertext: PodGroupedElGamalCiphertext2Handles) -> Result<Self, Self::Error> {
122        Self::from_bytes(&pod_ciphertext.0).ok_or(ElGamalError::CiphertextDeserialization)
123    }
124}
125
126impl_extract!(TYPE = PodGroupedElGamalCiphertext2Handles);
127
128/// The `GroupedElGamalCiphertext` type with three decryption handles as a `Pod`
129#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
130#[derive(Clone, Copy, bytemuck_derive::Pod, bytemuck_derive::Zeroable, PartialEq, Eq)]
131#[repr(transparent)]
132pub struct PodGroupedElGamalCiphertext3Handles(
133    pub(crate) [u8; GROUPED_ELGAMAL_CIPHERTEXT_3_HANDLES],
134);
135
136impl fmt::Debug for PodGroupedElGamalCiphertext3Handles {
137    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
138        write!(f, "{:?}", self.0)
139    }
140}
141
142impl Default for PodGroupedElGamalCiphertext3Handles {
143    fn default() -> Self {
144        Self::zeroed()
145    }
146}
147
148impl fmt::Display for PodGroupedElGamalCiphertext3Handles {
149    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
150        write!(f, "{}", BASE64_STANDARD.encode(self.0))
151    }
152}
153
154impl_from_str!(
155    TYPE = PodGroupedElGamalCiphertext3Handles,
156    BYTES_LEN = GROUPED_ELGAMAL_CIPHERTEXT_3_HANDLES,
157    BASE64_LEN = GROUPED_ELGAMAL_CIPHERTEXT_3_HANDLES_MAX_BASE64_LEN
158);
159
160impl_from_bytes!(
161    TYPE = PodGroupedElGamalCiphertext3Handles,
162    BYTES_LEN = GROUPED_ELGAMAL_CIPHERTEXT_3_HANDLES
163);
164
165#[cfg(not(target_os = "solana"))]
166impl From<GroupedElGamalCiphertext<3>> for PodGroupedElGamalCiphertext3Handles {
167    fn from(decoded_ciphertext: GroupedElGamalCiphertext<3>) -> Self {
168        Self(decoded_ciphertext.to_bytes().try_into().unwrap())
169    }
170}
171
172#[cfg(not(target_os = "solana"))]
173impl TryFrom<PodGroupedElGamalCiphertext3Handles> for GroupedElGamalCiphertext<3> {
174    type Error = ElGamalError;
175
176    fn try_from(pod_ciphertext: PodGroupedElGamalCiphertext3Handles) -> Result<Self, Self::Error> {
177        Self::from_bytes(&pod_ciphertext.0).ok_or(ElGamalError::CiphertextDeserialization)
178    }
179}
180
181impl_extract!(TYPE = PodGroupedElGamalCiphertext3Handles);
182
183#[cfg(test)]
184mod tests {
185    use {
186        super::*,
187        crate::encryption::{
188            elgamal::ElGamalKeypair, grouped_elgamal::GroupedElGamal, pedersen::Pedersen,
189            pod::pedersen::PodPedersenCommitment,
190        },
191    };
192
193    #[test]
194    fn test_2_handles_ciphertext_extraction() {
195        let elgamal_keypair_0 = ElGamalKeypair::new_rand();
196        let elgamal_keypair_1 = ElGamalKeypair::new_rand();
197
198        let amount: u64 = 10;
199        let (commitment, opening) = Pedersen::new(amount);
200
201        let grouped_ciphertext = GroupedElGamal::encrypt_with(
202            [elgamal_keypair_0.pubkey(), elgamal_keypair_1.pubkey()],
203            amount,
204            &opening,
205        );
206        let pod_grouped_ciphertext: PodGroupedElGamalCiphertext2Handles = grouped_ciphertext.into();
207
208        let expected_pod_commitment: PodPedersenCommitment = commitment.into();
209        let actual_pod_commitment = pod_grouped_ciphertext.extract_commitment();
210        assert_eq!(expected_pod_commitment, actual_pod_commitment);
211
212        let expected_ciphertext_0 = elgamal_keypair_0.pubkey().encrypt_with(amount, &opening);
213        let expected_pod_ciphertext_0: PodElGamalCiphertext = expected_ciphertext_0.into();
214        let actual_pod_ciphertext_0 = pod_grouped_ciphertext.try_extract_ciphertext(0).unwrap();
215        assert_eq!(expected_pod_ciphertext_0, actual_pod_ciphertext_0);
216
217        let expected_ciphertext_1 = elgamal_keypair_1.pubkey().encrypt_with(amount, &opening);
218        let expected_pod_ciphertext_1: PodElGamalCiphertext = expected_ciphertext_1.into();
219        let actual_pod_ciphertext_1 = pod_grouped_ciphertext.try_extract_ciphertext(1).unwrap();
220        assert_eq!(expected_pod_ciphertext_1, actual_pod_ciphertext_1);
221
222        let err = pod_grouped_ciphertext
223            .try_extract_ciphertext(2)
224            .unwrap_err();
225        assert_eq!(err, ElGamalError::CiphertextDeserialization);
226    }
227
228    #[test]
229    fn test_3_handles_ciphertext_extraction() {
230        let elgamal_keypair_0 = ElGamalKeypair::new_rand();
231        let elgamal_keypair_1 = ElGamalKeypair::new_rand();
232        let elgamal_keypair_2 = ElGamalKeypair::new_rand();
233
234        let amount: u64 = 10;
235        let (commitment, opening) = Pedersen::new(amount);
236
237        let grouped_ciphertext = GroupedElGamal::encrypt_with(
238            [
239                elgamal_keypair_0.pubkey(),
240                elgamal_keypair_1.pubkey(),
241                elgamal_keypair_2.pubkey(),
242            ],
243            amount,
244            &opening,
245        );
246        let pod_grouped_ciphertext: PodGroupedElGamalCiphertext3Handles = grouped_ciphertext.into();
247
248        let expected_pod_commitment: PodPedersenCommitment = commitment.into();
249        let actual_pod_commitment = pod_grouped_ciphertext.extract_commitment();
250        assert_eq!(expected_pod_commitment, actual_pod_commitment);
251
252        let expected_ciphertext_0 = elgamal_keypair_0.pubkey().encrypt_with(amount, &opening);
253        let expected_pod_ciphertext_0: PodElGamalCiphertext = expected_ciphertext_0.into();
254        let actual_pod_ciphertext_0 = pod_grouped_ciphertext.try_extract_ciphertext(0).unwrap();
255        assert_eq!(expected_pod_ciphertext_0, actual_pod_ciphertext_0);
256
257        let expected_ciphertext_1 = elgamal_keypair_1.pubkey().encrypt_with(amount, &opening);
258        let expected_pod_ciphertext_1: PodElGamalCiphertext = expected_ciphertext_1.into();
259        let actual_pod_ciphertext_1 = pod_grouped_ciphertext.try_extract_ciphertext(1).unwrap();
260        assert_eq!(expected_pod_ciphertext_1, actual_pod_ciphertext_1);
261
262        let expected_ciphertext_2 = elgamal_keypair_2.pubkey().encrypt_with(amount, &opening);
263        let expected_pod_ciphertext_2: PodElGamalCiphertext = expected_ciphertext_2.into();
264        let actual_pod_ciphertext_2 = pod_grouped_ciphertext.try_extract_ciphertext(2).unwrap();
265        assert_eq!(expected_pod_ciphertext_2, actual_pod_ciphertext_2);
266
267        let err = pod_grouped_ciphertext
268            .try_extract_ciphertext(3)
269            .unwrap_err();
270        assert_eq!(err, ElGamalError::CiphertextDeserialization);
271    }
272}