gorb_ct_ciphertext_arithmetic/
lib.rs1use {
2 base64::{engine::general_purpose::STANDARD, Engine},
3 bytemuck::bytes_of,
4 solana_curve25519::{
5 ristretto::{add_ristretto, multiply_ristretto, subtract_ristretto, PodRistrettoPoint},
6 scalar::PodScalar,
7 },
8 solana_zk_sdk::encryption::pod::elgamal::PodElGamalCiphertext,
9 std::str::FromStr,
10};
11
12const SHIFT_BITS: usize = 16;
13
14const G: PodRistrettoPoint = PodRistrettoPoint([
15 226, 242, 174, 10, 106, 188, 78, 113, 168, 132, 169, 97, 197, 0, 81, 95, 88, 227, 11, 106, 165,
16 130, 221, 141, 182, 166, 89, 69, 224, 141, 45, 118,
17]);
18
19pub fn add(
21 left_ciphertext: &PodElGamalCiphertext,
22 right_ciphertext: &PodElGamalCiphertext,
23) -> Option<PodElGamalCiphertext> {
24 let (left_commitment, left_handle) = elgamal_ciphertext_to_ristretto(left_ciphertext);
25 let (right_commitment, right_handle) = elgamal_ciphertext_to_ristretto(right_ciphertext);
26
27 let result_commitment = add_ristretto(&left_commitment, &right_commitment)?;
28 let result_handle = add_ristretto(&left_handle, &right_handle)?;
29
30 Some(ristretto_to_elgamal_ciphertext(
31 &result_commitment,
32 &result_handle,
33 ))
34}
35
36pub fn multiply(
38 scalar: &PodScalar,
39 ciphertext: &PodElGamalCiphertext,
40) -> Option<PodElGamalCiphertext> {
41 let (commitment, handle) = elgamal_ciphertext_to_ristretto(ciphertext);
42
43 let result_commitment = multiply_ristretto(scalar, &commitment)?;
44 let result_handle = multiply_ristretto(scalar, &handle)?;
45
46 Some(ristretto_to_elgamal_ciphertext(
47 &result_commitment,
48 &result_handle,
49 ))
50}
51
52pub fn add_with_lo_hi(
55 left_ciphertext: &PodElGamalCiphertext,
56 right_ciphertext_lo: &PodElGamalCiphertext,
57 right_ciphertext_hi: &PodElGamalCiphertext,
58) -> Option<PodElGamalCiphertext> {
59 let shift_scalar = u64_to_scalar(1_u64 << SHIFT_BITS);
60 let shifted_right_ciphertext_hi = multiply(&shift_scalar, right_ciphertext_hi)?;
61 let combined_right_ciphertext = add(right_ciphertext_lo, &shifted_right_ciphertext_hi)?;
62 add(left_ciphertext, &combined_right_ciphertext)
63}
64
65pub fn subtract(
67 left_ciphertext: &PodElGamalCiphertext,
68 right_ciphertext: &PodElGamalCiphertext,
69) -> Option<PodElGamalCiphertext> {
70 let (left_commitment, left_handle) = elgamal_ciphertext_to_ristretto(left_ciphertext);
71 let (right_commitment, right_handle) = elgamal_ciphertext_to_ristretto(right_ciphertext);
72
73 let result_commitment = subtract_ristretto(&left_commitment, &right_commitment)?;
74 let result_handle = subtract_ristretto(&left_handle, &right_handle)?;
75
76 Some(ristretto_to_elgamal_ciphertext(
77 &result_commitment,
78 &result_handle,
79 ))
80}
81
82pub fn subtract_with_lo_hi(
85 left_ciphertext: &PodElGamalCiphertext,
86 right_ciphertext_lo: &PodElGamalCiphertext,
87 right_ciphertext_hi: &PodElGamalCiphertext,
88) -> Option<PodElGamalCiphertext> {
89 let shift_scalar = u64_to_scalar(1_u64 << SHIFT_BITS);
90 let shifted_right_ciphertext_hi = multiply(&shift_scalar, right_ciphertext_hi)?;
91 let combined_right_ciphertext = add(right_ciphertext_lo, &shifted_right_ciphertext_hi)?;
92 subtract(left_ciphertext, &combined_right_ciphertext)
93}
94
95pub fn add_to(ciphertext: &PodElGamalCiphertext, amount: u64) -> Option<PodElGamalCiphertext> {
97 let amount_scalar = u64_to_scalar(amount);
98 let amount_point = multiply_ristretto(&amount_scalar, &G)?;
99
100 let (commitment, handle) = elgamal_ciphertext_to_ristretto(ciphertext);
101
102 let result_commitment = add_ristretto(&commitment, &amount_point)?;
103
104 Some(ristretto_to_elgamal_ciphertext(&result_commitment, &handle))
105}
106
107pub fn subtract_from(
109 ciphertext: &PodElGamalCiphertext,
110 amount: u64,
111) -> Option<PodElGamalCiphertext> {
112 let amount_scalar = u64_to_scalar(amount);
113 let amount_point = multiply_ristretto(&amount_scalar, &G)?;
114
115 let (commitment, handle) = elgamal_ciphertext_to_ristretto(ciphertext);
116
117 let result_commitment = subtract_ristretto(&commitment, &amount_point)?;
118
119 Some(ristretto_to_elgamal_ciphertext(&result_commitment, &handle))
120}
121
122fn u64_to_scalar(amount: u64) -> PodScalar {
124 let mut amount_bytes = [0u8; 32];
125 amount_bytes[..8].copy_from_slice(&amount.to_le_bytes());
126 PodScalar(amount_bytes)
127}
128
129fn elgamal_ciphertext_to_ristretto(
132 ciphertext: &PodElGamalCiphertext,
133) -> (PodRistrettoPoint, PodRistrettoPoint) {
134 let ciphertext_bytes = bytes_of(ciphertext); let commitment_bytes = ciphertext_bytes[..32].try_into().unwrap();
136 let handle_bytes = ciphertext_bytes[32..64].try_into().unwrap();
137 (
138 PodRistrettoPoint(commitment_bytes),
139 PodRistrettoPoint(handle_bytes),
140 )
141}
142
143fn ristretto_to_elgamal_ciphertext(
146 commitment: &PodRistrettoPoint,
147 handle: &PodRistrettoPoint,
148) -> PodElGamalCiphertext {
149 let mut ciphertext_bytes = [0u8; 64];
150 ciphertext_bytes[..32].copy_from_slice(bytes_of(commitment));
151 ciphertext_bytes[32..64].copy_from_slice(bytes_of(handle));
152 let ciphertext_string = STANDARD.encode(ciphertext_bytes);
157 FromStr::from_str(&ciphertext_string).unwrap()
158}
159
160#[cfg(test)]
161mod tests {
162 use {
163 super::*,
164 bytemuck::Zeroable,
165 curve25519_dalek::scalar::Scalar,
166 solana_zk_sdk::encryption::{
167 elgamal::{ElGamalCiphertext, ElGamalKeypair},
168 pedersen::{Pedersen, PedersenOpening},
169 pod::{elgamal::PodDecryptHandle, pedersen::PodPedersenCommitment},
170 },
171 spl_token_confidential_transfer_proof_generation::try_split_u64,
172 };
173
174 const TWO_16: u64 = 65536;
175
176 #[test]
177 fn test_zero_ct() {
178 let spendable_balance = PodElGamalCiphertext::zeroed();
179 let spendable_ct: ElGamalCiphertext = spendable_balance.try_into().unwrap();
180
181 let keypair = ElGamalKeypair::new_rand();
184 let public = keypair.pubkey();
185 let balance: u64 = 0;
186 assert_eq!(
187 spendable_ct,
188 public.encrypt_with(balance, &PedersenOpening::default())
189 );
190
191 let open = PedersenOpening::new_rand();
193 let transfer_amount_ciphertext = public.encrypt_with(55_u64, &open);
194 let transfer_amount_pod: PodElGamalCiphertext = transfer_amount_ciphertext.into();
195
196 let sum = add(&spendable_balance, &transfer_amount_pod).unwrap();
197
198 let expected: PodElGamalCiphertext = public.encrypt_with(55_u64, &open).into();
199 assert_eq!(expected, sum);
200 }
201
202 #[test]
203 fn test_add_to() {
204 let spendable_balance = PodElGamalCiphertext::zeroed();
205
206 let added_ciphertext = add_to(&spendable_balance, 55).unwrap();
207
208 let keypair = ElGamalKeypair::new_rand();
209 let public = keypair.pubkey();
210 let expected: PodElGamalCiphertext = public
211 .encrypt_with(55_u64, &PedersenOpening::default())
212 .into();
213
214 assert_eq!(expected, added_ciphertext);
215 }
216
217 #[test]
218 fn test_subtract_from() {
219 let amount = 77_u64;
220 let keypair = ElGamalKeypair::new_rand();
221 let public = keypair.pubkey();
222 let open = PedersenOpening::new_rand();
223 let encrypted_amount: PodElGamalCiphertext = public.encrypt_with(amount, &open).into();
224
225 let subtracted_ciphertext = subtract_from(&encrypted_amount, 55).unwrap();
226
227 let expected: PodElGamalCiphertext = public.encrypt_with(22_u64, &open).into();
228
229 assert_eq!(expected, subtracted_ciphertext);
230 }
231
232 #[test]
233 fn test_transfer_arithmetic() {
234 let transfer_amount: u64 = 55;
236 let (amount_lo, amount_hi) = try_split_u64(transfer_amount, 16).unwrap();
237
238 let source_keypair = ElGamalKeypair::new_rand();
240 let source_pubkey = source_keypair.pubkey();
241
242 let destination_keypair = ElGamalKeypair::new_rand();
243 let destination_pubkey = destination_keypair.pubkey();
244
245 let auditor_keypair = ElGamalKeypair::new_rand();
246 let auditor_pubkey = auditor_keypair.pubkey();
247
248 let (commitment_lo, opening_lo) = Pedersen::new(amount_lo);
250 let (commitment_hi, opening_hi) = Pedersen::new(amount_hi);
251
252 let commitment_lo: PodPedersenCommitment = commitment_lo.into();
253 let commitment_hi: PodPedersenCommitment = commitment_hi.into();
254
255 let source_handle_lo: PodDecryptHandle = source_pubkey.decrypt_handle(&opening_lo).into();
257 let destination_handle_lo: PodDecryptHandle =
258 destination_pubkey.decrypt_handle(&opening_lo).into();
259 let _auditor_handle_lo: PodDecryptHandle =
260 auditor_pubkey.decrypt_handle(&opening_lo).into();
261
262 let source_handle_hi: PodDecryptHandle = source_pubkey.decrypt_handle(&opening_hi).into();
263 let destination_handle_hi: PodDecryptHandle =
264 destination_pubkey.decrypt_handle(&opening_hi).into();
265 let _auditor_handle_hi: PodDecryptHandle =
266 auditor_pubkey.decrypt_handle(&opening_hi).into();
267
268 let source_opening = PedersenOpening::new_rand();
270 let destination_opening = PedersenOpening::new_rand();
271
272 let source_spendable_ciphertext: PodElGamalCiphertext =
273 source_pubkey.encrypt_with(77_u64, &source_opening).into();
274 let destination_pending_ciphertext: PodElGamalCiphertext = destination_pubkey
275 .encrypt_with(77_u64, &destination_opening)
276 .into();
277
278 let commitment_lo_point = PodRistrettoPoint(bytes_of(&commitment_lo).try_into().unwrap());
280 let source_handle_lo_point =
281 PodRistrettoPoint(bytes_of(&source_handle_lo).try_into().unwrap());
282
283 let commitment_hi_point = PodRistrettoPoint(bytes_of(&commitment_hi).try_into().unwrap());
284 let source_handle_hi_point =
285 PodRistrettoPoint(bytes_of(&source_handle_hi).try_into().unwrap());
286
287 let source_ciphertext_lo =
288 ristretto_to_elgamal_ciphertext(&commitment_lo_point, &source_handle_lo_point);
289 let source_ciphertext_hi =
290 ristretto_to_elgamal_ciphertext(&commitment_hi_point, &source_handle_hi_point);
291
292 let final_source_spendable = subtract_with_lo_hi(
293 &source_spendable_ciphertext,
294 &source_ciphertext_lo,
295 &source_ciphertext_hi,
296 )
297 .unwrap();
298
299 let final_source_opening =
300 source_opening - (opening_lo.clone() + opening_hi.clone() * Scalar::from(TWO_16));
301 let expected_source: PodElGamalCiphertext = source_pubkey
302 .encrypt_with(22_u64, &final_source_opening)
303 .into();
304 assert_eq!(expected_source, final_source_spendable);
305
306 let destination_handle_lo_point =
308 PodRistrettoPoint(bytes_of(&destination_handle_lo).try_into().unwrap());
309 let destination_handle_hi_point =
310 PodRistrettoPoint(bytes_of(&destination_handle_hi).try_into().unwrap());
311
312 let destination_ciphertext_lo =
313 ristretto_to_elgamal_ciphertext(&commitment_lo_point, &destination_handle_lo_point);
314 let destination_ciphertext_hi =
315 ristretto_to_elgamal_ciphertext(&commitment_hi_point, &destination_handle_hi_point);
316
317 let final_destination_pending_ciphertext = add_with_lo_hi(
318 &destination_pending_ciphertext,
319 &destination_ciphertext_lo,
320 &destination_ciphertext_hi,
321 )
322 .unwrap();
323
324 let final_destination_opening =
325 destination_opening + (opening_lo + opening_hi * Scalar::from(TWO_16));
326 let expected_destination_ciphertext: PodElGamalCiphertext = destination_pubkey
327 .encrypt_with(132_u64, &final_destination_opening)
328 .into();
329 assert_eq!(
330 expected_destination_ciphertext,
331 final_destination_pending_ciphertext
332 );
333 }
334}