solana_zk_token_sdk/zk_token_elgamal/
ops.rs

1use {
2    crate::zk_token_elgamal::pod,
3    solana_curve25519::{
4        ristretto::{add_ristretto, multiply_ristretto, subtract_ristretto, PodRistrettoPoint},
5        scalar::PodScalar,
6    },
7};
8
9const SHIFT_BITS: usize = 16;
10
11const G: PodRistrettoPoint = PodRistrettoPoint([
12    226, 242, 174, 10, 106, 188, 78, 113, 168, 132, 169, 97, 197, 0, 81, 95, 88, 227, 11, 106, 165,
13    130, 221, 141, 182, 166, 89, 69, 224, 141, 45, 118,
14]);
15
16/// Add two ElGamal ciphertexts
17pub fn add(
18    left_ciphertext: &pod::ElGamalCiphertext,
19    right_ciphertext: &pod::ElGamalCiphertext,
20) -> Option<pod::ElGamalCiphertext> {
21    let (left_commitment, left_handle): (pod::PedersenCommitment, pod::DecryptHandle) =
22        (*left_ciphertext).into();
23    let (right_commitment, right_handle): (pod::PedersenCommitment, pod::DecryptHandle) =
24        (*right_ciphertext).into();
25
26    let result_commitment: pod::PedersenCommitment =
27        add_ristretto(&left_commitment.into(), &right_commitment.into())?.into();
28    let result_handle: pod::DecryptHandle =
29        add_ristretto(&left_handle.into(), &right_handle.into())?.into();
30
31    Some((result_commitment, result_handle).into())
32}
33
34/// Multiply an ElGamal ciphertext by a scalar
35pub fn multiply(
36    scalar: &PodScalar,
37    ciphertext: &pod::ElGamalCiphertext,
38) -> Option<pod::ElGamalCiphertext> {
39    let (commitment, handle): (pod::PedersenCommitment, pod::DecryptHandle) = (*ciphertext).into();
40
41    let commitment_point: PodRistrettoPoint = commitment.into();
42    let handle_point: PodRistrettoPoint = handle.into();
43
44    let result_commitment: pod::PedersenCommitment =
45        multiply_ristretto(scalar, &commitment_point)?.into();
46    let result_handle: pod::DecryptHandle = multiply_ristretto(scalar, &handle_point)?.into();
47
48    Some((result_commitment, result_handle).into())
49}
50
51/// Compute `left_ciphertext + (right_ciphertext_lo + 2^16 * right_ciphertext_hi)`
52pub fn add_with_lo_hi(
53    left_ciphertext: &pod::ElGamalCiphertext,
54    right_ciphertext_lo: &pod::ElGamalCiphertext,
55    right_ciphertext_hi: &pod::ElGamalCiphertext,
56) -> Option<pod::ElGamalCiphertext> {
57    let shift_scalar = to_scalar(1_u64 << SHIFT_BITS);
58    let shifted_right_ciphertext_hi = multiply(&shift_scalar, right_ciphertext_hi)?;
59    let combined_right_ciphertext = add(right_ciphertext_lo, &shifted_right_ciphertext_hi)?;
60    add(left_ciphertext, &combined_right_ciphertext)
61}
62
63/// Subtract two ElGamal ciphertexts
64pub fn subtract(
65    left_ciphertext: &pod::ElGamalCiphertext,
66    right_ciphertext: &pod::ElGamalCiphertext,
67) -> Option<pod::ElGamalCiphertext> {
68    let (left_commitment, left_handle): (pod::PedersenCommitment, pod::DecryptHandle) =
69        (*left_ciphertext).into();
70    let (right_commitment, right_handle): (pod::PedersenCommitment, pod::DecryptHandle) =
71        (*right_ciphertext).into();
72
73    let result_commitment: pod::PedersenCommitment =
74        subtract_ristretto(&left_commitment.into(), &right_commitment.into())?.into();
75    let result_handle: pod::DecryptHandle =
76        subtract_ristretto(&left_handle.into(), &right_handle.into())?.into();
77
78    Some((result_commitment, result_handle).into())
79}
80
81/// Compute `left_ciphertext - (right_ciphertext_lo + 2^16 * right_ciphertext_hi)`
82pub fn subtract_with_lo_hi(
83    left_ciphertext: &pod::ElGamalCiphertext,
84    right_ciphertext_lo: &pod::ElGamalCiphertext,
85    right_ciphertext_hi: &pod::ElGamalCiphertext,
86) -> Option<pod::ElGamalCiphertext> {
87    let shift_scalar = to_scalar(1_u64 << SHIFT_BITS);
88    let shifted_right_ciphertext_hi = multiply(&shift_scalar, right_ciphertext_hi)?;
89    let combined_right_ciphertext = add(right_ciphertext_lo, &shifted_right_ciphertext_hi)?;
90    subtract(left_ciphertext, &combined_right_ciphertext)
91}
92
93/// Add a constant amount to a ciphertext
94pub fn add_to(ciphertext: &pod::ElGamalCiphertext, amount: u64) -> Option<pod::ElGamalCiphertext> {
95    let amount_scalar = to_scalar(amount);
96    let amount_point = multiply_ristretto(&amount_scalar, &G)?;
97
98    let (commitment, handle): (pod::PedersenCommitment, pod::DecryptHandle) = (*ciphertext).into();
99    let commitment_point: PodRistrettoPoint = commitment.into();
100
101    let result_commitment: pod::PedersenCommitment =
102        add_ristretto(&commitment_point, &amount_point)?.into();
103    Some((result_commitment, handle).into())
104}
105
106/// Subtract a constant amount to a ciphertext
107pub fn subtract_from(
108    ciphertext: &pod::ElGamalCiphertext,
109    amount: u64,
110) -> Option<pod::ElGamalCiphertext> {
111    let amount_scalar = to_scalar(amount);
112    let amount_point = multiply_ristretto(&amount_scalar, &G)?;
113
114    let (commitment, handle): (pod::PedersenCommitment, pod::DecryptHandle) = (*ciphertext).into();
115    let commitment_point: PodRistrettoPoint = commitment.into();
116
117    let result_commitment: pod::PedersenCommitment =
118        subtract_ristretto(&commitment_point, &amount_point)?.into();
119    Some((result_commitment, handle).into())
120}
121
122/// Convert a `u64` amount into a curve25519 scalar
123fn to_scalar(amount: u64) -> PodScalar {
124    let mut bytes = [0u8; 32];
125    bytes[..8].copy_from_slice(&amount.to_le_bytes());
126    PodScalar(bytes)
127}
128
129#[cfg(test)]
130mod tests {
131    use {
132        crate::{
133            encryption::{
134                elgamal::{ElGamalCiphertext, ElGamalKeypair},
135                pedersen::{Pedersen, PedersenOpening},
136            },
137            instruction::transfer::try_split_u64,
138            zk_token_elgamal::{ops, pod},
139        },
140        bytemuck::Zeroable,
141        curve25519_dalek::scalar::Scalar,
142        std::convert::TryInto,
143    };
144
145    const TWO_16: u64 = 65536;
146
147    #[test]
148    fn test_zero_ct() {
149        let spendable_balance = pod::ElGamalCiphertext::zeroed();
150        let spendable_ct: ElGamalCiphertext = spendable_balance.try_into().unwrap();
151
152        // spendable_ct should be an encryption of 0 for any public key when
153        // `PedersenOpen::default()` is used
154        let keypair = ElGamalKeypair::new_rand();
155        let public = keypair.pubkey();
156        let balance: u64 = 0;
157        assert_eq!(
158            spendable_ct,
159            public.encrypt_with(balance, &PedersenOpening::default())
160        );
161
162        // homomorphism should work like any other ciphertext
163        let open = PedersenOpening::new_rand();
164        let transfer_amount_ct = public.encrypt_with(55_u64, &open);
165        let transfer_amount_pod: pod::ElGamalCiphertext = transfer_amount_ct.into();
166
167        let sum = ops::add(&spendable_balance, &transfer_amount_pod).unwrap();
168
169        let expected: pod::ElGamalCiphertext = public.encrypt_with(55_u64, &open).into();
170        assert_eq!(expected, sum);
171    }
172
173    #[test]
174    fn test_add_to() {
175        let spendable_balance = pod::ElGamalCiphertext::zeroed();
176
177        let added_ct = ops::add_to(&spendable_balance, 55).unwrap();
178
179        let keypair = ElGamalKeypair::new_rand();
180        let public = keypair.pubkey();
181        let expected: pod::ElGamalCiphertext = public
182            .encrypt_with(55_u64, &PedersenOpening::default())
183            .into();
184
185        assert_eq!(expected, added_ct);
186    }
187
188    #[test]
189    fn test_subtract_from() {
190        let amount = 77_u64;
191        let keypair = ElGamalKeypair::new_rand();
192        let public = keypair.pubkey();
193        let open = PedersenOpening::new_rand();
194        let encrypted_amount: pod::ElGamalCiphertext = public.encrypt_with(amount, &open).into();
195
196        let subtracted_ct = ops::subtract_from(&encrypted_amount, 55).unwrap();
197
198        let expected: pod::ElGamalCiphertext = public.encrypt_with(22_u64, &open).into();
199
200        assert_eq!(expected, subtracted_ct);
201    }
202
203    #[test]
204    fn test_transfer_arithmetic() {
205        // transfer amount
206        let transfer_amount: u64 = 55;
207        let (amount_lo, amount_hi) = try_split_u64(transfer_amount, 16).unwrap();
208
209        // generate public keys
210        let source_keypair = ElGamalKeypair::new_rand();
211        let source_pk = source_keypair.pubkey();
212
213        let dest_keypair = ElGamalKeypair::new_rand();
214        let dest_pk = dest_keypair.pubkey();
215
216        let auditor_keypair = ElGamalKeypair::new_rand();
217        let auditor_pk = auditor_keypair.pubkey();
218
219        // commitments associated with TransferRangeProof
220        let (comm_lo, open_lo) = Pedersen::new(amount_lo);
221        let (comm_hi, open_hi) = Pedersen::new(amount_hi);
222
223        let comm_lo: pod::PedersenCommitment = comm_lo.into();
224        let comm_hi: pod::PedersenCommitment = comm_hi.into();
225
226        // decryption handles associated with TransferValidityProof
227        let handle_source_lo: pod::DecryptHandle = source_pk.decrypt_handle(&open_lo).into();
228        let handle_dest_lo: pod::DecryptHandle = dest_pk.decrypt_handle(&open_lo).into();
229        let _handle_auditor_lo: pod::DecryptHandle = auditor_pk.decrypt_handle(&open_lo).into();
230
231        let handle_source_hi: pod::DecryptHandle = source_pk.decrypt_handle(&open_hi).into();
232        let handle_dest_hi: pod::DecryptHandle = dest_pk.decrypt_handle(&open_hi).into();
233        let _handle_auditor_hi: pod::DecryptHandle = auditor_pk.decrypt_handle(&open_hi).into();
234
235        // source spendable and recipient pending
236        let source_open = PedersenOpening::new_rand();
237        let dest_open = PedersenOpening::new_rand();
238
239        let source_spendable_ct: pod::ElGamalCiphertext =
240            source_pk.encrypt_with(77_u64, &source_open).into();
241        let dest_pending_ct: pod::ElGamalCiphertext =
242            dest_pk.encrypt_with(77_u64, &dest_open).into();
243
244        // program arithmetic for the source account
245        let source_lo_ct: pod::ElGamalCiphertext = (comm_lo, handle_source_lo).into();
246        let source_hi_ct: pod::ElGamalCiphertext = (comm_hi, handle_source_hi).into();
247
248        let final_source_spendable =
249            ops::subtract_with_lo_hi(&source_spendable_ct, &source_lo_ct, &source_hi_ct).unwrap();
250
251        let final_source_open =
252            source_open - (open_lo.clone() + open_hi.clone() * Scalar::from(TWO_16));
253        let expected_source: pod::ElGamalCiphertext =
254            source_pk.encrypt_with(22_u64, &final_source_open).into();
255        assert_eq!(expected_source, final_source_spendable);
256
257        // program arithmetic for the destination account
258        let dest_lo_ct: pod::ElGamalCiphertext = (comm_lo, handle_dest_lo).into();
259        let dest_hi_ct: pod::ElGamalCiphertext = (comm_hi, handle_dest_hi).into();
260
261        let final_dest_pending =
262            ops::add_with_lo_hi(&dest_pending_ct, &dest_lo_ct, &dest_hi_ct).unwrap();
263
264        let final_dest_open = dest_open + (open_lo + open_hi * Scalar::from(TWO_16));
265        let expected_dest_ct: pod::ElGamalCiphertext =
266            dest_pk.encrypt_with(132_u64, &final_dest_open).into();
267        assert_eq!(expected_dest_ct, final_dest_pending);
268    }
269}