gorb_ct_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: u64 = 10_000;
25const REMAINING_BALANCE_BIT_LENGTH: u8 = 64;
26const TRANSFER_AMOUNT_LO_BIT_LENGTH: u8 = 16;
27const TRANSFER_AMOUNT_HI_BIT_LENGTH: u8 = 32;
28const DELTA_BIT_LENGTH: u8 = 48;
29const FEE_AMOUNT_LO_BIT_LENGTH: u8 = 16;
30const FEE_AMOUNT_HI_BIT_LENGTH: u8 = 32;
31
32pub struct TransferWithFeePubkeys {
34 pub source: PodElGamalPubkey,
36 pub destination: PodElGamalPubkey,
38 pub auditor: PodElGamalPubkey,
40 pub withdraw_withheld_authority: PodElGamalPubkey,
42}
43
44pub struct TransferWithFeeProofContext {
47 pub ciphertext_lo: PodTransferAmountCiphertext,
49 pub ciphertext_hi: PodTransferAmountCiphertext,
51 pub transfer_with_fee_pubkeys: TransferWithFeePubkeys,
54 pub new_source_ciphertext: PodElGamalCiphertext,
56 pub fee_ciphertext_lo: PodFeeCiphertext,
59 pub fee_ciphertext_hi: PodFeeCiphertext,
61}
62
63impl TransferWithFeeProofContext {
64 pub fn verify_and_extract(
65 equality_proof_context: &CiphertextCommitmentEqualityProofContext,
66 transfer_amount_ciphertext_validity_proof_context: &BatchedGroupedCiphertext3HandlesValidityProofContext,
67 fee_sigma_proof_context: &PercentageWithCapProofContext,
68 fee_ciphertext_validity_proof_context: &BatchedGroupedCiphertext2HandlesValidityProofContext,
69 range_proof_context: &BatchedRangeProofContext,
70 expected_fee_rate_basis_points: u16,
71 expected_maximum_fee: u64,
72 ) -> Result<Self, TokenProofExtractionError> {
73 let CiphertextCommitmentEqualityProofContext {
79 pubkey: source_pubkey_from_equality_proof,
80 ciphertext: new_source_ciphertext,
81 commitment: new_source_commitment,
82 } = equality_proof_context;
83
84 let BatchedGroupedCiphertext3HandlesValidityProofContext {
91 first_pubkey: source_pubkey_from_validity_proof,
92 second_pubkey: destination_pubkey,
93 third_pubkey: auditor_pubkey,
94 grouped_ciphertext_lo: transfer_amount_ciphertext_lo,
95 grouped_ciphertext_hi: transfer_amount_ciphertext_hi,
96 } = transfer_amount_ciphertext_validity_proof_context;
97
98 let PercentageWithCapProofContext {
105 percentage_commitment: fee_commitment,
106 delta_commitment,
107 claimed_commitment,
108 max_value: proof_maximum_fee,
109 } = fee_sigma_proof_context;
110
111 let proof_maximum_fee: u64 = (*proof_maximum_fee).into();
112 if expected_maximum_fee != proof_maximum_fee {
113 return Err(TokenProofExtractionError::FeeParametersMismatch);
114 }
115
116 let BatchedGroupedCiphertext2HandlesValidityProofContext {
126 first_pubkey: destination_pubkey_from_transfer_fee_validity_proof,
127 second_pubkey: withdraw_withheld_authority_pubkey,
128 grouped_ciphertext_lo: fee_ciphertext_lo,
129 grouped_ciphertext_hi: fee_ciphertext_hi,
130 } = fee_ciphertext_validity_proof_context;
131
132 if destination_pubkey != destination_pubkey_from_transfer_fee_validity_proof {
133 return Err(TokenProofExtractionError::ElGamalPubkeyMismatch);
134 }
135
136 let BatchedRangeProofContext {
147 commitments: range_proof_commitments,
148 bit_lengths: range_proof_bit_lengths,
149 } = range_proof_context;
150
151 let transfer_amount_commitment_lo = transfer_amount_ciphertext_lo.extract_commitment();
154 let transfer_amount_commitment_hi = transfer_amount_ciphertext_hi.extract_commitment();
155
156 let fee_commitment_lo = fee_ciphertext_lo.extract_commitment();
157 let fee_commitment_hi = fee_ciphertext_hi.extract_commitment();
158
159 let max_fee_basis_points_scalar = u64_to_scalar(MAX_FEE_BASIS_POINTS);
160 let max_fee_basis_points_commitment =
161 ristretto::multiply_ristretto(&max_fee_basis_points_scalar, &G)
162 .ok_or(TokenProofExtractionError::CurveArithmetic)?;
163 let claimed_complement_commitment = ristretto::subtract_ristretto(
164 &max_fee_basis_points_commitment,
165 &commitment_to_ristretto(claimed_commitment),
166 )
167 .ok_or(TokenProofExtractionError::CurveArithmetic)?;
168
169 let expected_commitments = [
170 bytes_of(new_source_commitment),
171 bytes_of(&transfer_amount_commitment_lo),
172 bytes_of(&transfer_amount_commitment_hi),
173 bytes_of(claimed_commitment),
174 bytes_of(&claimed_complement_commitment),
175 bytes_of(&fee_commitment_lo),
176 bytes_of(&fee_commitment_hi),
177 ];
178
179 if !range_proof_commitments
180 .iter()
181 .zip(expected_commitments.into_iter())
182 .all(|(proof_commitment, expected_commitment)| {
183 bytes_of(proof_commitment) == expected_commitment
184 })
185 {
186 return Err(TokenProofExtractionError::PedersenCommitmentMismatch);
187 }
188
189 let expected_bit_lengths = [
191 REMAINING_BALANCE_BIT_LENGTH,
192 TRANSFER_AMOUNT_LO_BIT_LENGTH,
193 TRANSFER_AMOUNT_HI_BIT_LENGTH,
194 DELTA_BIT_LENGTH,
195 DELTA_BIT_LENGTH,
196 FEE_AMOUNT_LO_BIT_LENGTH,
197 FEE_AMOUNT_HI_BIT_LENGTH,
198 ]
199 .iter();
200
201 if !range_proof_bit_lengths
202 .iter()
203 .zip(expected_bit_lengths)
204 .all(|(proof_len, expected_len)| proof_len == expected_len)
205 {
206 return Err(TokenProofExtractionError::RangeProofLengthMismatch);
207 }
208
209 let sigma_proof_fee_commitment_point: PodRistrettoPoint =
211 commitment_to_ristretto(fee_commitment);
212 let validity_proof_fee_point = combine_lo_hi_pedersen_points(
213 &commitment_to_ristretto(&fee_commitment_lo),
214 &commitment_to_ristretto(&fee_commitment_hi),
215 )
216 .ok_or(TokenProofExtractionError::CurveArithmetic)?;
217
218 if source_pubkey_from_equality_proof != source_pubkey_from_validity_proof {
219 return Err(TokenProofExtractionError::ElGamalPubkeyMismatch);
220 }
221
222 if validity_proof_fee_point != sigma_proof_fee_commitment_point {
223 return Err(TokenProofExtractionError::FeeParametersMismatch);
224 }
225
226 verify_delta_commitment(
227 &transfer_amount_commitment_lo,
228 &transfer_amount_commitment_hi,
229 fee_commitment,
230 delta_commitment,
231 expected_fee_rate_basis_points,
232 )?;
233
234 let transfer_with_fee_pubkeys = TransferWithFeePubkeys {
236 source: *source_pubkey_from_equality_proof,
237 destination: *destination_pubkey,
238 auditor: *auditor_pubkey,
239 withdraw_withheld_authority: *withdraw_withheld_authority_pubkey,
240 };
241
242 Ok(Self {
243 ciphertext_lo: PodTransferAmountCiphertext(*transfer_amount_ciphertext_lo),
244 ciphertext_hi: PodTransferAmountCiphertext(*transfer_amount_ciphertext_hi),
245 transfer_with_fee_pubkeys,
246 new_source_ciphertext: *new_source_ciphertext,
247 fee_ciphertext_lo: PodFeeCiphertext(*fee_ciphertext_lo),
248 fee_ciphertext_hi: PodFeeCiphertext(*fee_ciphertext_hi),
249 })
250 }
251}
252
253const G: PodRistrettoPoint = PodRistrettoPoint([
255 226, 242, 174, 10, 106, 188, 78, 113, 168, 132, 169, 97, 197, 0, 81, 95, 88, 227, 11, 106, 165,
256 130, 221, 141, 182, 166, 89, 69, 224, 141, 45, 118,
257]);
258
259fn u64_to_scalar(amount: u64) -> PodScalar {
261 let mut bytes = [0u8; 32];
262 bytes[..8].copy_from_slice(&amount.to_le_bytes());
263 PodScalar(bytes)
264}
265
266fn u16_to_scalar(amount: u16) -> PodScalar {
268 let mut bytes = [0u8; 32];
269 bytes[..2].copy_from_slice(&amount.to_le_bytes());
270 PodScalar(bytes)
271}
272
273fn commitment_to_ristretto(commitment: &PodPedersenCommitment) -> PodRistrettoPoint {
274 let mut bytes = [0u8; 32];
275 bytes.copy_from_slice(bytes_of(commitment));
276 PodRistrettoPoint(bytes)
277}
278
279fn combine_lo_hi_pedersen_points(
281 point_lo: &PodRistrettoPoint,
282 point_hi: &PodRistrettoPoint,
283) -> Option<PodRistrettoPoint> {
284 const SCALING_CONSTANT: u64 = 65536;
285 let scaling_constant_scalar = u64_to_scalar(SCALING_CONSTANT);
286 let scaled_point_hi = ristretto::multiply_ristretto(&scaling_constant_scalar, point_hi)?;
287 ristretto::add_ristretto(point_lo, &scaled_point_hi)
288}
289
290fn verify_delta_commitment(
292 transfer_amount_commitment_lo: &PodPedersenCommitment,
293 transfer_amount_commitment_hi: &PodPedersenCommitment,
294 fee_commitment: &PodPedersenCommitment,
295 proof_delta_commitment: &PodPedersenCommitment,
296 transfer_fee_basis_points: u16,
297) -> Result<(), TokenProofExtractionError> {
298 let transfer_amount_point = combine_lo_hi_pedersen_points(
299 &commitment_to_ristretto(transfer_amount_commitment_lo),
300 &commitment_to_ristretto(transfer_amount_commitment_hi),
301 )
302 .ok_or(TokenProofExtractionError::CurveArithmetic)?;
303 let transfer_fee_basis_points_scalar = u16_to_scalar(transfer_fee_basis_points);
304 let scaled_transfer_amount_point =
305 ristretto::multiply_ristretto(&transfer_fee_basis_points_scalar, &transfer_amount_point)
306 .ok_or(TokenProofExtractionError::CurveArithmetic)?;
307
308 let max_fee_basis_points_scalar = u64_to_scalar(MAX_FEE_BASIS_POINTS);
309 let fee_point: PodRistrettoPoint = commitment_to_ristretto(fee_commitment);
310 let scaled_fee_point = ristretto::multiply_ristretto(&max_fee_basis_points_scalar, &fee_point)
311 .ok_or(TokenProofExtractionError::CurveArithmetic)?;
312
313 let expected_delta_commitment_point =
314 ristretto::subtract_ristretto(&scaled_fee_point, &scaled_transfer_amount_point)
315 .ok_or(TokenProofExtractionError::CurveArithmetic)?;
316
317 let proof_delta_commitment_point = commitment_to_ristretto(proof_delta_commitment);
318 if expected_delta_commitment_point != proof_delta_commitment_point {
319 return Err(TokenProofExtractionError::CurveArithmetic);
320 }
321 Ok(())
322}