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
34pub 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 let (transfer_amount_lo, transfer_amount_hi) =
63 try_split_u64(transfer_amount, TRANSFER_AMOUNT_LO_BITS)
64 .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?;
65
66 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 let current_decrypted_available_balance = current_decryptable_available_balance
85 .decrypt(aes_key)
86 .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?;
87
88 let new_decrypted_available_balance = current_decrypted_available_balance
90 .checked_sub(transfer_amount)
91 .ok_or(TokenProofGenerationError::NotEnoughFunds)?;
92
93 let (new_available_balance_commitment, new_source_opening) =
95 Pedersen::new(new_decrypted_available_balance);
96
97 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 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 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 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 let fee_amount = std::cmp::min(transfer_fee_maximum_fee, raw_fee_amount);
170
171 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 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 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 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 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 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 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 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}