spl_token_confidential_transfer_proof_extraction/
transfer_with_fee.rs1use {
2 crate::{
3 encryption::{PodFeeCiphertext, PodTransferAmountCiphertext},
4 errors::TokenProofExtractionError,
5 },
6 bytemuck::bytes_of,
7 solana_curve25519::{
8 ristretto::{self, PodRistrettoPoint},
9 scalar::PodScalar,
10 },
11 solana_zk_sdk::{
12 encryption::pod::{
13 elgamal::{PodElGamalCiphertext, PodElGamalPubkey},
14 pedersen::PodPedersenCommitment,
15 },
16 zk_elgamal_proof_program::proof_data::{
17 BatchedGroupedCiphertext2HandlesValidityProofContext,
18 BatchedGroupedCiphertext3HandlesValidityProofContext, BatchedRangeProofContext,
19 CiphertextCommitmentEqualityProofContext, PercentageWithCapProofContext,
20 },
21 },
22};
23
24const MAX_FEE_BASIS_POINTS_SUB_ONE: u64 = 9_999;
25const MAX_FEE_BASIS_POINTS: u64 = 10_000;
26const REMAINING_BALANCE_BIT_LENGTH: u8 = 64;
27const TRANSFER_AMOUNT_LO_BIT_LENGTH: u8 = 16;
28const TRANSFER_AMOUNT_HI_BIT_LENGTH: u8 = 32;
29const DELTA_BIT_LENGTH: u8 = 16;
30const FEE_AMOUNT_LO_BIT_LENGTH: u8 = 16;
31const FEE_AMOUNT_HI_BIT_LENGTH: u8 = 32;
32const NET_TRANSFER_AMOUNT_BIT_LENGTH: u8 = 64;
33
34pub struct TransferWithFeePubkeys {
36 pub source: PodElGamalPubkey,
38 pub destination: PodElGamalPubkey,
40 pub auditor: PodElGamalPubkey,
42 pub withdraw_withheld_authority: PodElGamalPubkey,
44}
45
46pub struct TransferWithFeeProofContext {
49 pub ciphertext_lo: PodTransferAmountCiphertext,
51 pub ciphertext_hi: PodTransferAmountCiphertext,
53 pub transfer_with_fee_pubkeys: TransferWithFeePubkeys,
56 pub new_source_ciphertext: PodElGamalCiphertext,
58 pub fee_ciphertext_lo: PodFeeCiphertext,
61 pub fee_ciphertext_hi: PodFeeCiphertext,
63}
64
65impl TransferWithFeeProofContext {
66 pub fn verify_and_extract(
67 equality_proof_context: &CiphertextCommitmentEqualityProofContext,
68 transfer_amount_ciphertext_validity_proof_context: &BatchedGroupedCiphertext3HandlesValidityProofContext,
69 fee_sigma_proof_context: &PercentageWithCapProofContext,
70 fee_ciphertext_validity_proof_context: &BatchedGroupedCiphertext2HandlesValidityProofContext,
71 range_proof_context: &BatchedRangeProofContext,
72 expected_fee_rate_basis_points: u16,
73 expected_maximum_fee: u64,
74 ) -> Result<Self, TokenProofExtractionError> {
75 let CiphertextCommitmentEqualityProofContext {
81 pubkey: source_pubkey_from_equality_proof,
82 ciphertext: new_source_ciphertext,
83 commitment: new_source_commitment,
84 } = equality_proof_context;
85
86 let BatchedGroupedCiphertext3HandlesValidityProofContext {
93 first_pubkey: source_pubkey_from_validity_proof,
94 second_pubkey: destination_pubkey,
95 third_pubkey: auditor_pubkey,
96 grouped_ciphertext_lo: transfer_amount_ciphertext_lo,
97 grouped_ciphertext_hi: transfer_amount_ciphertext_hi,
98 } = transfer_amount_ciphertext_validity_proof_context;
99
100 let PercentageWithCapProofContext {
107 percentage_commitment: fee_commitment,
108 delta_commitment,
109 claimed_commitment,
110 max_value: proof_maximum_fee,
111 } = fee_sigma_proof_context;
112
113 let proof_maximum_fee: u64 = (*proof_maximum_fee).into();
114 if expected_maximum_fee != proof_maximum_fee {
115 return Err(TokenProofExtractionError::FeeParametersMismatch);
116 }
117
118 let BatchedGroupedCiphertext2HandlesValidityProofContext {
128 first_pubkey: destination_pubkey_from_transfer_fee_validity_proof,
129 second_pubkey: withdraw_withheld_authority_pubkey,
130 grouped_ciphertext_lo: fee_ciphertext_lo,
131 grouped_ciphertext_hi: fee_ciphertext_hi,
132 } = fee_ciphertext_validity_proof_context;
133
134 if destination_pubkey != destination_pubkey_from_transfer_fee_validity_proof {
135 return Err(TokenProofExtractionError::ElGamalPubkeyMismatch);
136 }
137
138 let BatchedRangeProofContext {
150 commitments: range_proof_commitments,
151 bit_lengths: range_proof_bit_lengths,
152 } = range_proof_context;
153
154 let transfer_amount_commitment_lo = transfer_amount_ciphertext_lo.extract_commitment();
157 let transfer_amount_commitment_hi = transfer_amount_ciphertext_hi.extract_commitment();
158
159 let fee_commitment_lo = fee_ciphertext_lo.extract_commitment();
160 let fee_commitment_hi = fee_ciphertext_hi.extract_commitment();
161
162 let max_fee_basis_points_sub_one_scalar = u64_to_scalar(MAX_FEE_BASIS_POINTS_SUB_ONE);
163 let max_fee_basis_points_sub_one_commitment =
164 ristretto::multiply_ristretto(&max_fee_basis_points_sub_one_scalar, &G)
165 .ok_or(TokenProofExtractionError::CurveArithmetic)?;
166 let claimed_complement_commitment = ristretto::subtract_ristretto(
167 &max_fee_basis_points_sub_one_commitment,
168 &commitment_to_ristretto(claimed_commitment),
169 )
170 .ok_or(TokenProofExtractionError::CurveArithmetic)?;
171
172 let transfer_amount_point = combine_lo_hi_pedersen_points(
173 &commitment_to_ristretto(&transfer_amount_commitment_lo),
174 &commitment_to_ristretto(&transfer_amount_commitment_hi),
175 )
176 .ok_or(TokenProofExtractionError::CurveArithmetic)?;
177 let fee_commitment_point = commitment_to_ristretto(fee_commitment);
178
179 let net_transfer_commitment_point =
180 ristretto::subtract_ristretto(&transfer_amount_point, &fee_commitment_point)
181 .ok_or(TokenProofExtractionError::CurveArithmetic)?;
182
183 let expected_commitments = [
184 bytes_of(new_source_commitment),
185 bytes_of(&transfer_amount_commitment_lo),
186 bytes_of(&transfer_amount_commitment_hi),
187 bytes_of(claimed_commitment),
188 bytes_of(&claimed_complement_commitment),
189 bytes_of(&fee_commitment_lo),
190 bytes_of(&fee_commitment_hi),
191 bytes_of(&net_transfer_commitment_point),
192 ];
193
194 if !range_proof_commitments
198 .iter()
199 .zip(expected_commitments.into_iter())
200 .all(|(proof_commitment, expected_commitment)| {
201 bytes_of(proof_commitment) == expected_commitment
202 })
203 {
204 return Err(TokenProofExtractionError::PedersenCommitmentMismatch);
205 }
206
207 let expected_bit_lengths = [
209 REMAINING_BALANCE_BIT_LENGTH,
210 TRANSFER_AMOUNT_LO_BIT_LENGTH,
211 TRANSFER_AMOUNT_HI_BIT_LENGTH,
212 DELTA_BIT_LENGTH,
213 DELTA_BIT_LENGTH,
214 FEE_AMOUNT_LO_BIT_LENGTH,
215 FEE_AMOUNT_HI_BIT_LENGTH,
216 NET_TRANSFER_AMOUNT_BIT_LENGTH,
217 ]
218 .iter();
219
220 if !range_proof_bit_lengths
224 .iter()
225 .zip(expected_bit_lengths)
226 .all(|(proof_len, expected_len)| proof_len == expected_len)
227 {
228 return Err(TokenProofExtractionError::RangeProofLengthMismatch);
229 }
230
231 let sigma_proof_fee_commitment_point: PodRistrettoPoint =
233 commitment_to_ristretto(fee_commitment);
234 let validity_proof_fee_point = combine_lo_hi_pedersen_points(
235 &commitment_to_ristretto(&fee_commitment_lo),
236 &commitment_to_ristretto(&fee_commitment_hi),
237 )
238 .ok_or(TokenProofExtractionError::CurveArithmetic)?;
239
240 if source_pubkey_from_equality_proof != source_pubkey_from_validity_proof {
241 return Err(TokenProofExtractionError::ElGamalPubkeyMismatch);
242 }
243
244 if validity_proof_fee_point != sigma_proof_fee_commitment_point {
245 return Err(TokenProofExtractionError::FeeParametersMismatch);
246 }
247
248 verify_delta_commitment(
249 &transfer_amount_commitment_lo,
250 &transfer_amount_commitment_hi,
251 fee_commitment,
252 delta_commitment,
253 expected_fee_rate_basis_points,
254 )?;
255
256 let transfer_with_fee_pubkeys = TransferWithFeePubkeys {
258 source: *source_pubkey_from_equality_proof,
259 destination: *destination_pubkey,
260 auditor: *auditor_pubkey,
261 withdraw_withheld_authority: *withdraw_withheld_authority_pubkey,
262 };
263
264 Ok(Self {
265 ciphertext_lo: PodTransferAmountCiphertext(*transfer_amount_ciphertext_lo),
266 ciphertext_hi: PodTransferAmountCiphertext(*transfer_amount_ciphertext_hi),
267 transfer_with_fee_pubkeys,
268 new_source_ciphertext: *new_source_ciphertext,
269 fee_ciphertext_lo: PodFeeCiphertext(*fee_ciphertext_lo),
270 fee_ciphertext_hi: PodFeeCiphertext(*fee_ciphertext_hi),
271 })
272 }
273}
274
275const G: PodRistrettoPoint = PodRistrettoPoint([
277 226, 242, 174, 10, 106, 188, 78, 113, 168, 132, 169, 97, 197, 0, 81, 95, 88, 227, 11, 106, 165,
278 130, 221, 141, 182, 166, 89, 69, 224, 141, 45, 118,
279]);
280
281fn u64_to_scalar(amount: u64) -> PodScalar {
283 let mut bytes = [0u8; 32];
284 bytes[..8].copy_from_slice(&amount.to_le_bytes());
285 PodScalar(bytes)
286}
287
288fn u16_to_scalar(amount: u16) -> PodScalar {
290 let mut bytes = [0u8; 32];
291 bytes[..2].copy_from_slice(&amount.to_le_bytes());
292 PodScalar(bytes)
293}
294
295fn commitment_to_ristretto(commitment: &PodPedersenCommitment) -> PodRistrettoPoint {
296 let mut bytes = [0u8; 32];
297 bytes.copy_from_slice(bytes_of(commitment));
298 PodRistrettoPoint(bytes)
299}
300
301fn combine_lo_hi_pedersen_points(
303 point_lo: &PodRistrettoPoint,
304 point_hi: &PodRistrettoPoint,
305) -> Option<PodRistrettoPoint> {
306 const SCALING_CONSTANT: u64 = 65536;
307 let scaling_constant_scalar = u64_to_scalar(SCALING_CONSTANT);
308 let scaled_point_hi = ristretto::multiply_ristretto(&scaling_constant_scalar, point_hi)?;
309 ristretto::add_ristretto(point_lo, &scaled_point_hi)
310}
311
312fn verify_delta_commitment(
314 transfer_amount_commitment_lo: &PodPedersenCommitment,
315 transfer_amount_commitment_hi: &PodPedersenCommitment,
316 fee_commitment: &PodPedersenCommitment,
317 proof_delta_commitment: &PodPedersenCommitment,
318 transfer_fee_basis_points: u16,
319) -> Result<(), TokenProofExtractionError> {
320 let transfer_amount_point = combine_lo_hi_pedersen_points(
321 &commitment_to_ristretto(transfer_amount_commitment_lo),
322 &commitment_to_ristretto(transfer_amount_commitment_hi),
323 )
324 .ok_or(TokenProofExtractionError::CurveArithmetic)?;
325 let transfer_fee_basis_points_scalar = u16_to_scalar(transfer_fee_basis_points);
326 let scaled_transfer_amount_point =
327 ristretto::multiply_ristretto(&transfer_fee_basis_points_scalar, &transfer_amount_point)
328 .ok_or(TokenProofExtractionError::CurveArithmetic)?;
329
330 let max_fee_basis_points_scalar = u64_to_scalar(MAX_FEE_BASIS_POINTS);
331 let fee_point: PodRistrettoPoint = commitment_to_ristretto(fee_commitment);
332 let scaled_fee_point = ristretto::multiply_ristretto(&max_fee_basis_points_scalar, &fee_point)
333 .ok_or(TokenProofExtractionError::CurveArithmetic)?;
334
335 let expected_delta_commitment_point =
336 ristretto::subtract_ristretto(&scaled_fee_point, &scaled_transfer_amount_point)
337 .ok_or(TokenProofExtractionError::CurveArithmetic)?;
338
339 let proof_delta_commitment_point = commitment_to_ristretto(proof_delta_commitment);
340 if expected_delta_commitment_point != proof_delta_commitment_point {
341 return Err(TokenProofExtractionError::CurveArithmetic);
342 }
343 Ok(())
344}