solana_zk_token_sdk/instruction/transfer/
with_fee.rs

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/// The instruction data that is needed for the `ProofInstruction::TransferWithFee` instruction.
68///
69/// It includes the cryptographic proof as well as the context data information needed to verify
70/// the proof.
71#[derive(Clone, Copy, bytemuck_derive::Pod, bytemuck_derive::Zeroable)]
72#[repr(C)]
73pub struct TransferWithFeeData {
74    /// The context data for the transfer with fee proof
75    pub context: TransferWithFeeProofContext,
76
77    // transfer fee proof
78    pub proof: TransferWithFeeProof,
79}
80
81/// The context data needed to verify a transfer-with-fee proof.
82#[derive(Clone, Copy, bytemuck_derive::Pod, bytemuck_derive::Zeroable)]
83#[repr(C)]
84pub struct TransferWithFeeProofContext {
85    /// Group encryption of the low 16 bites of the transfer amount
86    pub ciphertext_lo: pod::TransferAmountCiphertext, // 128 bytes
87
88    /// Group encryption of the high 48 bits of the transfer amount
89    pub ciphertext_hi: pod::TransferAmountCiphertext, // 128 bytes
90
91    /// The public encryption keys associated with the transfer: source, dest, and auditor
92    pub transfer_with_fee_pubkeys: TransferWithFeePubkeys, // 128 bytes
93
94    /// The final spendable ciphertext after the transfer,
95    pub new_source_ciphertext: pod::ElGamalCiphertext, // 64 bytes
96
97    // transfer fee encryption of the low 16 bits of the transfer fee amount
98    pub fee_ciphertext_lo: pod::FeeEncryption, // 96 bytes
99
100    // transfer fee encryption of the hi 32 bits of the transfer fee amount
101    pub fee_ciphertext_hi: pod::FeeEncryption, // 96 bytes
102
103    // fee parameters
104    pub fee_parameters: pod::FeeParameters, // 10 bytes
105}
106
107/// The ElGamal public keys needed for a transfer with fee
108#[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        // split and encrypt transfer amount
128        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        // subtract transfer amount from the spendable ciphertext
145        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        // calculate fee
168        //
169        // TODO: add comment on delta fee
170        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        // split and encrypt fee
179        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        // generate transcript and append all public inputs
196        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    /// Extracts the lo ciphertexts associated with a transfer-with-fee data
238    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    /// Extracts the lo ciphertexts associated with a transfer-with-fee data
263    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    /// Extracts the lo fee ciphertexts associated with a transfer_with_fee data
288    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    /// Extracts the hi fee ciphertexts associated with a transfer_with_fee data
315    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    /// Decrypts transfer amount from transfer-with-fee data
342    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    /// Decrypts transfer amount from transfer-with-fee data
362    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 parameters
476        (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        // generate a Pedersen commitment for the remaining balance in source
487        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        // generate equality_proof
493        let equality_proof = CiphertextCommitmentEqualityProof::new(
494            source_keypair,
495            new_source_ciphertext,
496            &opening_source,
497            source_new_balance,
498            transcript,
499        );
500
501        // generate ciphertext validity proof
502        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        // compute claimed delta commitment
510        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        // compute real delta commitment
538        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        // generate fee sigma proof
547        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        // generate ciphertext validity proof for fee ciphertexts
560        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        // generate the range proof
568        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, // 64
598                TRANSFER_AMOUNT_LO_BITS,     // 16
599                TRANSFER_AMOUNT_HI_BITS,     // 32
600                TRANSFER_DELTA_BITS,         // 16
601                TRANSFER_DELTA_BITS,         // 16
602                FEE_AMOUNT_LO_BITS,          // 16
603                FEE_AMOUNT_HI_BITS,          // 32
604                TRANSFER_SOURCE_AMOUNT_BITS, // 64
605            ],
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 parameters
643        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        // verify equality proof
662        equality_proof.verify(
663            source_pubkey,
664            new_spendable_ciphertext,
665            &new_source_commitment,
666            transcript,
667        )?;
668
669        // verify that the transfer amount is encrypted correctly
670        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        // verify fee sigma proof
688        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        // verify fee sigma proof
713        fee_sigma_proof.verify(
714            &combined_fee_commitment,
715            &delta_commitment,
716            &claimed_commitment,
717            fee_parameters.maximum_fee,
718            transcript,
719        )?;
720
721        // verify ciphertext validity proof for fee ciphertexts
722        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        // verify range proof
740        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, // 64
757                TRANSFER_AMOUNT_LO_BITS,     // 16
758                TRANSFER_AMOUNT_HI_BITS,     // 32
759                TRANSFER_DELTA_BITS,         // 16
760                TRANSFER_DELTA_BITS,         // 16
761                FEE_AMOUNT_LO_BITS,          // 16
762                FEE_AMOUNT_HI_BITS,          // 32
763                TRANSFER_SOURCE_AMOUNT_BITS, // 64
764            ],
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    // Warning: Division may involve CPU opcodes that have variable execution times. This
777    // non-constant-time execution of the fee calculation can theoretically reveal information
778    // about the transfer amount. For transfers that invole extremely sensitive data, additional
779    // care should be put into how the fees are calculated.
780    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        // Case 1: transfer 0 amount
836        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        // Case 2: transfer max amount
859        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        // Case 3: general success case
883        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        // Case 4: destination pubkey invalid
906        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        // destination pubkey invalid
917        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}