solana_zk_token_sdk/zk_token_elgamal/
ops.rs1use {
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
16pub 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
34pub 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
51pub 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
63pub 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
81pub 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
93pub 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
106pub 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
122fn 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 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 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 let transfer_amount: u64 = 55;
207 let (amount_lo, amount_hi) = try_split_u64(transfer_amount, 16).unwrap();
208
209 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 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 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 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 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 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}