1use {
2 crate::zk_token_elgamal::pod,
3 bytemuck::{Pod, Zeroable},
4};
5#[cfg(not(target_os = "solana"))]
6use {
7 crate::{
8 encryption::{
9 elgamal::{
10 DecryptHandle, ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey, ElGamalSecretKey,
11 },
12 pedersen::{Pedersen, PedersenCommitment, PedersenOpening},
13 },
14 errors::ProofError,
15 instruction::{
16 combine_lo_hi_ciphertexts, combine_lo_hi_commitments, combine_lo_hi_openings,
17 combine_lo_hi_u64, split_u64, transfer::TransferAmountEncryption, Role, Verifiable,
18 },
19 range_proof::RangeProof,
20 sigma_proofs::{
21 equality_proof::CtxtCommEqualityProof, fee_proof::FeeSigmaProof,
22 validity_proof::AggregatedValidityProof,
23 },
24 transcript::TranscriptProtocol,
25 },
26 arrayref::{array_ref, array_refs},
27 curve25519_dalek::scalar::Scalar,
28 merlin::Transcript,
29 std::convert::TryInto,
30 subtle::{ConditionallySelectable, ConstantTimeGreater},
31};
32
33#[cfg(not(target_os = "solana"))]
34const MAX_FEE_BASIS_POINTS: u64 = 10_000;
35#[cfg(not(target_os = "solana"))]
36const ONE_IN_BASIS_POINTS: u128 = MAX_FEE_BASIS_POINTS as u128;
37
38#[cfg(not(target_os = "solana"))]
39const TRANSFER_SOURCE_AMOUNT_BITS: usize = 64;
40#[cfg(not(target_os = "solana"))]
41const TRANSFER_AMOUNT_LO_BITS: usize = 16;
42#[cfg(not(target_os = "solana"))]
43const TRANSFER_AMOUNT_LO_NEGATED_BITS: usize = 16;
44#[cfg(not(target_os = "solana"))]
45const TRANSFER_AMOUNT_HI_BITS: usize = 32;
46#[cfg(not(target_os = "solana"))]
47const TRANSFER_DELTA_BITS: usize = 48;
48#[cfg(not(target_os = "solana"))]
49const FEE_AMOUNT_LO_BITS: usize = 16;
50#[cfg(not(target_os = "solana"))]
51const FEE_AMOUNT_HI_BITS: usize = 32;
52
53#[cfg(not(target_os = "solana"))]
54lazy_static::lazy_static! {
55 pub static ref COMMITMENT_MAX: PedersenCommitment = Pedersen::encode((1_u64 <<
56 TRANSFER_AMOUNT_LO_NEGATED_BITS) - 1);
57 pub static ref COMMITMENT_MAX_FEE_BASIS_POINTS: PedersenCommitment = Pedersen::encode(MAX_FEE_BASIS_POINTS);
58}
59
60#[derive(Clone, Copy, Pod, Zeroable)]
62#[repr(C)]
63pub struct TransferWithFeeData {
64 pub ciphertext_lo: pod::TransferAmountEncryption,
66
67 pub ciphertext_hi: pod::TransferAmountEncryption,
69
70 pub transfer_with_fee_pubkeys: pod::TransferWithFeePubkeys,
72
73 pub new_source_ciphertext: pod::ElGamalCiphertext,
75
76 pub fee_ciphertext_lo: pod::FeeEncryption,
78
79 pub fee_ciphertext_hi: pod::FeeEncryption,
81
82 pub fee_parameters: pod::FeeParameters,
84
85 pub proof: TransferWithFeeProof,
87}
88
89#[cfg(not(target_os = "solana"))]
90impl TransferWithFeeData {
91 pub fn new(
92 transfer_amount: u64,
93 (spendable_balance, old_source_ciphertext): (u64, &ElGamalCiphertext),
94 source_keypair: &ElGamalKeypair,
95 (destination_pubkey, auditor_pubkey): (&ElGamalPubkey, &ElGamalPubkey),
96 fee_parameters: FeeParameters,
97 withdraw_withheld_authority_pubkey: &ElGamalPubkey,
98 ) -> Result<Self, ProofError> {
99 let (amount_lo, amount_hi) = split_u64(transfer_amount, TRANSFER_AMOUNT_LO_BITS);
101
102 let (ciphertext_lo, opening_lo) = TransferAmountEncryption::new(
103 amount_lo,
104 &source_keypair.public,
105 destination_pubkey,
106 auditor_pubkey,
107 );
108 let (ciphertext_hi, opening_hi) = TransferAmountEncryption::new(
109 amount_hi,
110 &source_keypair.public,
111 destination_pubkey,
112 auditor_pubkey,
113 );
114
115 let new_spendable_balance = spendable_balance
117 .checked_sub(transfer_amount)
118 .ok_or(ProofError::Generation)?;
119
120 let transfer_amount_lo_source = ElGamalCiphertext {
121 commitment: ciphertext_lo.commitment,
122 handle: ciphertext_lo.source_handle,
123 };
124
125 let transfer_amount_hi_source = ElGamalCiphertext {
126 commitment: ciphertext_hi.commitment,
127 handle: ciphertext_hi.source_handle,
128 };
129
130 let new_source_ciphertext = old_source_ciphertext
131 - combine_lo_hi_ciphertexts(
132 &transfer_amount_lo_source,
133 &transfer_amount_hi_source,
134 TRANSFER_AMOUNT_LO_BITS,
135 );
136
137 let (fee_amount, delta_fee) =
141 calculate_fee(transfer_amount, fee_parameters.fee_rate_basis_points)
142 .ok_or(ProofError::Generation)?;
143
144 let below_max = u64::ct_gt(&fee_parameters.maximum_fee, &fee_amount);
145 let fee_to_encrypt =
146 u64::conditional_select(&fee_parameters.maximum_fee, &fee_amount, below_max);
147
148 let (fee_to_encrypt_lo, fee_to_encrypt_hi) = split_u64(fee_to_encrypt, FEE_AMOUNT_LO_BITS);
150
151 let (fee_ciphertext_lo, opening_fee_lo) = FeeEncryption::new(
152 fee_to_encrypt_lo,
153 destination_pubkey,
154 withdraw_withheld_authority_pubkey,
155 );
156
157 let (fee_ciphertext_hi, opening_fee_hi) = FeeEncryption::new(
158 fee_to_encrypt_hi,
159 destination_pubkey,
160 withdraw_withheld_authority_pubkey,
161 );
162
163 let pod_transfer_with_fee_pubkeys = pod::TransferWithFeePubkeys {
165 source_pubkey: source_keypair.public.into(),
166 destination_pubkey: (*destination_pubkey).into(),
167 auditor_pubkey: (*auditor_pubkey).into(),
168 withdraw_withheld_authority_pubkey: (*withdraw_withheld_authority_pubkey).into(),
169 };
170 let pod_ciphertext_lo: pod::TransferAmountEncryption = ciphertext_lo.to_pod();
171 let pod_ciphertext_hi: pod::TransferAmountEncryption = ciphertext_hi.to_pod();
172 let pod_new_source_ciphertext: pod::ElGamalCiphertext = new_source_ciphertext.into();
173 let pod_fee_ciphertext_lo: pod::FeeEncryption = fee_ciphertext_lo.to_pod();
174 let pod_fee_ciphertext_hi: pod::FeeEncryption = fee_ciphertext_hi.to_pod();
175
176 let mut transcript = TransferWithFeeProof::transcript_new(
177 &pod_transfer_with_fee_pubkeys,
178 &pod_ciphertext_lo,
179 &pod_ciphertext_hi,
180 &pod_new_source_ciphertext,
181 &pod_fee_ciphertext_lo,
182 &pod_fee_ciphertext_hi,
183 );
184
185 let proof = TransferWithFeeProof::new(
186 (amount_lo, &ciphertext_lo, &opening_lo),
187 (amount_hi, &ciphertext_hi, &opening_hi),
188 source_keypair,
189 (destination_pubkey, auditor_pubkey),
190 (new_spendable_balance, &new_source_ciphertext),
191 (fee_to_encrypt_lo, &fee_ciphertext_lo, &opening_fee_lo),
192 (fee_to_encrypt_hi, &fee_ciphertext_hi, &opening_fee_hi),
193 delta_fee,
194 withdraw_withheld_authority_pubkey,
195 fee_parameters,
196 &mut transcript,
197 );
198
199 Ok(Self {
200 ciphertext_lo: pod_ciphertext_lo,
201 ciphertext_hi: pod_ciphertext_hi,
202 transfer_with_fee_pubkeys: pod_transfer_with_fee_pubkeys,
203 new_source_ciphertext: pod_new_source_ciphertext,
204 fee_ciphertext_lo: pod_fee_ciphertext_lo,
205 fee_ciphertext_hi: pod_fee_ciphertext_hi,
206 fee_parameters: fee_parameters.into(),
207 proof,
208 })
209 }
210
211 fn ciphertext_lo(&self, role: Role) -> Result<ElGamalCiphertext, ProofError> {
213 let ciphertext_lo: TransferAmountEncryption = self.ciphertext_lo.try_into()?;
214
215 let handle_lo = match role {
216 Role::Source => Some(ciphertext_lo.source_handle),
217 Role::Destination => Some(ciphertext_lo.destination_handle),
218 Role::Auditor => Some(ciphertext_lo.auditor_handle),
219 Role::WithdrawWithheldAuthority => None,
220 };
221
222 if let Some(handle) = handle_lo {
223 Ok(ElGamalCiphertext {
224 commitment: ciphertext_lo.commitment,
225 handle,
226 })
227 } else {
228 Err(ProofError::MissingCiphertext)
229 }
230 }
231
232 fn ciphertext_hi(&self, role: Role) -> Result<ElGamalCiphertext, ProofError> {
234 let ciphertext_hi: TransferAmountEncryption = self.ciphertext_hi.try_into()?;
235
236 let handle_hi = match role {
237 Role::Source => Some(ciphertext_hi.source_handle),
238 Role::Destination => Some(ciphertext_hi.destination_handle),
239 Role::Auditor => Some(ciphertext_hi.auditor_handle),
240 Role::WithdrawWithheldAuthority => None,
241 };
242
243 if let Some(handle) = handle_hi {
244 Ok(ElGamalCiphertext {
245 commitment: ciphertext_hi.commitment,
246 handle,
247 })
248 } else {
249 Err(ProofError::MissingCiphertext)
250 }
251 }
252
253 fn fee_ciphertext_lo(&self, role: Role) -> Result<ElGamalCiphertext, ProofError> {
255 let fee_ciphertext_lo: FeeEncryption = self.fee_ciphertext_lo.try_into()?;
256
257 let fee_handle_lo = match role {
258 Role::Source => None,
259 Role::Destination => Some(fee_ciphertext_lo.destination_handle),
260 Role::Auditor => None,
261 Role::WithdrawWithheldAuthority => {
262 Some(fee_ciphertext_lo.withdraw_withheld_authority_handle)
263 }
264 };
265
266 if let Some(handle) = fee_handle_lo {
267 Ok(ElGamalCiphertext {
268 commitment: fee_ciphertext_lo.commitment,
269 handle,
270 })
271 } else {
272 Err(ProofError::MissingCiphertext)
273 }
274 }
275
276 fn fee_ciphertext_hi(&self, role: Role) -> Result<ElGamalCiphertext, ProofError> {
278 let fee_ciphertext_hi: FeeEncryption = self.fee_ciphertext_hi.try_into()?;
279
280 let fee_handle_hi = match role {
281 Role::Source => None,
282 Role::Destination => Some(fee_ciphertext_hi.destination_handle),
283 Role::Auditor => None,
284 Role::WithdrawWithheldAuthority => {
285 Some(fee_ciphertext_hi.withdraw_withheld_authority_handle)
286 }
287 };
288
289 if let Some(handle) = fee_handle_hi {
290 Ok(ElGamalCiphertext {
291 commitment: fee_ciphertext_hi.commitment,
292 handle,
293 })
294 } else {
295 Err(ProofError::MissingCiphertext)
296 }
297 }
298
299 pub fn decrypt_amount(&self, role: Role, sk: &ElGamalSecretKey) -> Result<u64, ProofError> {
301 let ciphertext_lo = self.ciphertext_lo(role)?;
302 let ciphertext_hi = self.ciphertext_hi(role)?;
303
304 let amount_lo = ciphertext_lo.decrypt_u32(sk);
305 let amount_hi = ciphertext_hi.decrypt_u32(sk);
306
307 if let (Some(amount_lo), Some(amount_hi)) = (amount_lo, amount_hi) {
308 let shifted_amount_hi = amount_hi << TRANSFER_AMOUNT_LO_BITS;
309 Ok(amount_lo + shifted_amount_hi)
310 } else {
311 Err(ProofError::Decryption)
312 }
313 }
314
315 pub fn decrypt_fee_amount(&self, role: Role, sk: &ElGamalSecretKey) -> Result<u64, ProofError> {
317 let ciphertext_lo = self.fee_ciphertext_lo(role)?;
318 let ciphertext_hi = self.fee_ciphertext_hi(role)?;
319
320 let fee_amount_lo = ciphertext_lo.decrypt_u32(sk);
321 let fee_amount_hi = ciphertext_hi.decrypt_u32(sk);
322
323 if let (Some(fee_amount_lo), Some(fee_amount_hi)) = (fee_amount_lo, fee_amount_hi) {
324 let shifted_fee_amount_hi = fee_amount_hi << FEE_AMOUNT_LO_BITS;
325 Ok(fee_amount_lo + shifted_fee_amount_hi)
326 } else {
327 Err(ProofError::Decryption)
328 }
329 }
330}
331
332#[cfg(not(target_os = "solana"))]
333impl Verifiable for TransferWithFeeData {
334 fn verify(&self) -> Result<(), ProofError> {
335 let mut transcript = TransferWithFeeProof::transcript_new(
336 &self.transfer_with_fee_pubkeys,
337 &self.ciphertext_lo,
338 &self.ciphertext_hi,
339 &self.new_source_ciphertext,
340 &self.fee_ciphertext_lo,
341 &self.fee_ciphertext_hi,
342 );
343
344 let ciphertext_lo = self.ciphertext_lo.try_into()?;
345 let ciphertext_hi = self.ciphertext_hi.try_into()?;
346 let pubkeys_transfer_with_fee = self.transfer_with_fee_pubkeys.try_into()?;
347 let new_source_ciphertext = self.new_source_ciphertext.try_into()?;
348
349 let fee_ciphertext_lo = self.fee_ciphertext_lo.try_into()?;
350 let fee_ciphertext_hi = self.fee_ciphertext_hi.try_into()?;
351 let fee_parameters = self.fee_parameters.into();
352
353 self.proof.verify(
354 &ciphertext_lo,
355 &ciphertext_hi,
356 &pubkeys_transfer_with_fee,
357 &new_source_ciphertext,
358 &fee_ciphertext_lo,
359 &fee_ciphertext_hi,
360 fee_parameters,
361 &mut transcript,
362 )
363 }
364}
365
366#[repr(C)]
368#[derive(Clone, Copy, Pod, Zeroable)]
369pub struct TransferWithFeeProof {
370 pub new_source_commitment: pod::PedersenCommitment,
371 pub claimed_commitment: pod::PedersenCommitment,
372 pub equality_proof: pod::CtxtCommEqualityProof,
373 pub ciphertext_amount_validity_proof: pod::AggregatedValidityProof,
374 pub fee_sigma_proof: pod::FeeSigmaProof,
375 pub fee_ciphertext_validity_proof: pod::AggregatedValidityProof,
376 pub range_proof: pod::RangeProof256,
377}
378
379#[allow(non_snake_case)]
380#[cfg(not(target_os = "solana"))]
381impl TransferWithFeeProof {
382 fn transcript_new(
383 transfer_with_fee_pubkeys: &pod::TransferWithFeePubkeys,
384 ciphertext_lo: &pod::TransferAmountEncryption,
385 ciphertext_hi: &pod::TransferAmountEncryption,
386 new_source_ciphertext: &pod::ElGamalCiphertext,
387 fee_ciphertext_lo: &pod::FeeEncryption,
388 fee_ciphertext_hi: &pod::FeeEncryption,
389 ) -> Transcript {
390 let mut transcript = Transcript::new(b"FeeProof");
391
392 transcript.append_pubkey(b"pubkey-source", &transfer_with_fee_pubkeys.source_pubkey);
393 transcript.append_pubkey(
394 b"pubkey-dest",
395 &transfer_with_fee_pubkeys.destination_pubkey,
396 );
397 transcript.append_pubkey(b"pubkey-auditor", &transfer_with_fee_pubkeys.auditor_pubkey);
398 transcript.append_pubkey(
399 b"withdraw_withheld_authority_pubkey",
400 &transfer_with_fee_pubkeys.withdraw_withheld_authority_pubkey,
401 );
402
403 transcript.append_commitment(b"comm-lo-amount", &ciphertext_lo.commitment);
404 transcript.append_handle(b"handle-lo-source", &ciphertext_lo.source_handle);
405 transcript.append_handle(b"handle-lo-dest", &ciphertext_lo.destination_handle);
406 transcript.append_handle(b"handle-lo-auditor", &ciphertext_lo.auditor_handle);
407
408 transcript.append_commitment(b"comm-hi-amount", &ciphertext_hi.commitment);
409 transcript.append_handle(b"handle-hi-source", &ciphertext_hi.source_handle);
410 transcript.append_handle(b"handle-hi-dest", &ciphertext_hi.destination_handle);
411 transcript.append_handle(b"handle-hi-auditor", &ciphertext_hi.auditor_handle);
412
413 transcript.append_ciphertext(b"ctxt-new-source", new_source_ciphertext);
414
415 transcript.append_commitment(b"comm-fee-lo", &fee_ciphertext_lo.commitment);
416 transcript.append_handle(b"handle-fee-lo-dest", &fee_ciphertext_lo.destination_handle);
417 transcript.append_handle(
418 b"handle-fee-lo-auditor",
419 &fee_ciphertext_lo.withdraw_withheld_authority_handle,
420 );
421
422 transcript.append_commitment(b"comm-fee-hi", &fee_ciphertext_hi.commitment);
423 transcript.append_handle(b"handle-fee-hi-dest", &fee_ciphertext_hi.destination_handle);
424 transcript.append_handle(
425 b"handle-fee-hi-auditor",
426 &fee_ciphertext_hi.withdraw_withheld_authority_handle,
427 );
428
429 transcript
430 }
431
432 #[allow(clippy::too_many_arguments)]
433 #[allow(clippy::many_single_char_names)]
434 pub fn new(
435 transfer_amount_lo_data: (u64, &TransferAmountEncryption, &PedersenOpening),
436 transfer_amount_hi_data: (u64, &TransferAmountEncryption, &PedersenOpening),
437 source_keypair: &ElGamalKeypair,
438 (destination_pubkey, auditor_pubkey): (&ElGamalPubkey, &ElGamalPubkey),
439 (source_new_balance, new_source_ciphertext): (u64, &ElGamalCiphertext),
440 (fee_amount_lo, fee_ciphertext_lo, opening_fee_lo): (u64, &FeeEncryption, &PedersenOpening),
442 (fee_amount_hi, fee_ciphertext_hi, opening_fee_hi): (u64, &FeeEncryption, &PedersenOpening),
443 delta_fee: u64,
444 withdraw_withheld_authority_pubkey: &ElGamalPubkey,
445 fee_parameters: FeeParameters,
446 transcript: &mut Transcript,
447 ) -> Self {
448 let (transfer_amount_lo, ciphertext_lo, opening_lo) = transfer_amount_lo_data;
449 let (transfer_amount_hi, ciphertext_hi, opening_hi) = transfer_amount_hi_data;
450
451 let (new_source_commitment, opening_source) = Pedersen::new(source_new_balance);
453 let pod_new_source_commitment: pod::PedersenCommitment = new_source_commitment.into();
454
455 transcript.append_commitment(b"commitment-new-source", &pod_new_source_commitment);
456
457 let equality_proof = CtxtCommEqualityProof::new(
459 source_keypair,
460 new_source_ciphertext,
461 source_new_balance,
462 &opening_source,
463 transcript,
464 );
465
466 let ciphertext_amount_validity_proof = AggregatedValidityProof::new(
468 (destination_pubkey, auditor_pubkey),
469 (transfer_amount_lo, transfer_amount_hi),
470 (opening_lo, opening_hi),
471 transcript,
472 );
473
474 let (claimed_commitment, opening_claimed) = Pedersen::new(delta_fee);
476 let pod_claimed_commitment: pod::PedersenCommitment = claimed_commitment.into();
477 transcript.append_commitment(b"commitment-claimed", &pod_claimed_commitment);
478
479 let combined_commitment = combine_lo_hi_commitments(
480 &ciphertext_lo.commitment,
481 &ciphertext_hi.commitment,
482 TRANSFER_AMOUNT_LO_BITS,
483 );
484 let combined_opening =
485 combine_lo_hi_openings(opening_lo, opening_hi, TRANSFER_AMOUNT_LO_BITS);
486
487 let combined_fee_amount =
488 combine_lo_hi_u64(fee_amount_lo, fee_amount_hi, TRANSFER_AMOUNT_LO_BITS);
489 let combined_fee_commitment = combine_lo_hi_commitments(
490 &fee_ciphertext_lo.commitment,
491 &fee_ciphertext_hi.commitment,
492 TRANSFER_AMOUNT_LO_BITS,
493 );
494 let combined_fee_opening =
495 combine_lo_hi_openings(opening_fee_lo, opening_fee_hi, TRANSFER_AMOUNT_LO_BITS);
496
497 let (delta_commitment, opening_delta) = compute_delta_commitment_and_opening(
499 (&combined_commitment, &combined_opening),
500 (&combined_fee_commitment, &combined_fee_opening),
501 fee_parameters.fee_rate_basis_points,
502 );
503 let pod_delta_commitment: pod::PedersenCommitment = delta_commitment.into();
504 transcript.append_commitment(b"commitment-delta", &pod_delta_commitment);
505
506 let fee_sigma_proof = FeeSigmaProof::new(
508 (
509 combined_fee_amount,
510 &combined_fee_commitment,
511 &combined_fee_opening,
512 ),
513 (delta_fee, &delta_commitment, &opening_delta),
514 (&claimed_commitment, &opening_claimed),
515 fee_parameters.maximum_fee,
516 transcript,
517 );
518
519 let fee_ciphertext_validity_proof = AggregatedValidityProof::new(
521 (destination_pubkey, withdraw_withheld_authority_pubkey),
522 (fee_amount_lo, fee_amount_hi),
523 (opening_fee_lo, opening_fee_hi),
524 transcript,
525 );
526
527 let opening_claimed_negated = &PedersenOpening::default() - &opening_claimed;
529 let range_proof = RangeProof::new(
530 vec![
531 source_new_balance,
532 transfer_amount_lo,
533 transfer_amount_hi,
534 delta_fee,
535 MAX_FEE_BASIS_POINTS - delta_fee,
536 fee_amount_lo,
537 fee_amount_hi,
538 ],
539 vec![
540 TRANSFER_SOURCE_AMOUNT_BITS, TRANSFER_AMOUNT_LO_BITS, TRANSFER_AMOUNT_HI_BITS, TRANSFER_DELTA_BITS, TRANSFER_DELTA_BITS, FEE_AMOUNT_LO_BITS, FEE_AMOUNT_HI_BITS, ],
548 vec![
549 &opening_source,
550 opening_lo,
551 opening_hi,
552 &opening_claimed,
553 &opening_claimed_negated,
554 opening_fee_lo,
555 opening_fee_hi,
556 ],
557 transcript,
558 );
559
560 Self {
561 new_source_commitment: pod_new_source_commitment,
562 claimed_commitment: pod_claimed_commitment,
563 equality_proof: equality_proof.into(),
564 ciphertext_amount_validity_proof: ciphertext_amount_validity_proof.into(),
565 fee_sigma_proof: fee_sigma_proof.into(),
566 fee_ciphertext_validity_proof: fee_ciphertext_validity_proof.into(),
567 range_proof: range_proof.try_into().expect("range proof: length error"),
568 }
569 }
570
571 pub fn verify(
572 &self,
573 ciphertext_lo: &TransferAmountEncryption,
574 ciphertext_hi: &TransferAmountEncryption,
575 transfer_with_fee_pubkeys: &TransferWithFeePubkeys,
576 new_spendable_ciphertext: &ElGamalCiphertext,
577 fee_ciphertext_lo: &FeeEncryption,
579 fee_ciphertext_hi: &FeeEncryption,
580 fee_parameters: FeeParameters,
581 transcript: &mut Transcript,
582 ) -> Result<(), ProofError> {
583 transcript.append_commitment(b"commitment-new-source", &self.new_source_commitment);
584
585 let new_source_commitment: PedersenCommitment = self.new_source_commitment.try_into()?;
586 let claimed_commitment: PedersenCommitment = self.claimed_commitment.try_into()?;
587
588 let equality_proof: CtxtCommEqualityProof = self.equality_proof.try_into()?;
589 let ciphertext_amount_validity_proof: AggregatedValidityProof =
590 self.ciphertext_amount_validity_proof.try_into()?;
591 let fee_sigma_proof: FeeSigmaProof = self.fee_sigma_proof.try_into()?;
592 let fee_ciphertext_validity_proof: AggregatedValidityProof =
593 self.fee_ciphertext_validity_proof.try_into()?;
594 let range_proof: RangeProof = self.range_proof.try_into()?;
595
596 equality_proof.verify(
598 &transfer_with_fee_pubkeys.source_pubkey,
599 new_spendable_ciphertext,
600 &new_source_commitment,
601 transcript,
602 )?;
603
604 ciphertext_amount_validity_proof.verify(
606 (
607 &transfer_with_fee_pubkeys.destination_pubkey,
608 &transfer_with_fee_pubkeys.auditor_pubkey,
609 ),
610 (&ciphertext_lo.commitment, &ciphertext_hi.commitment),
611 (
612 &ciphertext_lo.destination_handle,
613 &ciphertext_hi.destination_handle,
614 ),
615 (&ciphertext_lo.auditor_handle, &ciphertext_hi.auditor_handle),
616 transcript,
617 )?;
618
619 transcript.append_commitment(b"commitment-claimed", &self.claimed_commitment);
621
622 let combined_commitment = combine_lo_hi_commitments(
623 &ciphertext_lo.commitment,
624 &ciphertext_hi.commitment,
625 TRANSFER_AMOUNT_LO_BITS,
626 );
627 let combined_fee_commitment = combine_lo_hi_commitments(
628 &fee_ciphertext_lo.commitment,
629 &fee_ciphertext_hi.commitment,
630 TRANSFER_AMOUNT_LO_BITS,
631 );
632
633 let delta_commitment = compute_delta_commitment(
634 &combined_commitment,
635 &combined_fee_commitment,
636 fee_parameters.fee_rate_basis_points,
637 );
638
639 let pod_delta_commitment: pod::PedersenCommitment = delta_commitment.into();
640 transcript.append_commitment(b"commitment-delta", &pod_delta_commitment);
641
642 fee_sigma_proof.verify(
644 &combined_fee_commitment,
645 &delta_commitment,
646 &claimed_commitment,
647 fee_parameters.maximum_fee,
648 transcript,
649 )?;
650
651 fee_ciphertext_validity_proof.verify(
653 (
654 &transfer_with_fee_pubkeys.destination_pubkey,
655 &transfer_with_fee_pubkeys.withdraw_withheld_authority_pubkey,
656 ),
657 (&fee_ciphertext_lo.commitment, &fee_ciphertext_hi.commitment),
658 (
659 &fee_ciphertext_lo.destination_handle,
660 &fee_ciphertext_hi.destination_handle,
661 ),
662 (
663 &fee_ciphertext_lo.withdraw_withheld_authority_handle,
664 &fee_ciphertext_hi.withdraw_withheld_authority_handle,
665 ),
666 transcript,
667 )?;
668
669 let new_source_commitment = self.new_source_commitment.try_into()?;
671 let claimed_commitment_negated = &(*COMMITMENT_MAX_FEE_BASIS_POINTS) - &claimed_commitment;
672
673 range_proof.verify(
674 vec![
675 &new_source_commitment,
676 &ciphertext_lo.commitment,
677 &ciphertext_hi.commitment,
678 &claimed_commitment,
679 &claimed_commitment_negated,
680 &fee_ciphertext_lo.commitment,
681 &fee_ciphertext_hi.commitment,
682 ],
683 vec![
684 TRANSFER_SOURCE_AMOUNT_BITS, TRANSFER_AMOUNT_LO_BITS, TRANSFER_AMOUNT_HI_BITS, TRANSFER_DELTA_BITS, TRANSFER_DELTA_BITS, FEE_AMOUNT_LO_BITS, FEE_AMOUNT_HI_BITS, ],
692 transcript,
693 )?;
694
695 Ok(())
696 }
697}
698
699#[derive(Clone)]
701#[repr(C)]
702#[cfg(not(target_os = "solana"))]
703pub struct TransferWithFeePubkeys {
704 pub source_pubkey: ElGamalPubkey,
705 pub destination_pubkey: ElGamalPubkey,
706 pub auditor_pubkey: ElGamalPubkey,
707 pub withdraw_withheld_authority_pubkey: ElGamalPubkey,
708}
709
710#[cfg(not(target_os = "solana"))]
711impl TransferWithFeePubkeys {
712 pub fn to_bytes(&self) -> [u8; 128] {
713 let mut bytes = [0u8; 128];
714 bytes[..32].copy_from_slice(&self.source_pubkey.to_bytes());
715 bytes[32..64].copy_from_slice(&self.destination_pubkey.to_bytes());
716 bytes[64..96].copy_from_slice(&self.auditor_pubkey.to_bytes());
717 bytes[96..128].copy_from_slice(&self.withdraw_withheld_authority_pubkey.to_bytes());
718 bytes
719 }
720
721 pub fn from_bytes(bytes: &[u8]) -> Result<Self, ProofError> {
722 let bytes = array_ref![bytes, 0, 128];
723 let (source_pubkey, destination_pubkey, auditor_pubkey, withdraw_withheld_authority_pubkey) =
724 array_refs![bytes, 32, 32, 32, 32];
725
726 let source_pubkey =
727 ElGamalPubkey::from_bytes(source_pubkey).ok_or(ProofError::PubkeyDeserialization)?;
728 let destination_pubkey = ElGamalPubkey::from_bytes(destination_pubkey)
729 .ok_or(ProofError::PubkeyDeserialization)?;
730 let auditor_pubkey =
731 ElGamalPubkey::from_bytes(auditor_pubkey).ok_or(ProofError::PubkeyDeserialization)?;
732 let withdraw_withheld_authority_pubkey =
733 ElGamalPubkey::from_bytes(withdraw_withheld_authority_pubkey)
734 .ok_or(ProofError::PubkeyDeserialization)?;
735
736 Ok(Self {
737 source_pubkey,
738 destination_pubkey,
739 auditor_pubkey,
740 withdraw_withheld_authority_pubkey,
741 })
742 }
743}
744
745#[derive(Clone)]
746#[repr(C)]
747#[cfg(not(target_os = "solana"))]
748pub struct FeeEncryption {
749 pub commitment: PedersenCommitment,
750 pub destination_handle: DecryptHandle,
751 pub withdraw_withheld_authority_handle: DecryptHandle,
752}
753
754#[cfg(not(target_os = "solana"))]
755impl FeeEncryption {
756 pub fn new(
757 amount: u64,
758 destination_pubkey: &ElGamalPubkey,
759 withdraw_withheld_authority_pubkey: &ElGamalPubkey,
760 ) -> (Self, PedersenOpening) {
761 let (commitment, opening) = Pedersen::new(amount);
762 let fee_encryption = Self {
763 commitment,
764 destination_handle: destination_pubkey.decrypt_handle(&opening),
765 withdraw_withheld_authority_handle: withdraw_withheld_authority_pubkey
766 .decrypt_handle(&opening),
767 };
768
769 (fee_encryption, opening)
770 }
771
772 pub fn to_pod(&self) -> pod::FeeEncryption {
773 pod::FeeEncryption {
774 commitment: self.commitment.into(),
775 destination_handle: self.destination_handle.into(),
776 withdraw_withheld_authority_handle: self.withdraw_withheld_authority_handle.into(),
777 }
778 }
779}
780
781#[derive(Clone, Copy)]
782#[repr(C)]
783pub struct FeeParameters {
784 pub fee_rate_basis_points: u16,
786 pub maximum_fee: u64,
788}
789
790#[cfg(not(target_os = "solana"))]
791impl FeeParameters {
792 pub fn to_bytes(&self) -> [u8; 10] {
793 let mut bytes = [0u8; 10];
794 bytes[..2].copy_from_slice(&self.fee_rate_basis_points.to_le_bytes());
795 bytes[2..10].copy_from_slice(&self.maximum_fee.to_le_bytes());
796
797 bytes
798 }
799
800 pub fn from_bytes(bytes: &[u8]) -> Self {
801 let bytes = array_ref![bytes, 0, 10];
802 let (fee_rate_basis_points, maximum_fee) = array_refs![bytes, 2, 8];
803
804 Self {
805 fee_rate_basis_points: u16::from_le_bytes(*fee_rate_basis_points),
806 maximum_fee: u64::from_le_bytes(*maximum_fee),
807 }
808 }
809}
810
811#[cfg(not(target_os = "solana"))]
812fn calculate_fee(transfer_amount: u64, fee_rate_basis_points: u16) -> Option<(u64, u64)> {
813 let numerator = (transfer_amount as u128).checked_mul(fee_rate_basis_points as u128)?;
814
815 let fee = numerator
820 .checked_add(ONE_IN_BASIS_POINTS)?
821 .checked_sub(1)?
822 .checked_div(ONE_IN_BASIS_POINTS)?;
823
824 let delta_fee = fee
825 .checked_mul(ONE_IN_BASIS_POINTS)?
826 .checked_sub(numerator)?;
827
828 Some((fee as u64, delta_fee as u64))
829}
830
831#[cfg(not(target_os = "solana"))]
832fn compute_delta_commitment_and_opening(
833 (combined_commitment, combined_opening): (&PedersenCommitment, &PedersenOpening),
834 (combined_fee_commitment, combined_fee_opening): (&PedersenCommitment, &PedersenOpening),
835 fee_rate_basis_points: u16,
836) -> (PedersenCommitment, PedersenOpening) {
837 let fee_rate_scalar = Scalar::from(fee_rate_basis_points);
838 let delta_commitment = combined_fee_commitment * Scalar::from(MAX_FEE_BASIS_POINTS)
839 - combined_commitment * &fee_rate_scalar;
840 let delta_opening = combined_fee_opening * Scalar::from(MAX_FEE_BASIS_POINTS)
841 - combined_opening * &fee_rate_scalar;
842
843 (delta_commitment, delta_opening)
844}
845
846#[cfg(not(target_os = "solana"))]
847fn compute_delta_commitment(
848 combined_commitment: &PedersenCommitment,
849 combined_fee_commitment: &PedersenCommitment,
850 fee_rate_basis_points: u16,
851) -> PedersenCommitment {
852 let fee_rate_scalar = Scalar::from(fee_rate_basis_points);
853 combined_fee_commitment * Scalar::from(MAX_FEE_BASIS_POINTS)
854 - combined_commitment * &fee_rate_scalar
855}
856
857#[cfg(test)]
858mod test {
859 use super::*;
860
861 #[test]
862 fn test_fee_correctness() {
863 let source_keypair = ElGamalKeypair::new_rand();
864 let destination_pubkey = ElGamalKeypair::new_rand().public;
865 let auditor_pubkey = ElGamalKeypair::new_rand().public;
866 let withdraw_withheld_authority_pubkey = ElGamalKeypair::new_rand().public;
867
868 let spendable_balance: u64 = 120;
870 let spendable_ciphertext = source_keypair.public.encrypt(spendable_balance);
871
872 let transfer_amount: u64 = 0;
873
874 let fee_parameters = FeeParameters {
875 fee_rate_basis_points: 400,
876 maximum_fee: 3,
877 };
878
879 let fee_data = TransferWithFeeData::new(
880 transfer_amount,
881 (spendable_balance, &spendable_ciphertext),
882 &source_keypair,
883 (&destination_pubkey, &auditor_pubkey),
884 fee_parameters,
885 &withdraw_withheld_authority_pubkey,
886 )
887 .unwrap();
888
889 assert!(fee_data.verify().is_ok());
890
891 let spendable_balance: u64 = u64::max_value();
893 let spendable_ciphertext = source_keypair.public.encrypt(spendable_balance);
894
895 let transfer_amount: u64 =
896 (1u64 << (TRANSFER_AMOUNT_LO_BITS + TRANSFER_AMOUNT_HI_BITS)) - 1;
897
898 let fee_parameters = FeeParameters {
899 fee_rate_basis_points: 400,
900 maximum_fee: 3,
901 };
902
903 let fee_data = TransferWithFeeData::new(
904 transfer_amount,
905 (spendable_balance, &spendable_ciphertext),
906 &source_keypair,
907 (&destination_pubkey, &auditor_pubkey),
908 fee_parameters,
909 &withdraw_withheld_authority_pubkey,
910 )
911 .unwrap();
912
913 assert!(fee_data.verify().is_ok());
914
915 let spendable_balance: u64 = 120;
917 let spendable_ciphertext = source_keypair.public.encrypt(spendable_balance);
918
919 let transfer_amount: u64 = 100;
920
921 let fee_parameters = FeeParameters {
922 fee_rate_basis_points: 400,
923 maximum_fee: 3,
924 };
925
926 let fee_data = TransferWithFeeData::new(
927 transfer_amount,
928 (spendable_balance, &spendable_ciphertext),
929 &source_keypair,
930 (&destination_pubkey, &auditor_pubkey),
931 fee_parameters,
932 &withdraw_withheld_authority_pubkey,
933 )
934 .unwrap();
935
936 assert!(fee_data.verify().is_ok());
937
938 let spendable_balance: u64 = 120;
940 let spendable_ciphertext = source_keypair.public.encrypt(spendable_balance);
941
942 let transfer_amount: u64 = 0;
943
944 let fee_parameters = FeeParameters {
945 fee_rate_basis_points: 400,
946 maximum_fee: 3,
947 };
948
949 let destination_pubkey: ElGamalPubkey = pod::ElGamalPubkey::zeroed().try_into().unwrap();
951 let auditor_pubkey = ElGamalKeypair::new_rand().public;
952 let withdraw_withheld_authority_pubkey = ElGamalKeypair::new_rand().public;
953
954 let fee_data = TransferWithFeeData::new(
955 transfer_amount,
956 (spendable_balance, &spendable_ciphertext),
957 &source_keypair,
958 (&destination_pubkey, &auditor_pubkey),
959 fee_parameters,
960 &withdraw_withheld_authority_pubkey,
961 )
962 .unwrap();
963
964 assert!(fee_data.verify().is_err());
965
966 let destination_pubkey: ElGamalPubkey = ElGamalKeypair::new_rand().public;
968 let auditor_pubkey = pod::ElGamalPubkey::zeroed().try_into().unwrap();
969 let withdraw_withheld_authority_pubkey = ElGamalKeypair::new_rand().public;
970
971 let fee_data = TransferWithFeeData::new(
972 transfer_amount,
973 (spendable_balance, &spendable_ciphertext),
974 &source_keypair,
975 (&destination_pubkey, &auditor_pubkey),
976 fee_parameters,
977 &withdraw_withheld_authority_pubkey,
978 )
979 .unwrap();
980
981 assert!(fee_data.verify().is_err());
982
983 let destination_pubkey: ElGamalPubkey = ElGamalKeypair::new_rand().public;
985 let auditor_pubkey = ElGamalKeypair::new_rand().public;
986 let withdraw_withheld_authority_pubkey = pod::ElGamalPubkey::zeroed().try_into().unwrap();
987
988 let fee_data = TransferWithFeeData::new(
989 transfer_amount,
990 (spendable_balance, &spendable_ciphertext),
991 &source_keypair,
992 (&destination_pubkey, &auditor_pubkey),
993 fee_parameters,
994 &withdraw_withheld_authority_pubkey,
995 )
996 .unwrap();
997
998 assert!(fee_data.verify().is_err());
999 }
1000}