1#[cfg(not(target_arch = "wasm32"))]
50use solana_zk_sdk::encryption::grouped_elgamal::GroupedElGamal;
51#[cfg(target_arch = "wasm32")]
52use solana_zk_sdk::encryption::grouped_elgamal::{
53 GroupedElGamalCiphertext2Handles, GroupedElGamalCiphertext3Handles,
54};
55use {
56 crate::{
57 encryption::{FeeCiphertext, TransferAmountCiphertext},
58 errors::TokenProofGenerationError,
59 try_combine_lo_hi_ciphertexts, try_combine_lo_hi_commitments, try_combine_lo_hi_openings,
60 try_split_u64, CiphertextValidityProofWithAuditorCiphertext, TRANSFER_AMOUNT_HI_BITS,
61 TRANSFER_AMOUNT_LO_BITS,
62 },
63 curve25519_dalek::scalar::Scalar,
64 solana_zk_sdk::{
65 encryption::{
66 auth_encryption::{AeCiphertext, AeKey},
67 elgamal::{ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey},
68 pedersen::{Pedersen, PedersenCommitment, PedersenOpening},
69 },
70 zk_elgamal_proof_program::proof_data::{
71 BatchedGroupedCiphertext2HandlesValidityProofData,
72 BatchedGroupedCiphertext3HandlesValidityProofData, BatchedRangeProofU256Data,
73 CiphertextCommitmentEqualityProofData, PercentageWithCapProofData, ZkProofData,
74 },
75 },
76};
77
78const MAX_FEE_BASIS_POINTS_SUB_ONE: u64 = 9_999;
79const MAX_FEE_BASIS_POINTS: u64 = 10_000;
80const ONE_IN_BASIS_POINTS: u128 = MAX_FEE_BASIS_POINTS as u128;
81
82const FEE_AMOUNT_LO_BITS: usize = 16;
83const FEE_AMOUNT_HI_BITS: usize = 32;
84
85const REMAINING_BALANCE_BIT_LENGTH: usize = 64;
86const DELTA_BIT_LENGTH: usize = 16;
87const NET_TRANSFER_AMOUNT_BIT_LENGTH: usize = 64;
88
89pub struct TransferWithFeeProofData {
92 pub equality_proof_data: CiphertextCommitmentEqualityProofData,
93 pub transfer_amount_ciphertext_validity_proof_data_with_ciphertext:
94 CiphertextValidityProofWithAuditorCiphertext,
95 pub percentage_with_cap_proof_data: PercentageWithCapProofData,
96 pub fee_ciphertext_validity_proof_data: BatchedGroupedCiphertext2HandlesValidityProofData,
97 pub range_proof_data: BatchedRangeProofU256Data,
98}
99
100#[allow(clippy::too_many_arguments)]
101pub fn transfer_with_fee_split_proof_data(
102 current_available_balance: &ElGamalCiphertext,
103 current_decryptable_available_balance: &AeCiphertext,
104 transfer_amount: u64,
105 source_elgamal_keypair: &ElGamalKeypair,
106 aes_key: &AeKey,
107 destination_elgamal_pubkey: &ElGamalPubkey,
108 auditor_elgamal_pubkey: Option<&ElGamalPubkey>,
109 withdraw_withheld_authority_elgamal_pubkey: &ElGamalPubkey,
110 fee_rate_basis_points: u16,
111 maximum_fee: u64,
112) -> Result<TransferWithFeeProofData, TokenProofGenerationError> {
113 let default_auditor_pubkey = ElGamalPubkey::default();
114 let auditor_elgamal_pubkey = auditor_elgamal_pubkey.unwrap_or(&default_auditor_pubkey);
115
116 let (transfer_amount_lo, transfer_amount_hi) =
118 try_split_u64(transfer_amount, TRANSFER_AMOUNT_LO_BITS)
119 .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?;
120
121 let (transfer_amount_grouped_ciphertext_lo, transfer_amount_opening_lo) =
123 TransferAmountCiphertext::new(
124 transfer_amount_lo,
125 source_elgamal_keypair.pubkey(),
126 destination_elgamal_pubkey,
127 auditor_elgamal_pubkey,
128 );
129 #[cfg(not(target_arch = "wasm32"))]
130 let grouped_ciphertext_lo = transfer_amount_grouped_ciphertext_lo.0;
131 #[cfg(target_arch = "wasm32")]
132 let grouped_ciphertext_lo = GroupedElGamalCiphertext3Handles::encrypt_with_u64(
133 source_elgamal_keypair.pubkey(),
134 destination_elgamal_pubkey,
135 auditor_elgamal_pubkey,
136 transfer_amount_lo,
137 &transfer_amount_opening_lo,
138 );
139
140 let (transfer_amount_grouped_ciphertext_hi, transfer_amount_opening_hi) =
141 TransferAmountCiphertext::new(
142 transfer_amount_hi,
143 source_elgamal_keypair.pubkey(),
144 destination_elgamal_pubkey,
145 auditor_elgamal_pubkey,
146 );
147 #[cfg(not(target_arch = "wasm32"))]
148 let grouped_ciphertext_hi = transfer_amount_grouped_ciphertext_hi.0;
149 #[cfg(target_arch = "wasm32")]
150 let grouped_ciphertext_hi = GroupedElGamalCiphertext3Handles::encrypt_with_u64(
151 source_elgamal_keypair.pubkey(),
152 destination_elgamal_pubkey,
153 auditor_elgamal_pubkey,
154 transfer_amount_hi,
155 &transfer_amount_opening_hi,
156 );
157
158 let current_decrypted_available_balance = current_decryptable_available_balance
160 .decrypt(aes_key)
161 .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?;
162
163 let new_decrypted_available_balance = current_decrypted_available_balance
165 .checked_sub(transfer_amount)
166 .ok_or(TokenProofGenerationError::NotEnoughFunds)?;
167
168 let (new_available_balance_commitment, new_source_opening) =
170 Pedersen::new(new_decrypted_available_balance);
171
172 let transfer_amount_source_ciphertext_lo = transfer_amount_grouped_ciphertext_lo
174 .0
175 .to_elgamal_ciphertext(0)
176 .unwrap();
177
178 let transfer_amount_source_ciphertext_hi = transfer_amount_grouped_ciphertext_hi
179 .0
180 .to_elgamal_ciphertext(0)
181 .unwrap();
182
183 #[allow(clippy::arithmetic_side_effects)]
184 let new_available_balance_ciphertext = current_available_balance
185 - try_combine_lo_hi_ciphertexts(
186 &transfer_amount_source_ciphertext_lo,
187 &transfer_amount_source_ciphertext_hi,
188 TRANSFER_AMOUNT_LO_BITS,
189 )
190 .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?;
191
192 let equality_proof_data = CiphertextCommitmentEqualityProofData::new(
194 source_elgamal_keypair,
195 &new_available_balance_ciphertext,
196 &new_available_balance_commitment,
197 &new_source_opening,
198 new_decrypted_available_balance,
199 )
200 .map_err(TokenProofGenerationError::from)?;
201
202 let transfer_amount_ciphertext_validity_proof_data =
204 BatchedGroupedCiphertext3HandlesValidityProofData::new(
205 source_elgamal_keypair.pubkey(),
206 destination_elgamal_pubkey,
207 auditor_elgamal_pubkey,
208 &grouped_ciphertext_lo,
209 &grouped_ciphertext_hi,
210 transfer_amount_lo,
211 transfer_amount_hi,
212 &transfer_amount_opening_lo,
213 &transfer_amount_opening_hi,
214 )
215 .map_err(TokenProofGenerationError::from)?;
216
217 let transfer_amount_auditor_ciphertext_lo = transfer_amount_ciphertext_validity_proof_data
218 .context_data()
219 .grouped_ciphertext_lo
220 .try_extract_ciphertext(2)
221 .map_err(|_| TokenProofGenerationError::CiphertextExtraction)?;
222
223 let transfer_amount_auditor_ciphertext_hi = transfer_amount_ciphertext_validity_proof_data
224 .context_data()
225 .grouped_ciphertext_hi
226 .try_extract_ciphertext(2)
227 .map_err(|_| TokenProofGenerationError::CiphertextExtraction)?;
228
229 let transfer_amount_ciphertext_validity_proof_data_with_ciphertext =
230 CiphertextValidityProofWithAuditorCiphertext {
231 proof_data: transfer_amount_ciphertext_validity_proof_data,
232 ciphertext_lo: transfer_amount_auditor_ciphertext_lo,
233 ciphertext_hi: transfer_amount_auditor_ciphertext_hi,
234 };
235
236 let transfer_fee_basis_points = fee_rate_basis_points;
238 let transfer_fee_maximum_fee = maximum_fee;
239 let (raw_fee_amount, raw_delta_fee) = calculate_fee(transfer_amount, transfer_fee_basis_points)
240 .ok_or(TokenProofGenerationError::FeeCalculation)?;
241
242 let (fee_amount, claimed_delta_fee) = if transfer_fee_maximum_fee < raw_fee_amount {
245 (transfer_fee_maximum_fee, 0)
246 } else {
247 (raw_fee_amount, raw_delta_fee)
248 };
249 let net_transfer_amount = transfer_amount
250 .checked_sub(fee_amount)
251 .ok_or(TokenProofGenerationError::FeeCalculation)?;
252
253 let (fee_amount_lo, fee_amount_hi) = try_split_u64(fee_amount, FEE_AMOUNT_LO_BITS)
255 .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?;
256 let (fee_ciphertext_lo, fee_opening_lo) = FeeCiphertext::new(
257 fee_amount_lo,
258 destination_elgamal_pubkey,
259 withdraw_withheld_authority_elgamal_pubkey,
260 );
261 let (fee_ciphertext_hi, fee_opening_hi) = FeeCiphertext::new(
262 fee_amount_hi,
263 destination_elgamal_pubkey,
264 withdraw_withheld_authority_elgamal_pubkey,
265 );
266
267 let combined_transfer_amount_commitment = try_combine_lo_hi_commitments(
269 transfer_amount_grouped_ciphertext_lo.get_commitment(),
270 transfer_amount_grouped_ciphertext_hi.get_commitment(),
271 TRANSFER_AMOUNT_LO_BITS,
272 )
273 .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?;
274 let combined_transfer_amount_opening = try_combine_lo_hi_openings(
275 &transfer_amount_opening_lo,
276 &transfer_amount_opening_hi,
277 TRANSFER_AMOUNT_LO_BITS,
278 )
279 .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?;
280
281 let combined_fee_commitment = try_combine_lo_hi_commitments(
282 fee_ciphertext_lo.get_commitment(),
283 fee_ciphertext_hi.get_commitment(),
284 FEE_AMOUNT_LO_BITS,
285 )
286 .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?;
287 let combined_fee_opening =
288 try_combine_lo_hi_openings(&fee_opening_lo, &fee_opening_hi, FEE_AMOUNT_LO_BITS)
289 .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?;
290
291 #[allow(clippy::arithmetic_side_effects)]
293 let net_transfer_amount_commitment =
294 combined_transfer_amount_commitment - combined_fee_commitment;
295 #[allow(clippy::arithmetic_side_effects)]
296 let net_transfer_amount_opening = &combined_transfer_amount_opening - &combined_fee_opening;
297
298 let (claimed_commitment, claimed_opening) = Pedersen::new(claimed_delta_fee);
300 let (delta_commitment, delta_opening) = compute_delta_commitment_and_opening(
301 (
302 &combined_transfer_amount_commitment,
303 &combined_transfer_amount_opening,
304 ),
305 (&combined_fee_commitment, &combined_fee_opening),
306 transfer_fee_basis_points,
307 );
308
309 let percentage_with_cap_proof_data = PercentageWithCapProofData::new(
311 &combined_fee_commitment,
312 &combined_fee_opening,
313 fee_amount,
314 &delta_commitment,
315 &delta_opening,
316 claimed_delta_fee,
317 &claimed_commitment,
318 &claimed_opening,
319 transfer_fee_maximum_fee,
320 )
321 .map_err(TokenProofGenerationError::from)?;
322
323 #[cfg(not(target_arch = "wasm32"))]
326 let fee_destination_withdraw_withheld_authority_ciphertext_lo = GroupedElGamal::encrypt_with(
327 [
328 destination_elgamal_pubkey,
329 withdraw_withheld_authority_elgamal_pubkey,
330 ],
331 fee_amount_lo,
332 &fee_opening_lo,
333 );
334 #[cfg(target_arch = "wasm32")]
335 let fee_destination_withdraw_withheld_authority_ciphertext_lo =
336 GroupedElGamalCiphertext2Handles::encrypt_with_u64(
337 destination_elgamal_pubkey,
338 withdraw_withheld_authority_elgamal_pubkey,
339 fee_amount_lo,
340 &fee_opening_lo,
341 );
342
343 #[cfg(not(target_arch = "wasm32"))]
344 let fee_destination_withdraw_withheld_authority_ciphertext_hi = GroupedElGamal::encrypt_with(
345 [
346 destination_elgamal_pubkey,
347 withdraw_withheld_authority_elgamal_pubkey,
348 ],
349 fee_amount_hi,
350 &fee_opening_hi,
351 );
352 #[cfg(target_arch = "wasm32")]
353 let fee_destination_withdraw_withheld_authority_ciphertext_hi =
354 GroupedElGamalCiphertext2Handles::encrypt_with_u64(
355 destination_elgamal_pubkey,
356 withdraw_withheld_authority_elgamal_pubkey,
357 fee_amount_hi,
358 &fee_opening_hi,
359 );
360
361 let fee_ciphertext_validity_proof_data =
363 BatchedGroupedCiphertext2HandlesValidityProofData::new(
364 destination_elgamal_pubkey,
365 withdraw_withheld_authority_elgamal_pubkey,
366 &fee_destination_withdraw_withheld_authority_ciphertext_lo,
367 &fee_destination_withdraw_withheld_authority_ciphertext_hi,
368 fee_amount_lo,
369 fee_amount_hi,
370 &fee_opening_lo,
371 &fee_opening_hi,
372 )
373 .map_err(TokenProofGenerationError::from)?;
374
375 let delta_fee_complement = MAX_FEE_BASIS_POINTS_SUB_ONE
377 .checked_sub(claimed_delta_fee)
378 .ok_or(TokenProofGenerationError::FeeCalculation)?;
379
380 let max_fee_basis_points_sub_one_commitment =
381 Pedersen::with(MAX_FEE_BASIS_POINTS_SUB_ONE, &PedersenOpening::default());
382 #[allow(clippy::arithmetic_side_effects)]
383 let claimed_complement_commitment =
384 max_fee_basis_points_sub_one_commitment - claimed_commitment;
385 #[allow(clippy::arithmetic_side_effects)]
386 let claimed_complement_opening = PedersenOpening::default() - &claimed_opening;
387
388 let range_proof_data = BatchedRangeProofU256Data::new(
389 vec![
390 &new_available_balance_commitment,
391 transfer_amount_grouped_ciphertext_lo.get_commitment(),
392 transfer_amount_grouped_ciphertext_hi.get_commitment(),
393 &claimed_commitment,
394 &claimed_complement_commitment,
395 fee_ciphertext_lo.get_commitment(),
396 fee_ciphertext_hi.get_commitment(),
397 &net_transfer_amount_commitment,
398 ],
399 vec![
400 new_decrypted_available_balance,
401 transfer_amount_lo,
402 transfer_amount_hi,
403 claimed_delta_fee,
404 delta_fee_complement,
405 fee_amount_lo,
406 fee_amount_hi,
407 net_transfer_amount,
408 ],
409 vec![
410 REMAINING_BALANCE_BIT_LENGTH,
411 TRANSFER_AMOUNT_LO_BITS,
412 TRANSFER_AMOUNT_HI_BITS,
413 DELTA_BIT_LENGTH,
414 DELTA_BIT_LENGTH,
415 FEE_AMOUNT_LO_BITS,
416 FEE_AMOUNT_HI_BITS,
417 NET_TRANSFER_AMOUNT_BIT_LENGTH,
418 ],
419 vec![
420 &new_source_opening,
421 &transfer_amount_opening_lo,
422 &transfer_amount_opening_hi,
423 &claimed_opening,
424 &claimed_complement_opening,
425 &fee_opening_lo,
426 &fee_opening_hi,
427 &net_transfer_amount_opening,
428 ],
429 )
430 .map_err(TokenProofGenerationError::from)?;
431
432 Ok(TransferWithFeeProofData {
433 equality_proof_data,
434 transfer_amount_ciphertext_validity_proof_data_with_ciphertext,
435 percentage_with_cap_proof_data,
436 fee_ciphertext_validity_proof_data,
437 range_proof_data,
438 })
439}
440
441fn calculate_fee(transfer_amount: u64, fee_rate_basis_points: u16) -> Option<(u64, u64)> {
442 let numerator = (transfer_amount as u128).checked_mul(fee_rate_basis_points as u128)?;
443
444 let fee = numerator
450 .checked_add(ONE_IN_BASIS_POINTS)?
451 .checked_sub(1)?
452 .checked_div(ONE_IN_BASIS_POINTS)?;
453
454 let delta_fee = fee
455 .checked_mul(ONE_IN_BASIS_POINTS)?
456 .checked_sub(numerator)?;
457
458 Some((fee as u64, delta_fee as u64))
459}
460
461#[allow(clippy::arithmetic_side_effects)]
462fn compute_delta_commitment_and_opening(
463 (combined_commitment, combined_opening): (&PedersenCommitment, &PedersenOpening),
464 (combined_fee_commitment, combined_fee_opening): (&PedersenCommitment, &PedersenOpening),
465 fee_rate_basis_points: u16,
466) -> (PedersenCommitment, PedersenOpening) {
467 let fee_rate_scalar = Scalar::from(fee_rate_basis_points);
468 let delta_commitment = combined_fee_commitment * Scalar::from(MAX_FEE_BASIS_POINTS)
469 - combined_commitment * fee_rate_scalar;
470 let delta_opening = combined_fee_opening * Scalar::from(MAX_FEE_BASIS_POINTS)
471 - combined_opening * fee_rate_scalar;
472
473 (delta_commitment, delta_opening)
474}