1use crate::{
2 instruction::{ProofType, ZkProofData},
3 zk_token_elgamal::pod,
4};
5#[cfg(not(target_os = "solana"))]
6use {
7 crate::{
8 encryption::{
9 elgamal::{ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey, ElGamalSecretKey},
10 pedersen::{Pedersen, PedersenCommitment, PedersenOpening},
11 },
12 errors::{ProofGenerationError, ProofVerificationError},
13 instruction::{
14 errors::InstructionError,
15 transfer::{
16 encryption::{FeeEncryption, TransferAmountCiphertext},
17 try_combine_lo_hi_ciphertexts, try_combine_lo_hi_commitments,
18 try_combine_lo_hi_openings, try_combine_lo_hi_u64, try_split_u64, FeeParameters,
19 Role,
20 },
21 },
22 range_proof::RangeProof,
23 sigma_proofs::{
24 batched_grouped_ciphertext_validity_proof::BatchedGroupedCiphertext2HandlesValidityProof,
25 ciphertext_commitment_equality_proof::CiphertextCommitmentEqualityProof,
26 fee_proof::FeeSigmaProof,
27 },
28 transcript::TranscriptProtocol,
29 },
30 bytemuck::bytes_of,
31 curve25519_dalek::scalar::Scalar,
32 merlin::Transcript,
33 std::convert::TryInto,
34 subtle::{ConditionallySelectable, ConstantTimeGreater},
35};
36
37#[cfg(not(target_os = "solana"))]
38const MAX_FEE_BASIS_POINTS: u64 = 10_000;
39#[cfg(not(target_os = "solana"))]
40const ONE_IN_BASIS_POINTS: u128 = MAX_FEE_BASIS_POINTS as u128;
41#[cfg(not(target_os = "solana"))]
42const MAX_DELTA_RANGE: u64 = MAX_FEE_BASIS_POINTS - 1;
43
44#[cfg(not(target_os = "solana"))]
45const TRANSFER_SOURCE_AMOUNT_BITS: usize = 64;
46#[cfg(not(target_os = "solana"))]
47const TRANSFER_AMOUNT_LO_BITS: usize = 16;
48#[cfg(not(target_os = "solana"))]
49const TRANSFER_AMOUNT_LO_NEGATED_BITS: usize = 16;
50#[cfg(not(target_os = "solana"))]
51const TRANSFER_AMOUNT_HI_BITS: usize = 32;
52#[cfg(not(target_os = "solana"))]
53const TRANSFER_DELTA_BITS: usize = 16;
54#[cfg(not(target_os = "solana"))]
55const FEE_AMOUNT_LO_BITS: usize = 16;
56#[cfg(not(target_os = "solana"))]
57const FEE_AMOUNT_HI_BITS: usize = 32;
58
59#[cfg(not(target_os = "solana"))]
60lazy_static::lazy_static! {
61 pub static ref COMMITMENT_MAX: PedersenCommitment = Pedersen::encode((1_u64 <<
62 TRANSFER_AMOUNT_LO_NEGATED_BITS) - 1);
63 pub static ref COMMITMENT_MAX_FEE_BASIS_POINTS: PedersenCommitment = Pedersen::encode(MAX_FEE_BASIS_POINTS);
64 pub static ref COMMITMENT_MAX_DELTA_RANGE: PedersenCommitment = Pedersen::encode(MAX_DELTA_RANGE);
65}
66
67#[derive(Clone, Copy, bytemuck_derive::Pod, bytemuck_derive::Zeroable)]
72#[repr(C)]
73pub struct TransferWithFeeData {
74 pub context: TransferWithFeeProofContext,
76
77 pub proof: TransferWithFeeProof,
79}
80
81#[derive(Clone, Copy, bytemuck_derive::Pod, bytemuck_derive::Zeroable)]
83#[repr(C)]
84pub struct TransferWithFeeProofContext {
85 pub ciphertext_lo: pod::TransferAmountCiphertext, pub ciphertext_hi: pod::TransferAmountCiphertext, pub transfer_with_fee_pubkeys: TransferWithFeePubkeys, pub new_source_ciphertext: pod::ElGamalCiphertext, pub fee_ciphertext_lo: pod::FeeEncryption, pub fee_ciphertext_hi: pod::FeeEncryption, pub fee_parameters: pod::FeeParameters, }
106
107#[derive(Clone, Copy, bytemuck_derive::Pod, bytemuck_derive::Zeroable)]
109#[repr(C)]
110pub struct TransferWithFeePubkeys {
111 pub source: pod::ElGamalPubkey,
112 pub destination: pod::ElGamalPubkey,
113 pub auditor: pod::ElGamalPubkey,
114 pub withdraw_withheld_authority: pod::ElGamalPubkey,
115}
116
117#[cfg(not(target_os = "solana"))]
118impl TransferWithFeeData {
119 pub fn new(
120 transfer_amount: u64,
121 (spendable_balance, old_source_ciphertext): (u64, &ElGamalCiphertext),
122 source_keypair: &ElGamalKeypair,
123 (destination_pubkey, auditor_pubkey): (&ElGamalPubkey, &ElGamalPubkey),
124 fee_parameters: FeeParameters,
125 withdraw_withheld_authority_pubkey: &ElGamalPubkey,
126 ) -> Result<Self, ProofGenerationError> {
127 let (amount_lo, amount_hi) = try_split_u64(transfer_amount, TRANSFER_AMOUNT_LO_BITS)
129 .map_err(|_| ProofGenerationError::IllegalAmountBitLength)?;
130
131 let (ciphertext_lo, opening_lo) = TransferAmountCiphertext::new(
132 amount_lo,
133 source_keypair.pubkey(),
134 destination_pubkey,
135 auditor_pubkey,
136 );
137 let (ciphertext_hi, opening_hi) = TransferAmountCiphertext::new(
138 amount_hi,
139 source_keypair.pubkey(),
140 destination_pubkey,
141 auditor_pubkey,
142 );
143
144 let new_spendable_balance = spendable_balance
146 .checked_sub(transfer_amount)
147 .ok_or(ProofGenerationError::NotEnoughFunds)?;
148
149 let transfer_amount_lo_source = ElGamalCiphertext {
150 commitment: *ciphertext_lo.get_commitment(),
151 handle: *ciphertext_lo.get_source_handle(),
152 };
153
154 let transfer_amount_hi_source = ElGamalCiphertext {
155 commitment: *ciphertext_hi.get_commitment(),
156 handle: *ciphertext_hi.get_source_handle(),
157 };
158
159 let new_source_ciphertext = old_source_ciphertext
160 - try_combine_lo_hi_ciphertexts(
161 &transfer_amount_lo_source,
162 &transfer_amount_hi_source,
163 TRANSFER_AMOUNT_LO_BITS,
164 )
165 .map_err(|_| ProofGenerationError::IllegalAmountBitLength)?;
166
167 let (fee_amount, delta_fee) =
171 calculate_fee(transfer_amount, fee_parameters.fee_rate_basis_points)
172 .ok_or(ProofGenerationError::FeeCalculation)?;
173
174 let below_max = u64::ct_gt(&fee_parameters.maximum_fee, &fee_amount);
175 let fee_to_encrypt =
176 u64::conditional_select(&fee_parameters.maximum_fee, &fee_amount, below_max);
177
178 let (fee_to_encrypt_lo, fee_to_encrypt_hi) =
180 try_split_u64(fee_to_encrypt, FEE_AMOUNT_LO_BITS)
181 .map_err(|_| ProofGenerationError::IllegalAmountBitLength)?;
182
183 let (fee_ciphertext_lo, opening_fee_lo) = FeeEncryption::new(
184 fee_to_encrypt_lo,
185 destination_pubkey,
186 withdraw_withheld_authority_pubkey,
187 );
188
189 let (fee_ciphertext_hi, opening_fee_hi) = FeeEncryption::new(
190 fee_to_encrypt_hi,
191 destination_pubkey,
192 withdraw_withheld_authority_pubkey,
193 );
194
195 let pod_transfer_with_fee_pubkeys = TransferWithFeePubkeys {
197 source: (*source_keypair.pubkey()).into(),
198 destination: (*destination_pubkey).into(),
199 auditor: (*auditor_pubkey).into(),
200 withdraw_withheld_authority: (*withdraw_withheld_authority_pubkey).into(),
201 };
202 let pod_ciphertext_lo: pod::TransferAmountCiphertext = ciphertext_lo.into();
203 let pod_ciphertext_hi: pod::TransferAmountCiphertext = ciphertext_hi.into();
204 let pod_new_source_ciphertext: pod::ElGamalCiphertext = new_source_ciphertext.into();
205 let pod_fee_ciphertext_lo: pod::FeeEncryption = fee_ciphertext_lo.into();
206 let pod_fee_ciphertext_hi: pod::FeeEncryption = fee_ciphertext_hi.into();
207
208 let context = TransferWithFeeProofContext {
209 ciphertext_lo: pod_ciphertext_lo,
210 ciphertext_hi: pod_ciphertext_hi,
211 transfer_with_fee_pubkeys: pod_transfer_with_fee_pubkeys,
212 new_source_ciphertext: pod_new_source_ciphertext,
213 fee_ciphertext_lo: pod_fee_ciphertext_lo,
214 fee_ciphertext_hi: pod_fee_ciphertext_hi,
215 fee_parameters: fee_parameters.into(),
216 };
217
218 let mut transcript = context.new_transcript();
219
220 let proof = TransferWithFeeProof::new(
221 (amount_lo, &ciphertext_lo, &opening_lo),
222 (amount_hi, &ciphertext_hi, &opening_hi),
223 source_keypair,
224 (destination_pubkey, auditor_pubkey),
225 (new_spendable_balance, &new_source_ciphertext),
226 (fee_to_encrypt_lo, &fee_ciphertext_lo, &opening_fee_lo),
227 (fee_to_encrypt_hi, &fee_ciphertext_hi, &opening_fee_hi),
228 delta_fee,
229 withdraw_withheld_authority_pubkey,
230 fee_parameters,
231 &mut transcript,
232 )?;
233
234 Ok(Self { context, proof })
235 }
236
237 fn ciphertext_lo(&self, role: Role) -> Result<ElGamalCiphertext, InstructionError> {
239 let ciphertext_lo: TransferAmountCiphertext = self
240 .context
241 .ciphertext_lo
242 .try_into()
243 .map_err(|_| InstructionError::Decryption)?;
244
245 let handle_lo = match role {
246 Role::Source => Some(ciphertext_lo.get_source_handle()),
247 Role::Destination => Some(ciphertext_lo.get_destination_handle()),
248 Role::Auditor => Some(ciphertext_lo.get_auditor_handle()),
249 Role::WithdrawWithheldAuthority => None,
250 };
251
252 if let Some(handle) = handle_lo {
253 Ok(ElGamalCiphertext {
254 commitment: *ciphertext_lo.get_commitment(),
255 handle: *handle,
256 })
257 } else {
258 Err(InstructionError::MissingCiphertext)
259 }
260 }
261
262 fn ciphertext_hi(&self, role: Role) -> Result<ElGamalCiphertext, InstructionError> {
264 let ciphertext_hi: TransferAmountCiphertext = self
265 .context
266 .ciphertext_hi
267 .try_into()
268 .map_err(|_| InstructionError::Decryption)?;
269
270 let handle_hi = match role {
271 Role::Source => Some(ciphertext_hi.get_source_handle()),
272 Role::Destination => Some(ciphertext_hi.get_destination_handle()),
273 Role::Auditor => Some(ciphertext_hi.get_auditor_handle()),
274 Role::WithdrawWithheldAuthority => None,
275 };
276
277 if let Some(handle) = handle_hi {
278 Ok(ElGamalCiphertext {
279 commitment: *ciphertext_hi.get_commitment(),
280 handle: *handle,
281 })
282 } else {
283 Err(InstructionError::MissingCiphertext)
284 }
285 }
286
287 fn fee_ciphertext_lo(&self, role: Role) -> Result<ElGamalCiphertext, InstructionError> {
289 let fee_ciphertext_lo: FeeEncryption = self
290 .context
291 .fee_ciphertext_lo
292 .try_into()
293 .map_err(|_| InstructionError::Decryption)?;
294
295 let fee_handle_lo = match role {
296 Role::Source => None,
297 Role::Destination => Some(fee_ciphertext_lo.get_destination_handle()),
298 Role::Auditor => None,
299 Role::WithdrawWithheldAuthority => {
300 Some(fee_ciphertext_lo.get_withdraw_withheld_authority_handle())
301 }
302 };
303
304 if let Some(handle) = fee_handle_lo {
305 Ok(ElGamalCiphertext {
306 commitment: *fee_ciphertext_lo.get_commitment(),
307 handle: *handle,
308 })
309 } else {
310 Err(InstructionError::MissingCiphertext)
311 }
312 }
313
314 fn fee_ciphertext_hi(&self, role: Role) -> Result<ElGamalCiphertext, InstructionError> {
316 let fee_ciphertext_hi: FeeEncryption = self
317 .context
318 .fee_ciphertext_hi
319 .try_into()
320 .map_err(|_| InstructionError::Decryption)?;
321
322 let fee_handle_hi = match role {
323 Role::Source => None,
324 Role::Destination => Some(fee_ciphertext_hi.get_destination_handle()),
325 Role::Auditor => None,
326 Role::WithdrawWithheldAuthority => {
327 Some(fee_ciphertext_hi.get_withdraw_withheld_authority_handle())
328 }
329 };
330
331 if let Some(handle) = fee_handle_hi {
332 Ok(ElGamalCiphertext {
333 commitment: *fee_ciphertext_hi.get_commitment(),
334 handle: *handle,
335 })
336 } else {
337 Err(InstructionError::MissingCiphertext)
338 }
339 }
340
341 pub fn decrypt_amount(
343 &self,
344 role: Role,
345 sk: &ElGamalSecretKey,
346 ) -> Result<u64, InstructionError> {
347 let ciphertext_lo = self.ciphertext_lo(role)?;
348 let ciphertext_hi = self.ciphertext_hi(role)?;
349
350 let amount_lo = ciphertext_lo.decrypt_u32(sk);
351 let amount_hi = ciphertext_hi.decrypt_u32(sk);
352
353 if let (Some(amount_lo), Some(amount_hi)) = (amount_lo, amount_hi) {
354 let shifted_amount_hi = amount_hi << TRANSFER_AMOUNT_LO_BITS;
355 Ok(amount_lo + shifted_amount_hi)
356 } else {
357 Err(InstructionError::Decryption)
358 }
359 }
360
361 pub fn decrypt_fee_amount(
363 &self,
364 role: Role,
365 sk: &ElGamalSecretKey,
366 ) -> Result<u64, InstructionError> {
367 let ciphertext_lo = self.fee_ciphertext_lo(role)?;
368 let ciphertext_hi = self.fee_ciphertext_hi(role)?;
369
370 let fee_amount_lo = ciphertext_lo.decrypt_u32(sk);
371 let fee_amount_hi = ciphertext_hi.decrypt_u32(sk);
372
373 if let (Some(fee_amount_lo), Some(fee_amount_hi)) = (fee_amount_lo, fee_amount_hi) {
374 let shifted_fee_amount_hi = fee_amount_hi << FEE_AMOUNT_LO_BITS;
375 Ok(fee_amount_lo + shifted_fee_amount_hi)
376 } else {
377 Err(InstructionError::Decryption)
378 }
379 }
380}
381
382impl ZkProofData<TransferWithFeeProofContext> for TransferWithFeeData {
383 const PROOF_TYPE: ProofType = ProofType::TransferWithFee;
384
385 fn context_data(&self) -> &TransferWithFeeProofContext {
386 &self.context
387 }
388
389 #[cfg(not(target_os = "solana"))]
390 fn verify_proof(&self) -> Result<(), ProofVerificationError> {
391 let mut transcript = self.context.new_transcript();
392
393 let source_pubkey = self.context.transfer_with_fee_pubkeys.source.try_into()?;
394 let destination_pubkey = self
395 .context
396 .transfer_with_fee_pubkeys
397 .destination
398 .try_into()?;
399 let auditor_pubkey = self.context.transfer_with_fee_pubkeys.auditor.try_into()?;
400 let withdraw_withheld_authority_pubkey = self
401 .context
402 .transfer_with_fee_pubkeys
403 .withdraw_withheld_authority
404 .try_into()?;
405
406 let ciphertext_lo = self.context.ciphertext_lo.try_into()?;
407 let ciphertext_hi = self.context.ciphertext_hi.try_into()?;
408 let new_source_ciphertext = self.context.new_source_ciphertext.try_into()?;
409
410 let fee_ciphertext_lo = self.context.fee_ciphertext_lo.try_into()?;
411 let fee_ciphertext_hi = self.context.fee_ciphertext_hi.try_into()?;
412 let fee_parameters = self.context.fee_parameters.into();
413
414 self.proof.verify(
415 &source_pubkey,
416 &destination_pubkey,
417 &auditor_pubkey,
418 &withdraw_withheld_authority_pubkey,
419 &ciphertext_lo,
420 &ciphertext_hi,
421 &new_source_ciphertext,
422 &fee_ciphertext_lo,
423 &fee_ciphertext_hi,
424 fee_parameters,
425 &mut transcript,
426 )
427 }
428}
429
430#[allow(non_snake_case)]
431#[cfg(not(target_os = "solana"))]
432impl TransferWithFeeProofContext {
433 fn new_transcript(&self) -> Transcript {
434 let mut transcript = Transcript::new(b"transfer-with-fee-proof");
435 transcript.append_message(b"ciphertext-lo", bytes_of(&self.ciphertext_lo));
436 transcript.append_message(b"ciphertext-hi", bytes_of(&self.ciphertext_hi));
437 transcript.append_message(
438 b"transfer-with-fee-pubkeys",
439 bytes_of(&self.transfer_with_fee_pubkeys),
440 );
441 transcript.append_message(
442 b"new-source-ciphertext",
443 bytes_of(&self.new_source_ciphertext),
444 );
445 transcript.append_message(b"fee-ciphertext-lo", bytes_of(&self.fee_ciphertext_lo));
446 transcript.append_message(b"fee-ciphertext-hi", bytes_of(&self.fee_ciphertext_hi));
447 transcript.append_message(b"fee-parameters", bytes_of(&self.fee_parameters));
448 transcript
449 }
450}
451
452#[repr(C)]
453#[derive(Clone, Copy, bytemuck_derive::Pod, bytemuck_derive::Zeroable)]
454pub struct TransferWithFeeProof {
455 pub new_source_commitment: pod::PedersenCommitment,
456 pub claimed_commitment: pod::PedersenCommitment,
457 pub equality_proof: pod::CiphertextCommitmentEqualityProof,
458 pub ciphertext_amount_validity_proof: pod::BatchedGroupedCiphertext2HandlesValidityProof,
459 pub fee_sigma_proof: pod::FeeSigmaProof,
460 pub fee_ciphertext_validity_proof: pod::BatchedGroupedCiphertext2HandlesValidityProof,
461 pub range_proof: pod::RangeProofU256,
462}
463
464#[allow(non_snake_case)]
465#[cfg(not(target_os = "solana"))]
466impl TransferWithFeeProof {
467 #[allow(clippy::too_many_arguments)]
468 #[allow(clippy::many_single_char_names)]
469 pub fn new(
470 transfer_amount_lo_data: (u64, &TransferAmountCiphertext, &PedersenOpening),
471 transfer_amount_hi_data: (u64, &TransferAmountCiphertext, &PedersenOpening),
472 source_keypair: &ElGamalKeypair,
473 (destination_pubkey, auditor_pubkey): (&ElGamalPubkey, &ElGamalPubkey),
474 (source_new_balance, new_source_ciphertext): (u64, &ElGamalCiphertext),
475 (fee_amount_lo, fee_ciphertext_lo, opening_fee_lo): (u64, &FeeEncryption, &PedersenOpening),
477 (fee_amount_hi, fee_ciphertext_hi, opening_fee_hi): (u64, &FeeEncryption, &PedersenOpening),
478 delta_fee: u64,
479 withdraw_withheld_authority_pubkey: &ElGamalPubkey,
480 fee_parameters: FeeParameters,
481 transcript: &mut Transcript,
482 ) -> Result<Self, ProofGenerationError> {
483 let (transfer_amount_lo, ciphertext_lo, opening_lo) = transfer_amount_lo_data;
484 let (transfer_amount_hi, ciphertext_hi, opening_hi) = transfer_amount_hi_data;
485
486 let (new_source_commitment, opening_source) = Pedersen::new(source_new_balance);
488 let pod_new_source_commitment: pod::PedersenCommitment = new_source_commitment.into();
489
490 transcript.append_commitment(b"commitment-new-source", &pod_new_source_commitment);
491
492 let equality_proof = CiphertextCommitmentEqualityProof::new(
494 source_keypair,
495 new_source_ciphertext,
496 &opening_source,
497 source_new_balance,
498 transcript,
499 );
500
501 let ciphertext_amount_validity_proof = BatchedGroupedCiphertext2HandlesValidityProof::new(
503 (destination_pubkey, auditor_pubkey),
504 (transfer_amount_lo, transfer_amount_hi),
505 (opening_lo, opening_hi),
506 transcript,
507 );
508
509 let (claimed_commitment, opening_claimed) = Pedersen::new(delta_fee);
511 let pod_claimed_commitment: pod::PedersenCommitment = claimed_commitment.into();
512 transcript.append_commitment(b"commitment-claimed", &pod_claimed_commitment);
513
514 let combined_commitment = try_combine_lo_hi_commitments(
515 ciphertext_lo.get_commitment(),
516 ciphertext_hi.get_commitment(),
517 TRANSFER_AMOUNT_LO_BITS,
518 )
519 .map_err(|_| ProofGenerationError::IllegalAmountBitLength)?;
520 let combined_opening =
521 try_combine_lo_hi_openings(opening_lo, opening_hi, TRANSFER_AMOUNT_LO_BITS)
522 .map_err(|_| ProofGenerationError::IllegalAmountBitLength)?;
523
524 let combined_fee_amount =
525 try_combine_lo_hi_u64(fee_amount_lo, fee_amount_hi, TRANSFER_AMOUNT_LO_BITS)
526 .map_err(|_| ProofGenerationError::IllegalAmountBitLength)?;
527 let combined_fee_commitment = try_combine_lo_hi_commitments(
528 fee_ciphertext_lo.get_commitment(),
529 fee_ciphertext_hi.get_commitment(),
530 TRANSFER_AMOUNT_LO_BITS,
531 )
532 .map_err(|_| ProofGenerationError::IllegalAmountBitLength)?;
533 let combined_fee_opening =
534 try_combine_lo_hi_openings(opening_fee_lo, opening_fee_hi, TRANSFER_AMOUNT_LO_BITS)
535 .map_err(|_| ProofGenerationError::IllegalAmountBitLength)?;
536
537 let (delta_commitment, opening_delta) = compute_delta_commitment_and_opening(
539 (&combined_commitment, &combined_opening),
540 (&combined_fee_commitment, &combined_fee_opening),
541 fee_parameters.fee_rate_basis_points,
542 );
543 let pod_delta_commitment: pod::PedersenCommitment = delta_commitment.into();
544 transcript.append_commitment(b"commitment-delta", &pod_delta_commitment);
545
546 let fee_sigma_proof = FeeSigmaProof::new(
548 (
549 combined_fee_amount,
550 &combined_fee_commitment,
551 &combined_fee_opening,
552 ),
553 (delta_fee, &delta_commitment, &opening_delta),
554 (&claimed_commitment, &opening_claimed),
555 fee_parameters.maximum_fee,
556 transcript,
557 );
558
559 let fee_ciphertext_validity_proof = BatchedGroupedCiphertext2HandlesValidityProof::new(
561 (destination_pubkey, withdraw_withheld_authority_pubkey),
562 (fee_amount_lo, fee_amount_hi),
563 (opening_fee_lo, opening_fee_hi),
564 transcript,
565 );
566
567 let opening_claimed_negated = &PedersenOpening::default() - &opening_claimed;
569
570 let combined_amount = try_combine_lo_hi_u64(
571 transfer_amount_lo,
572 transfer_amount_hi,
573 TRANSFER_AMOUNT_LO_BITS,
574 )
575 .map_err(|_| ProofGenerationError::IllegalAmountBitLength)?;
576 let amount_sub_fee = combined_amount
577 .checked_sub(combined_fee_amount)
578 .ok_or(ProofGenerationError::FeeCalculation)?;
579 let amount_sub_fee_opening = combined_opening - combined_fee_opening;
580
581 let delta_negated = MAX_DELTA_RANGE
582 .checked_sub(delta_fee)
583 .ok_or(ProofGenerationError::FeeCalculation)?;
584
585 let range_proof = RangeProof::new(
586 vec![
587 source_new_balance,
588 transfer_amount_lo,
589 transfer_amount_hi,
590 delta_fee,
591 delta_negated,
592 fee_amount_lo,
593 fee_amount_hi,
594 amount_sub_fee,
595 ],
596 vec![
597 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, TRANSFER_SOURCE_AMOUNT_BITS, ],
606 vec![
607 &opening_source,
608 opening_lo,
609 opening_hi,
610 &opening_claimed,
611 &opening_claimed_negated,
612 opening_fee_lo,
613 opening_fee_hi,
614 &amount_sub_fee_opening,
615 ],
616 transcript,
617 )?;
618
619 Ok(Self {
620 new_source_commitment: pod_new_source_commitment,
621 claimed_commitment: pod_claimed_commitment,
622 equality_proof: equality_proof.into(),
623 ciphertext_amount_validity_proof: ciphertext_amount_validity_proof.into(),
624 fee_sigma_proof: fee_sigma_proof.into(),
625 fee_ciphertext_validity_proof: fee_ciphertext_validity_proof.into(),
626 range_proof: range_proof
627 .try_into()
628 .map_err(|_| ProofGenerationError::ProofLength)?,
629 })
630 }
631
632 #[allow(clippy::too_many_arguments)]
633 pub fn verify(
634 &self,
635 source_pubkey: &ElGamalPubkey,
636 destination_pubkey: &ElGamalPubkey,
637 auditor_pubkey: &ElGamalPubkey,
638 withdraw_withheld_authority_pubkey: &ElGamalPubkey,
639 ciphertext_lo: &TransferAmountCiphertext,
640 ciphertext_hi: &TransferAmountCiphertext,
641 new_spendable_ciphertext: &ElGamalCiphertext,
642 fee_ciphertext_lo: &FeeEncryption,
644 fee_ciphertext_hi: &FeeEncryption,
645 fee_parameters: FeeParameters,
646 transcript: &mut Transcript,
647 ) -> Result<(), ProofVerificationError> {
648 transcript.append_commitment(b"commitment-new-source", &self.new_source_commitment);
649
650 let new_source_commitment: PedersenCommitment = self.new_source_commitment.try_into()?;
651 let claimed_commitment: PedersenCommitment = self.claimed_commitment.try_into()?;
652
653 let equality_proof: CiphertextCommitmentEqualityProof = self.equality_proof.try_into()?;
654 let ciphertext_amount_validity_proof: BatchedGroupedCiphertext2HandlesValidityProof =
655 self.ciphertext_amount_validity_proof.try_into()?;
656 let fee_sigma_proof: FeeSigmaProof = self.fee_sigma_proof.try_into()?;
657 let fee_ciphertext_validity_proof: BatchedGroupedCiphertext2HandlesValidityProof =
658 self.fee_ciphertext_validity_proof.try_into()?;
659 let range_proof: RangeProof = self.range_proof.try_into()?;
660
661 equality_proof.verify(
663 source_pubkey,
664 new_spendable_ciphertext,
665 &new_source_commitment,
666 transcript,
667 )?;
668
669 ciphertext_amount_validity_proof.verify(
671 (destination_pubkey, auditor_pubkey),
672 (
673 ciphertext_lo.get_commitment(),
674 ciphertext_hi.get_commitment(),
675 ),
676 (
677 ciphertext_lo.get_destination_handle(),
678 ciphertext_hi.get_destination_handle(),
679 ),
680 (
681 ciphertext_lo.get_auditor_handle(),
682 ciphertext_hi.get_auditor_handle(),
683 ),
684 transcript,
685 )?;
686
687 transcript.append_commitment(b"commitment-claimed", &self.claimed_commitment);
689
690 let combined_commitment = try_combine_lo_hi_commitments(
691 ciphertext_lo.get_commitment(),
692 ciphertext_hi.get_commitment(),
693 TRANSFER_AMOUNT_LO_BITS,
694 )
695 .map_err(|_| ProofVerificationError::IllegalAmountBitLength)?;
696 let combined_fee_commitment = try_combine_lo_hi_commitments(
697 fee_ciphertext_lo.get_commitment(),
698 fee_ciphertext_hi.get_commitment(),
699 TRANSFER_AMOUNT_LO_BITS,
700 )
701 .map_err(|_| ProofVerificationError::IllegalAmountBitLength)?;
702
703 let delta_commitment = compute_delta_commitment(
704 &combined_commitment,
705 &combined_fee_commitment,
706 fee_parameters.fee_rate_basis_points,
707 );
708
709 let pod_delta_commitment: pod::PedersenCommitment = delta_commitment.into();
710 transcript.append_commitment(b"commitment-delta", &pod_delta_commitment);
711
712 fee_sigma_proof.verify(
714 &combined_fee_commitment,
715 &delta_commitment,
716 &claimed_commitment,
717 fee_parameters.maximum_fee,
718 transcript,
719 )?;
720
721 fee_ciphertext_validity_proof.verify(
723 (destination_pubkey, withdraw_withheld_authority_pubkey),
724 (
725 fee_ciphertext_lo.get_commitment(),
726 fee_ciphertext_hi.get_commitment(),
727 ),
728 (
729 fee_ciphertext_lo.get_destination_handle(),
730 fee_ciphertext_hi.get_destination_handle(),
731 ),
732 (
733 fee_ciphertext_lo.get_withdraw_withheld_authority_handle(),
734 fee_ciphertext_hi.get_withdraw_withheld_authority_handle(),
735 ),
736 transcript,
737 )?;
738
739 let new_source_commitment = self.new_source_commitment.try_into()?;
741 let claimed_commitment_negated = &(*COMMITMENT_MAX_DELTA_RANGE) - &claimed_commitment;
742 let amount_sub_fee_commitment = combined_commitment - combined_fee_commitment;
743
744 range_proof.verify(
745 vec![
746 &new_source_commitment,
747 ciphertext_lo.get_commitment(),
748 ciphertext_hi.get_commitment(),
749 &claimed_commitment,
750 &claimed_commitment_negated,
751 fee_ciphertext_lo.get_commitment(),
752 fee_ciphertext_hi.get_commitment(),
753 &amount_sub_fee_commitment,
754 ],
755 vec![
756 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, TRANSFER_SOURCE_AMOUNT_BITS, ],
765 transcript,
766 )?;
767
768 Ok(())
769 }
770}
771
772#[cfg(not(target_os = "solana"))]
773fn calculate_fee(transfer_amount: u64, fee_rate_basis_points: u16) -> Option<(u64, u64)> {
774 let numerator = (transfer_amount as u128).checked_mul(fee_rate_basis_points as u128)?;
775
776 let fee = numerator
781 .checked_add(ONE_IN_BASIS_POINTS)?
782 .checked_sub(1)?
783 .checked_div(ONE_IN_BASIS_POINTS)?;
784
785 let delta_fee = fee
786 .checked_mul(ONE_IN_BASIS_POINTS)?
787 .checked_sub(numerator)?;
788
789 Some((fee as u64, delta_fee as u64))
790}
791
792#[cfg(not(target_os = "solana"))]
793fn compute_delta_commitment_and_opening(
794 (combined_commitment, combined_opening): (&PedersenCommitment, &PedersenOpening),
795 (combined_fee_commitment, combined_fee_opening): (&PedersenCommitment, &PedersenOpening),
796 fee_rate_basis_points: u16,
797) -> (PedersenCommitment, PedersenOpening) {
798 let fee_rate_scalar = Scalar::from(fee_rate_basis_points);
799 let delta_commitment = combined_fee_commitment * Scalar::from(MAX_FEE_BASIS_POINTS)
800 - combined_commitment * &fee_rate_scalar;
801 let delta_opening = combined_fee_opening * Scalar::from(MAX_FEE_BASIS_POINTS)
802 - combined_opening * &fee_rate_scalar;
803
804 (delta_commitment, delta_opening)
805}
806
807#[cfg(not(target_os = "solana"))]
808fn compute_delta_commitment(
809 combined_commitment: &PedersenCommitment,
810 combined_fee_commitment: &PedersenCommitment,
811 fee_rate_basis_points: u16,
812) -> PedersenCommitment {
813 let fee_rate_scalar = Scalar::from(fee_rate_basis_points);
814 combined_fee_commitment * Scalar::from(MAX_FEE_BASIS_POINTS)
815 - combined_commitment * &fee_rate_scalar
816}
817
818#[cfg(test)]
819mod test {
820 use {super::*, bytemuck::Zeroable};
821
822 #[test]
823 fn test_fee_correctness() {
824 let source_keypair = ElGamalKeypair::new_rand();
825
826 let destination_keypair = ElGamalKeypair::new_rand();
827 let destination_pubkey = destination_keypair.pubkey();
828
829 let auditor_keypair = ElGamalKeypair::new_rand();
830 let auditor_pubkey = auditor_keypair.pubkey();
831
832 let withdraw_withheld_authority_keypair = ElGamalKeypair::new_rand();
833 let withdraw_withheld_authority_pubkey = withdraw_withheld_authority_keypair.pubkey();
834
835 let spendable_balance: u64 = 120;
837 let spendable_ciphertext = source_keypair.pubkey().encrypt(spendable_balance);
838
839 let transfer_amount: u64 = 0;
840
841 let fee_parameters = FeeParameters {
842 fee_rate_basis_points: 400,
843 maximum_fee: 3,
844 };
845
846 let fee_data = TransferWithFeeData::new(
847 transfer_amount,
848 (spendable_balance, &spendable_ciphertext),
849 &source_keypair,
850 (destination_pubkey, auditor_pubkey),
851 fee_parameters,
852 withdraw_withheld_authority_pubkey,
853 )
854 .unwrap();
855
856 assert!(fee_data.verify_proof().is_ok());
857
858 let spendable_balance: u64 = u64::MAX;
860 let spendable_ciphertext = source_keypair.pubkey().encrypt(spendable_balance);
861
862 let transfer_amount: u64 =
863 (1u64 << (TRANSFER_AMOUNT_LO_BITS + TRANSFER_AMOUNT_HI_BITS)) - 1;
864
865 let fee_parameters = FeeParameters {
866 fee_rate_basis_points: 400,
867 maximum_fee: 3,
868 };
869
870 let fee_data = TransferWithFeeData::new(
871 transfer_amount,
872 (spendable_balance, &spendable_ciphertext),
873 &source_keypair,
874 (destination_pubkey, auditor_pubkey),
875 fee_parameters,
876 withdraw_withheld_authority_pubkey,
877 )
878 .unwrap();
879
880 assert!(fee_data.verify_proof().is_ok());
881
882 let spendable_balance: u64 = 120;
884 let spendable_ciphertext = source_keypair.pubkey().encrypt(spendable_balance);
885
886 let transfer_amount: u64 = 100;
887
888 let fee_parameters = FeeParameters {
889 fee_rate_basis_points: 400,
890 maximum_fee: 3,
891 };
892
893 let fee_data = TransferWithFeeData::new(
894 transfer_amount,
895 (spendable_balance, &spendable_ciphertext),
896 &source_keypair,
897 (destination_pubkey, auditor_pubkey),
898 fee_parameters,
899 withdraw_withheld_authority_pubkey,
900 )
901 .unwrap();
902
903 assert!(fee_data.verify_proof().is_ok());
904
905 let spendable_balance: u64 = 120;
907 let spendable_ciphertext = source_keypair.pubkey().encrypt(spendable_balance);
908
909 let transfer_amount: u64 = 0;
910
911 let fee_parameters = FeeParameters {
912 fee_rate_basis_points: 400,
913 maximum_fee: 3,
914 };
915
916 let destination_pubkey: ElGamalPubkey = pod::ElGamalPubkey::zeroed().try_into().unwrap();
918
919 let auditor_keypair = ElGamalKeypair::new_rand();
920 let auditor_pubkey = auditor_keypair.pubkey();
921
922 let withdraw_withheld_authority_keypair = ElGamalKeypair::new_rand();
923 let withdraw_withheld_authority_pubkey = withdraw_withheld_authority_keypair.pubkey();
924
925 let fee_data = TransferWithFeeData::new(
926 transfer_amount,
927 (spendable_balance, &spendable_ciphertext),
928 &source_keypair,
929 (&destination_pubkey, auditor_pubkey),
930 fee_parameters,
931 withdraw_withheld_authority_pubkey,
932 )
933 .unwrap();
934
935 assert!(fee_data.verify_proof().is_err());
936 }
937}