gorb_ct_ciphertext_arithmetic/
lib.rs

1use {
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
19/// Add two ElGamal ciphertexts
20pub 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
36/// Multiply an ElGamal ciphertext by a scalar
37pub 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
52/// Compute `left_ciphertext + (right_ciphertext_lo + 2^16 *
53/// right_ciphertext_hi)`
54pub 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
65/// Subtract two ElGamal ciphertexts
66pub 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
82/// Compute `left_ciphertext - (right_ciphertext_lo + 2^16 *
83/// right_ciphertext_hi)`
84pub 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
95/// Add a constant amount to a ciphertext
96pub 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
107/// Subtract a constant amount to a ciphertext
108pub 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
122/// Convert a `u64` amount into a curve-25519 scalar
123fn 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
129/// Convert a `PodElGamalCiphertext` into a tuple of commitment and decrypt
130/// handle `PodRistrettoPoint`
131fn elgamal_ciphertext_to_ristretto(
132    ciphertext: &PodElGamalCiphertext,
133) -> (PodRistrettoPoint, PodRistrettoPoint) {
134    let ciphertext_bytes = bytes_of(ciphertext); // must be of length 64 by type
135    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
143/// Convert a pair of `PodRistrettoPoint` to a `PodElGamalCiphertext`
144/// interpreting the first as the commitment and the second as the handle
145fn 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    // Unfortunately, the `solana-zk-sdk` does not exporse a constructor interface
153    // to construct `PodRistrettoPoint` from bytes. As a work-around, encode the
154    // bytes as base64 string and then convert the string to a
155    // `PodElGamalCiphertext`.
156    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        // spendable_ct should be an encryption of 0 for any public key when
182        // `PedersenOpen::default()` is used
183        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        // homomorphism should work like any other ciphertext
192        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        // transfer amount
235        let transfer_amount: u64 = 55;
236        let (amount_lo, amount_hi) = try_split_u64(transfer_amount, 16).unwrap();
237
238        // generate public keys
239        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        // commitments associated with TransferRangeProof
249        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        // decryption handles associated with TransferValidityProof
256        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        // source spendable and recipient pending
269        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        // program arithmetic for the source account
279        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        // program arithmetic for the destination account
307        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}