gorb_ctpg/
transfer_with_fee.rs

1use {
2    crate::{
3        encryption::{FeeCiphertext, TransferAmountCiphertext},
4        errors::TokenProofGenerationError,
5        try_combine_lo_hi_ciphertexts, try_combine_lo_hi_commitments, try_combine_lo_hi_openings,
6        try_split_u64, CiphertextValidityProofWithAuditorCiphertext, TRANSFER_AMOUNT_HI_BITS,
7        TRANSFER_AMOUNT_LO_BITS,
8    },
9    curve25519_dalek::scalar::Scalar,
10    solana_zk_sdk::{
11        encryption::{
12            auth_encryption::{AeCiphertext, AeKey},
13            elgamal::{ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey},
14            grouped_elgamal::GroupedElGamal,
15            pedersen::{Pedersen, PedersenCommitment, PedersenOpening},
16        },
17        zk_elgamal_proof_program::proof_data::{
18            BatchedGroupedCiphertext2HandlesValidityProofData,
19            BatchedGroupedCiphertext3HandlesValidityProofData, BatchedRangeProofU256Data,
20            CiphertextCommitmentEqualityProofData, PercentageWithCapProofData, ZkProofData,
21        },
22    },
23};
24
25const MAX_FEE_BASIS_POINTS: u64 = 10_000;
26const ONE_IN_BASIS_POINTS: u128 = MAX_FEE_BASIS_POINTS as u128;
27
28const FEE_AMOUNT_LO_BITS: usize = 16;
29const FEE_AMOUNT_HI_BITS: usize = 32;
30
31const REMAINING_BALANCE_BIT_LENGTH: usize = 64;
32const DELTA_BIT_LENGTH: usize = 48;
33
34/// The proof data required for a confidential transfer instruction when the
35/// mint is extended for fees
36pub struct TransferWithFeeProofData {
37    pub equality_proof_data: CiphertextCommitmentEqualityProofData,
38    pub transfer_amount_ciphertext_validity_proof_data_with_ciphertext:
39        CiphertextValidityProofWithAuditorCiphertext,
40    pub percentage_with_cap_proof_data: PercentageWithCapProofData,
41    pub fee_ciphertext_validity_proof_data: BatchedGroupedCiphertext2HandlesValidityProofData,
42    pub range_proof_data: BatchedRangeProofU256Data,
43}
44
45#[allow(clippy::too_many_arguments)]
46pub fn transfer_with_fee_split_proof_data(
47    current_available_balance: &ElGamalCiphertext,
48    current_decryptable_available_balance: &AeCiphertext,
49    transfer_amount: u64,
50    source_elgamal_keypair: &ElGamalKeypair,
51    aes_key: &AeKey,
52    destination_elgamal_pubkey: &ElGamalPubkey,
53    auditor_elgamal_pubkey: Option<&ElGamalPubkey>,
54    withdraw_withheld_authority_elgamal_pubkey: &ElGamalPubkey,
55    fee_rate_basis_points: u16,
56    maximum_fee: u64,
57) -> Result<TransferWithFeeProofData, TokenProofGenerationError> {
58    let default_auditor_pubkey = ElGamalPubkey::default();
59    let auditor_elgamal_pubkey = auditor_elgamal_pubkey.unwrap_or(&default_auditor_pubkey);
60
61    // Split the transfer amount into the low and high bit components
62    let (transfer_amount_lo, transfer_amount_hi) =
63        try_split_u64(transfer_amount, TRANSFER_AMOUNT_LO_BITS)
64            .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?;
65
66    // Encrypt the `lo` and `hi` transfer amounts
67    let (transfer_amount_grouped_ciphertext_lo, transfer_amount_opening_lo) =
68        TransferAmountCiphertext::new(
69            transfer_amount_lo,
70            source_elgamal_keypair.pubkey(),
71            destination_elgamal_pubkey,
72            auditor_elgamal_pubkey,
73        );
74
75    let (transfer_amount_grouped_ciphertext_hi, transfer_amount_opening_hi) =
76        TransferAmountCiphertext::new(
77            transfer_amount_hi,
78            source_elgamal_keypair.pubkey(),
79            destination_elgamal_pubkey,
80            auditor_elgamal_pubkey,
81        );
82
83    // Decrypt the current available balance at the source
84    let current_decrypted_available_balance = current_decryptable_available_balance
85        .decrypt(aes_key)
86        .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?;
87
88    // Compute the remaining balance at the source
89    let new_decrypted_available_balance = current_decrypted_available_balance
90        .checked_sub(transfer_amount)
91        .ok_or(TokenProofGenerationError::NotEnoughFunds)?;
92
93    // Create a new Pedersen commitment for the remaining balance at the source
94    let (new_available_balance_commitment, new_source_opening) =
95        Pedersen::new(new_decrypted_available_balance);
96
97    // Compute the remaining balance at the source as ElGamal ciphertexts
98    let transfer_amount_source_ciphertext_lo = transfer_amount_grouped_ciphertext_lo
99        .0
100        .to_elgamal_ciphertext(0)
101        .unwrap();
102
103    let transfer_amount_source_ciphertext_hi = transfer_amount_grouped_ciphertext_hi
104        .0
105        .to_elgamal_ciphertext(0)
106        .unwrap();
107
108    #[allow(clippy::arithmetic_side_effects)]
109    let new_available_balance_ciphertext = current_available_balance
110        - try_combine_lo_hi_ciphertexts(
111            &transfer_amount_source_ciphertext_lo,
112            &transfer_amount_source_ciphertext_hi,
113            TRANSFER_AMOUNT_LO_BITS,
114        )
115        .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?;
116
117    // generate equality proof data
118    let equality_proof_data = CiphertextCommitmentEqualityProofData::new(
119        source_elgamal_keypair,
120        &new_available_balance_ciphertext,
121        &new_available_balance_commitment,
122        &new_source_opening,
123        new_decrypted_available_balance,
124    )
125    .map_err(TokenProofGenerationError::from)?;
126
127    // generate ciphertext validity data
128    let transfer_amount_ciphertext_validity_proof_data =
129        BatchedGroupedCiphertext3HandlesValidityProofData::new(
130            source_elgamal_keypair.pubkey(),
131            destination_elgamal_pubkey,
132            auditor_elgamal_pubkey,
133            &transfer_amount_grouped_ciphertext_lo.0,
134            &transfer_amount_grouped_ciphertext_hi.0,
135            transfer_amount_lo,
136            transfer_amount_hi,
137            &transfer_amount_opening_lo,
138            &transfer_amount_opening_hi,
139        )
140        .map_err(TokenProofGenerationError::from)?;
141
142    let transfer_amount_auditor_ciphertext_lo = transfer_amount_ciphertext_validity_proof_data
143        .context_data()
144        .grouped_ciphertext_lo
145        .try_extract_ciphertext(2)
146        .map_err(|_| TokenProofGenerationError::CiphertextExtraction)?;
147
148    let transfer_amount_auditor_ciphertext_hi = transfer_amount_ciphertext_validity_proof_data
149        .context_data()
150        .grouped_ciphertext_hi
151        .try_extract_ciphertext(2)
152        .map_err(|_| TokenProofGenerationError::CiphertextExtraction)?;
153
154    let transfer_amount_ciphertext_validity_proof_data_with_ciphertext =
155        CiphertextValidityProofWithAuditorCiphertext {
156            proof_data: transfer_amount_ciphertext_validity_proof_data,
157            ciphertext_lo: transfer_amount_auditor_ciphertext_lo,
158            ciphertext_hi: transfer_amount_auditor_ciphertext_hi,
159        };
160
161    // calculate fee
162    let transfer_fee_basis_points = fee_rate_basis_points;
163    let transfer_fee_maximum_fee = maximum_fee;
164    let (raw_fee_amount, delta_fee) = calculate_fee(transfer_amount, transfer_fee_basis_points)
165        .ok_or(TokenProofGenerationError::FeeCalculation)?;
166
167    // if raw fee is greater than the maximum fee, then use the maximum fee for the
168    // fee amount
169    let fee_amount = std::cmp::min(transfer_fee_maximum_fee, raw_fee_amount);
170
171    // split and encrypt fee
172    let (fee_amount_lo, fee_amount_hi) = try_split_u64(fee_amount, FEE_AMOUNT_LO_BITS)
173        .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?;
174    let (fee_ciphertext_lo, fee_opening_lo) = FeeCiphertext::new(
175        fee_amount_lo,
176        destination_elgamal_pubkey,
177        withdraw_withheld_authority_elgamal_pubkey,
178    );
179    let (fee_ciphertext_hi, fee_opening_hi) = FeeCiphertext::new(
180        fee_amount_hi,
181        destination_elgamal_pubkey,
182        withdraw_withheld_authority_elgamal_pubkey,
183    );
184
185    // create combined commitments and openings to be used to generate proofs
186    let combined_transfer_amount_commitment = try_combine_lo_hi_commitments(
187        transfer_amount_grouped_ciphertext_lo.get_commitment(),
188        transfer_amount_grouped_ciphertext_hi.get_commitment(),
189        TRANSFER_AMOUNT_LO_BITS,
190    )
191    .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?;
192    let combined_transfer_amount_opening = try_combine_lo_hi_openings(
193        &transfer_amount_opening_lo,
194        &transfer_amount_opening_hi,
195        TRANSFER_AMOUNT_LO_BITS,
196    )
197    .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?;
198
199    let combined_fee_commitment = try_combine_lo_hi_commitments(
200        fee_ciphertext_lo.get_commitment(),
201        fee_ciphertext_hi.get_commitment(),
202        FEE_AMOUNT_LO_BITS,
203    )
204    .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?;
205    let combined_fee_opening =
206        try_combine_lo_hi_openings(&fee_opening_lo, &fee_opening_hi, FEE_AMOUNT_LO_BITS)
207            .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?;
208
209    // compute claimed and real delta commitment
210    let (claimed_commitment, claimed_opening) = Pedersen::new(delta_fee);
211    let (delta_commitment, delta_opening) = compute_delta_commitment_and_opening(
212        (
213            &combined_transfer_amount_commitment,
214            &combined_transfer_amount_opening,
215        ),
216        (&combined_fee_commitment, &combined_fee_opening),
217        transfer_fee_basis_points,
218    );
219
220    // generate fee sigma proof
221    let percentage_with_cap_proof_data = PercentageWithCapProofData::new(
222        &combined_fee_commitment,
223        &combined_fee_opening,
224        fee_amount,
225        &delta_commitment,
226        &delta_opening,
227        delta_fee,
228        &claimed_commitment,
229        &claimed_opening,
230        transfer_fee_maximum_fee,
231    )
232    .map_err(TokenProofGenerationError::from)?;
233
234    // encrypt the fee amount under the destination and withdraw withheld authority
235    // ElGamal public key
236    let fee_destination_withdraw_withheld_authority_ciphertext_lo = GroupedElGamal::encrypt_with(
237        [
238            destination_elgamal_pubkey,
239            withdraw_withheld_authority_elgamal_pubkey,
240        ],
241        fee_amount_lo,
242        &fee_opening_lo,
243    );
244    let fee_destination_withdraw_withheld_authority_ciphertext_hi = GroupedElGamal::encrypt_with(
245        [
246            destination_elgamal_pubkey,
247            withdraw_withheld_authority_elgamal_pubkey,
248        ],
249        fee_amount_hi,
250        &fee_opening_hi,
251    );
252
253    // generate fee ciphertext validity data
254    let fee_ciphertext_validity_proof_data =
255        BatchedGroupedCiphertext2HandlesValidityProofData::new(
256            destination_elgamal_pubkey,
257            withdraw_withheld_authority_elgamal_pubkey,
258            &fee_destination_withdraw_withheld_authority_ciphertext_lo,
259            &fee_destination_withdraw_withheld_authority_ciphertext_hi,
260            fee_amount_lo,
261            fee_amount_hi,
262            &fee_opening_lo,
263            &fee_opening_hi,
264        )
265        .map_err(TokenProofGenerationError::from)?;
266
267    // generate range proof data
268    let delta_fee_complement = MAX_FEE_BASIS_POINTS
269        .checked_sub(delta_fee)
270        .ok_or(TokenProofGenerationError::FeeCalculation)?;
271
272    let max_fee_basis_points_commitment =
273        Pedersen::with(MAX_FEE_BASIS_POINTS, &PedersenOpening::default());
274    #[allow(clippy::arithmetic_side_effects)]
275    let claimed_complement_commitment = max_fee_basis_points_commitment - claimed_commitment;
276    #[allow(clippy::arithmetic_side_effects)]
277    let claimed_complement_opening = PedersenOpening::default() - &claimed_opening;
278
279    let range_proof_data = BatchedRangeProofU256Data::new(
280        vec![
281            &new_available_balance_commitment,
282            transfer_amount_grouped_ciphertext_lo.get_commitment(),
283            transfer_amount_grouped_ciphertext_hi.get_commitment(),
284            &claimed_commitment,
285            &claimed_complement_commitment,
286            fee_ciphertext_lo.get_commitment(),
287            fee_ciphertext_hi.get_commitment(),
288        ],
289        vec![
290            new_decrypted_available_balance,
291            transfer_amount_lo,
292            transfer_amount_hi,
293            delta_fee,
294            delta_fee_complement,
295            fee_amount_lo,
296            fee_amount_hi,
297        ],
298        vec![
299            REMAINING_BALANCE_BIT_LENGTH,
300            TRANSFER_AMOUNT_LO_BITS,
301            TRANSFER_AMOUNT_HI_BITS,
302            DELTA_BIT_LENGTH,
303            DELTA_BIT_LENGTH,
304            FEE_AMOUNT_LO_BITS,
305            FEE_AMOUNT_HI_BITS,
306        ],
307        vec![
308            &new_source_opening,
309            &transfer_amount_opening_lo,
310            &transfer_amount_opening_hi,
311            &claimed_opening,
312            &claimed_complement_opening,
313            &fee_opening_lo,
314            &fee_opening_hi,
315        ],
316    )
317    .map_err(TokenProofGenerationError::from)?;
318
319    Ok(TransferWithFeeProofData {
320        equality_proof_data,
321        transfer_amount_ciphertext_validity_proof_data_with_ciphertext,
322        percentage_with_cap_proof_data,
323        fee_ciphertext_validity_proof_data,
324        range_proof_data,
325    })
326}
327
328fn calculate_fee(transfer_amount: u64, fee_rate_basis_points: u16) -> Option<(u64, u64)> {
329    let numerator = (transfer_amount as u128).checked_mul(fee_rate_basis_points as u128)?;
330
331    // Warning: Division may involve CPU opcodes that have variable execution times.
332    // This non-constant-time execution of the fee calculation can theoretically
333    // reveal information about the transfer amount. For transfers that involve
334    // extremely sensitive data, additional care should be put into how the fees
335    // are calculated.
336    let fee = numerator
337        .checked_add(ONE_IN_BASIS_POINTS)?
338        .checked_sub(1)?
339        .checked_div(ONE_IN_BASIS_POINTS)?;
340
341    let delta_fee = fee
342        .checked_mul(ONE_IN_BASIS_POINTS)?
343        .checked_sub(numerator)?;
344
345    Some((fee as u64, delta_fee as u64))
346}
347
348#[allow(clippy::arithmetic_side_effects)]
349fn compute_delta_commitment_and_opening(
350    (combined_commitment, combined_opening): (&PedersenCommitment, &PedersenOpening),
351    (combined_fee_commitment, combined_fee_opening): (&PedersenCommitment, &PedersenOpening),
352    fee_rate_basis_points: u16,
353) -> (PedersenCommitment, PedersenOpening) {
354    let fee_rate_scalar = Scalar::from(fee_rate_basis_points);
355    let delta_commitment = combined_fee_commitment * Scalar::from(MAX_FEE_BASIS_POINTS)
356        - combined_commitment * fee_rate_scalar;
357    let delta_opening = combined_fee_opening * Scalar::from(MAX_FEE_BASIS_POINTS)
358        - combined_opening * fee_rate_scalar;
359
360    (delta_commitment, delta_opening)
361}