Skip to main content

rialo_s_spl_token_confidential_transfer_ciphertext_arithmetic/
lib.rs

1// Copyright (c) Subzero Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::str::FromStr;
5
6use base64::{engine::general_purpose::STANDARD, Engine};
7use bytemuck::bytes_of;
8use rialo_s_curve25519::{
9    ristretto::{add_ristretto, multiply_ristretto, subtract_ristretto, PodRistrettoPoint},
10    scalar::PodScalar,
11};
12use rialo_s_zk_sdk::encryption::pod::elgamal::PodElGamalCiphertext;
13
14const SHIFT_BITS: usize = 16;
15
16const G: PodRistrettoPoint = PodRistrettoPoint([
17    226, 242, 174, 10, 106, 188, 78, 113, 168, 132, 169, 97, 197, 0, 81, 95, 88, 227, 11, 106, 165,
18    130, 221, 141, 182, 166, 89, 69, 224, 141, 45, 118,
19]);
20
21/// Add two ElGamal ciphertexts
22pub fn add(
23    left_ciphertext: &PodElGamalCiphertext,
24    right_ciphertext: &PodElGamalCiphertext,
25) -> Option<PodElGamalCiphertext> {
26    let (left_commitment, left_handle) = elgamal_ciphertext_to_ristretto(left_ciphertext);
27    let (right_commitment, right_handle) = elgamal_ciphertext_to_ristretto(right_ciphertext);
28
29    let result_commitment = add_ristretto(&left_commitment, &right_commitment)?;
30    let result_handle = add_ristretto(&left_handle, &right_handle)?;
31
32    Some(ristretto_to_elgamal_ciphertext(
33        &result_commitment,
34        &result_handle,
35    ))
36}
37
38/// Multiply an ElGamal ciphertext by a scalar
39pub fn multiply(
40    scalar: &PodScalar,
41    ciphertext: &PodElGamalCiphertext,
42) -> Option<PodElGamalCiphertext> {
43    let (commitment, handle) = elgamal_ciphertext_to_ristretto(ciphertext);
44
45    let result_commitment = multiply_ristretto(scalar, &commitment)?;
46    let result_handle = multiply_ristretto(scalar, &handle)?;
47
48    Some(ristretto_to_elgamal_ciphertext(
49        &result_commitment,
50        &result_handle,
51    ))
52}
53
54/// Compute `left_ciphertext + (right_ciphertext_lo + 2^16 *
55/// right_ciphertext_hi)`
56pub fn add_with_lo_hi(
57    left_ciphertext: &PodElGamalCiphertext,
58    right_ciphertext_lo: &PodElGamalCiphertext,
59    right_ciphertext_hi: &PodElGamalCiphertext,
60) -> Option<PodElGamalCiphertext> {
61    let shift_scalar = u64_to_scalar(1_u64 << SHIFT_BITS);
62    let shifted_right_ciphertext_hi = multiply(&shift_scalar, right_ciphertext_hi)?;
63    let combined_right_ciphertext = add(right_ciphertext_lo, &shifted_right_ciphertext_hi)?;
64    add(left_ciphertext, &combined_right_ciphertext)
65}
66
67/// Subtract two ElGamal ciphertexts
68pub fn subtract(
69    left_ciphertext: &PodElGamalCiphertext,
70    right_ciphertext: &PodElGamalCiphertext,
71) -> Option<PodElGamalCiphertext> {
72    let (left_commitment, left_handle) = elgamal_ciphertext_to_ristretto(left_ciphertext);
73    let (right_commitment, right_handle) = elgamal_ciphertext_to_ristretto(right_ciphertext);
74
75    let result_commitment = subtract_ristretto(&left_commitment, &right_commitment)?;
76    let result_handle = subtract_ristretto(&left_handle, &right_handle)?;
77
78    Some(ristretto_to_elgamal_ciphertext(
79        &result_commitment,
80        &result_handle,
81    ))
82}
83
84/// Compute `left_ciphertext - (right_ciphertext_lo + 2^16 *
85/// right_ciphertext_hi)`
86pub fn subtract_with_lo_hi(
87    left_ciphertext: &PodElGamalCiphertext,
88    right_ciphertext_lo: &PodElGamalCiphertext,
89    right_ciphertext_hi: &PodElGamalCiphertext,
90) -> Option<PodElGamalCiphertext> {
91    let shift_scalar = u64_to_scalar(1_u64 << SHIFT_BITS);
92    let shifted_right_ciphertext_hi = multiply(&shift_scalar, right_ciphertext_hi)?;
93    let combined_right_ciphertext = add(right_ciphertext_lo, &shifted_right_ciphertext_hi)?;
94    subtract(left_ciphertext, &combined_right_ciphertext)
95}
96
97/// Add a constant amount to a ciphertext
98pub fn add_to(ciphertext: &PodElGamalCiphertext, amount: u64) -> Option<PodElGamalCiphertext> {
99    let amount_scalar = u64_to_scalar(amount);
100    let amount_point = multiply_ristretto(&amount_scalar, &G)?;
101
102    let (commitment, handle) = elgamal_ciphertext_to_ristretto(ciphertext);
103
104    let result_commitment = add_ristretto(&commitment, &amount_point)?;
105
106    Some(ristretto_to_elgamal_ciphertext(&result_commitment, &handle))
107}
108
109/// Subtract a constant amount to a ciphertext
110pub fn subtract_from(
111    ciphertext: &PodElGamalCiphertext,
112    amount: u64,
113) -> Option<PodElGamalCiphertext> {
114    let amount_scalar = u64_to_scalar(amount);
115    let amount_point = multiply_ristretto(&amount_scalar, &G)?;
116
117    let (commitment, handle) = elgamal_ciphertext_to_ristretto(ciphertext);
118
119    let result_commitment = subtract_ristretto(&commitment, &amount_point)?;
120
121    Some(ristretto_to_elgamal_ciphertext(&result_commitment, &handle))
122}
123
124/// Convert a `u64` amount into a curve-25519 scalar
125fn u64_to_scalar(amount: u64) -> PodScalar {
126    let mut amount_bytes = [0u8; 32];
127    amount_bytes[..8].copy_from_slice(&amount.to_le_bytes());
128    PodScalar(amount_bytes)
129}
130
131/// Convert a `PodElGamalCiphertext` into a tuple of commitment and decrypt
132/// handle `PodRistrettoPoint`
133fn elgamal_ciphertext_to_ristretto(
134    ciphertext: &PodElGamalCiphertext,
135) -> (PodRistrettoPoint, PodRistrettoPoint) {
136    let ciphertext_bytes = bytes_of(ciphertext); // must be of length 64 by type
137    let commitment_bytes = ciphertext_bytes[..32].try_into().unwrap();
138    let handle_bytes = ciphertext_bytes[32..64].try_into().unwrap();
139    (
140        PodRistrettoPoint(commitment_bytes),
141        PodRistrettoPoint(handle_bytes),
142    )
143}
144
145/// Convert a pair of `PodRistrettoPoint` to a `PodElGamalCiphertext`
146/// interpreting the first as the commitment and the second as the handle
147#[allow(clippy::disallowed_methods)]
148fn ristretto_to_elgamal_ciphertext(
149    commitment: &PodRistrettoPoint,
150    handle: &PodRistrettoPoint,
151) -> PodElGamalCiphertext {
152    let mut ciphertext_bytes = [0u8; 64];
153    ciphertext_bytes[..32].copy_from_slice(bytes_of(commitment));
154    ciphertext_bytes[32..64].copy_from_slice(bytes_of(handle));
155    // Unfortunately, the `solana-zk-sdk` does not exporse a constructor interface
156    // to construct `PodRistrettoPoint` from bytes. As a work-around, encode the
157    // bytes as base64 string and then convert the string to a
158    // `PodElGamalCiphertext`.
159    let ciphertext_string = STANDARD.encode(ciphertext_bytes);
160    FromStr::from_str(&ciphertext_string).unwrap()
161}
162
163#[cfg(test)]
164mod tests {
165    use bytemuck::Zeroable;
166    use curve25519_dalek::scalar::Scalar;
167    use rialo_s_spl_token_confidential_transfer_proof_generation::try_split_u64;
168    use rialo_s_zk_sdk::encryption::{
169        elgamal::{ElGamalCiphertext, ElGamalKeypair},
170        pedersen::{Pedersen, PedersenOpening},
171        pod::{elgamal::PodDecryptHandle, pedersen::PodPedersenCommitment},
172    };
173
174    use super::*;
175
176    const TWO_16: u64 = 65536;
177
178    #[test]
179    fn test_zero_ct() {
180        let spendable_balance = PodElGamalCiphertext::zeroed();
181        let spendable_ct: ElGamalCiphertext = spendable_balance.try_into().unwrap();
182
183        // spendable_ct should be an encryption of 0 for any public key when
184        // `PedersenOpen::default()` is used
185        let keypair = ElGamalKeypair::new_rand();
186        let public = keypair.pubkey();
187        let balance: u64 = 0;
188        assert_eq!(
189            spendable_ct,
190            public.encrypt_with(balance, &PedersenOpening::default())
191        );
192
193        // homomorphism should work like any other ciphertext
194        let open = PedersenOpening::new_rand();
195        let transfer_amount_ciphertext = public.encrypt_with(55_u64, &open);
196        let transfer_amount_pod: PodElGamalCiphertext = transfer_amount_ciphertext.into();
197
198        let sum = add(&spendable_balance, &transfer_amount_pod).unwrap();
199
200        let expected: PodElGamalCiphertext = public.encrypt_with(55_u64, &open).into();
201        assert_eq!(expected, sum);
202    }
203
204    #[test]
205    fn test_add_to() {
206        let spendable_balance = PodElGamalCiphertext::zeroed();
207
208        let added_ciphertext = add_to(&spendable_balance, 55).unwrap();
209
210        let keypair = ElGamalKeypair::new_rand();
211        let public = keypair.pubkey();
212        let expected: PodElGamalCiphertext = public
213            .encrypt_with(55_u64, &PedersenOpening::default())
214            .into();
215
216        assert_eq!(expected, added_ciphertext);
217    }
218
219    #[test]
220    fn test_subtract_from() {
221        let amount = 77_u64;
222        let keypair = ElGamalKeypair::new_rand();
223        let public = keypair.pubkey();
224        let open = PedersenOpening::new_rand();
225        let encrypted_amount: PodElGamalCiphertext = public.encrypt_with(amount, &open).into();
226
227        let subtracted_ciphertext = subtract_from(&encrypted_amount, 55).unwrap();
228
229        let expected: PodElGamalCiphertext = public.encrypt_with(22_u64, &open).into();
230
231        assert_eq!(expected, subtracted_ciphertext);
232    }
233
234    #[test]
235    fn test_transfer_arithmetic() {
236        // transfer amount
237        let transfer_amount: u64 = 55;
238        let (amount_lo, amount_hi) = try_split_u64(transfer_amount, 16).unwrap();
239
240        // generate public keys
241        let source_keypair = ElGamalKeypair::new_rand();
242        let source_pubkey = source_keypair.pubkey();
243
244        let destination_keypair = ElGamalKeypair::new_rand();
245        let destination_pubkey = destination_keypair.pubkey();
246
247        let auditor_keypair = ElGamalKeypair::new_rand();
248        let auditor_pubkey = auditor_keypair.pubkey();
249
250        // commitments associated with TransferRangeProof
251        let (commitment_lo, opening_lo) = Pedersen::new(amount_lo);
252        let (commitment_hi, opening_hi) = Pedersen::new(amount_hi);
253
254        let commitment_lo: PodPedersenCommitment = commitment_lo.into();
255        let commitment_hi: PodPedersenCommitment = commitment_hi.into();
256
257        // decryption handles associated with TransferValidityProof
258        let source_handle_lo: PodDecryptHandle = source_pubkey.decrypt_handle(&opening_lo).into();
259        let destination_handle_lo: PodDecryptHandle =
260            destination_pubkey.decrypt_handle(&opening_lo).into();
261        let _auditor_handle_lo: PodDecryptHandle =
262            auditor_pubkey.decrypt_handle(&opening_lo).into();
263
264        let source_handle_hi: PodDecryptHandle = source_pubkey.decrypt_handle(&opening_hi).into();
265        let destination_handle_hi: PodDecryptHandle =
266            destination_pubkey.decrypt_handle(&opening_hi).into();
267        let _auditor_handle_hi: PodDecryptHandle =
268            auditor_pubkey.decrypt_handle(&opening_hi).into();
269
270        // source spendable and recipient pending
271        let source_opening = PedersenOpening::new_rand();
272        let destination_opening = PedersenOpening::new_rand();
273
274        let source_spendable_ciphertext: PodElGamalCiphertext =
275            source_pubkey.encrypt_with(77_u64, &source_opening).into();
276        let destination_pending_ciphertext: PodElGamalCiphertext = destination_pubkey
277            .encrypt_with(77_u64, &destination_opening)
278            .into();
279
280        // program arithmetic for the source account
281        let commitment_lo_point = PodRistrettoPoint(bytes_of(&commitment_lo).try_into().unwrap());
282        let source_handle_lo_point =
283            PodRistrettoPoint(bytes_of(&source_handle_lo).try_into().unwrap());
284
285        let commitment_hi_point = PodRistrettoPoint(bytes_of(&commitment_hi).try_into().unwrap());
286        let source_handle_hi_point =
287            PodRistrettoPoint(bytes_of(&source_handle_hi).try_into().unwrap());
288
289        let source_ciphertext_lo =
290            ristretto_to_elgamal_ciphertext(&commitment_lo_point, &source_handle_lo_point);
291        let source_ciphertext_hi =
292            ristretto_to_elgamal_ciphertext(&commitment_hi_point, &source_handle_hi_point);
293
294        let final_source_spendable = subtract_with_lo_hi(
295            &source_spendable_ciphertext,
296            &source_ciphertext_lo,
297            &source_ciphertext_hi,
298        )
299        .unwrap();
300
301        let final_source_opening =
302            source_opening - (opening_lo.clone() + opening_hi.clone() * Scalar::from(TWO_16));
303        let expected_source: PodElGamalCiphertext = source_pubkey
304            .encrypt_with(22_u64, &final_source_opening)
305            .into();
306        assert_eq!(expected_source, final_source_spendable);
307
308        // program arithmetic for the destination account
309        let destination_handle_lo_point =
310            PodRistrettoPoint(bytes_of(&destination_handle_lo).try_into().unwrap());
311        let destination_handle_hi_point =
312            PodRistrettoPoint(bytes_of(&destination_handle_hi).try_into().unwrap());
313
314        let destination_ciphertext_lo =
315            ristretto_to_elgamal_ciphertext(&commitment_lo_point, &destination_handle_lo_point);
316        let destination_ciphertext_hi =
317            ristretto_to_elgamal_ciphertext(&commitment_hi_point, &destination_handle_hi_point);
318
319        let final_destination_pending_ciphertext = add_with_lo_hi(
320            &destination_pending_ciphertext,
321            &destination_ciphertext_lo,
322            &destination_ciphertext_hi,
323        )
324        .unwrap();
325
326        let final_destination_opening =
327            destination_opening + (opening_lo + opening_hi * Scalar::from(TWO_16));
328        let expected_destination_ciphertext: PodElGamalCiphertext = destination_pubkey
329            .encrypt_with(132_u64, &final_destination_opening)
330            .into();
331        assert_eq!(
332            expected_destination_ciphertext,
333            final_destination_pending_ciphertext
334        );
335    }
336}