safe_zk_token_sdk/instruction/
transfer_with_fee.rs

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)]
61#[derive(Clone, Copy, Pod, Zeroable)]
62#[repr(C)]
63pub struct TransferWithFeeData {
64    /// Group encryption of the low 16 bites of the transfer amount
65    pub ciphertext_lo: pod::TransferAmountEncryption,
66
67    /// Group encryption of the high 48 bits of the transfer amount
68    pub ciphertext_hi: pod::TransferAmountEncryption,
69
70    /// The public encryption keys associated with the transfer: source, dest, and auditor
71    pub transfer_with_fee_pubkeys: pod::TransferWithFeePubkeys,
72
73    /// The final spendable ciphertext after the transfer,
74    pub new_source_ciphertext: pod::ElGamalCiphertext,
75
76    // transfer fee encryption of the low 16 bits of the transfer fee amount
77    pub fee_ciphertext_lo: pod::FeeEncryption,
78
79    // transfer fee encryption of the hi 32 bits of the transfer fee amount
80    pub fee_ciphertext_hi: pod::FeeEncryption,
81
82    // fee parameters
83    pub fee_parameters: pod::FeeParameters,
84
85    // transfer fee proof
86    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        // split and encrypt transfer amount
100        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        // subtract transfer amount from the spendable ciphertext
116        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        // calculate fee
138        //
139        // TODO: add comment on delta fee
140        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        // split and encrypt fee
149        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        // generate transcript and append all public inputs
164        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    /// Extracts the lo ciphertexts associated with a transfer-with-fee data
212    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    /// Extracts the lo ciphertexts associated with a transfer-with-fee data
233    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    /// Extracts the lo fee ciphertexts associated with a transfer_with_fee data
254    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    /// Extracts the hi fee ciphertexts associated with a transfer_with_fee data
277    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    /// Decrypts transfer amount from transfer-with-fee data
300    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    /// Decrypts transfer amount from transfer-with-fee data
316    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// #[derive(Clone, Copy, Pod, Zeroable)]
367#[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 parameters
441        (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        // generate a Pedersen commitment for the remaining balance in source
452        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        // generate equality_proof
458        let equality_proof = CtxtCommEqualityProof::new(
459            source_keypair,
460            new_source_ciphertext,
461            source_new_balance,
462            &opening_source,
463            transcript,
464        );
465
466        // generate ciphertext validity proof
467        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        // compute claimed delta commitment
475        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        // compute real delta commitment
498        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        // generate fee sigma proof
507        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        // generate ciphertext validity proof for fee ciphertexts
520        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        // generate the range proof
528        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, // 64
541                TRANSFER_AMOUNT_LO_BITS,     // 16
542                TRANSFER_AMOUNT_HI_BITS,     // 32
543                TRANSFER_DELTA_BITS,         // 48
544                TRANSFER_DELTA_BITS,         // 48
545                FEE_AMOUNT_LO_BITS,          // 16
546                FEE_AMOUNT_HI_BITS,          // 32
547            ],
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 parameters
578        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        // verify equality proof
597        equality_proof.verify(
598            &transfer_with_fee_pubkeys.source_pubkey,
599            new_spendable_ciphertext,
600            &new_source_commitment,
601            transcript,
602        )?;
603
604        // verify that the transfer amount is encrypted correctly
605        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        // verify fee sigma proof
620        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        // verify fee sigma proof
643        fee_sigma_proof.verify(
644            &combined_fee_commitment,
645            &delta_commitment,
646            &claimed_commitment,
647            fee_parameters.maximum_fee,
648            transcript,
649        )?;
650
651        // verify ciphertext validity proof for fee ciphertexts
652        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        // verify range proof
670        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, // 64
685                TRANSFER_AMOUNT_LO_BITS,     // 16
686                TRANSFER_AMOUNT_HI_BITS,     // 32
687                TRANSFER_DELTA_BITS,         // 48
688                TRANSFER_DELTA_BITS,         // 48
689                FEE_AMOUNT_LO_BITS,          // 16
690                FEE_AMOUNT_HI_BITS,          // 32
691            ],
692            transcript,
693        )?;
694
695        Ok(())
696    }
697}
698
699/// The ElGamal public keys needed for a transfer with fee
700#[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    /// Fee rate expressed as basis points of the transfer amount, i.e. increments of 0.01%
785    pub fee_rate_basis_points: u16,
786    /// Maximum fee assessed on transfers, expressed as an amount of tokens
787    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    // Warning: Division may involve CPU opcodes that have variable execution times. This
816    // non-constant-time execution of the fee calculation can theoretically reveal information
817    // about the transfer amount. For transfers that invole extremely sensitive data, additional
818    // care should be put into how the fees are calculated.
819    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        // Case 1: transfer 0 amount
869        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        // Case 2: transfer max amount
892        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        // Case 3: general success case
916        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        // Case 4: invalid destination, auditor, or withdraw authority pubkeys
939        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        // destination pubkey invalid
950        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        // auditor pubkey invalid
967        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        // withdraw authority invalid
984        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}