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