Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit b98c896

Browse files
committed
add transfer with fee proof generation
1 parent a898de1 commit b98c896

File tree

4 files changed

+375
-0
lines changed

4 files changed

+375
-0
lines changed

token/confidential-transfer/proof-generation/src/encryption.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,42 @@ impl TransferAmountCiphertext {
4747
self.0.handles.get(2).unwrap()
4848
}
4949
}
50+
51+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
52+
#[repr(C)]
53+
#[cfg(not(target_os = "solana"))]
54+
pub struct FeeCiphertext(pub(crate) GroupedElGamalCiphertext<2>);
55+
56+
#[cfg(not(target_os = "solana"))]
57+
impl FeeCiphertext {
58+
pub fn new(
59+
amount: u64,
60+
destination_pubkey: &ElGamalPubkey,
61+
withdraw_withheld_authority_pubkey: &ElGamalPubkey,
62+
) -> (Self, PedersenOpening) {
63+
let opening = PedersenOpening::new_rand();
64+
let grouped_ciphertext = GroupedElGamal::<2>::encrypt_with(
65+
[destination_pubkey, withdraw_withheld_authority_pubkey],
66+
amount,
67+
&opening,
68+
);
69+
70+
(Self(grouped_ciphertext), opening)
71+
}
72+
73+
pub fn get_commitment(&self) -> &PedersenCommitment {
74+
&self.0.commitment
75+
}
76+
77+
pub fn get_destination_handle(&self) -> &DecryptHandle {
78+
// `FeeEncryption` is a wrapper for `GroupedElGamalCiphertext<2>`, which holds
79+
// exactly two decryption handles.
80+
self.0.handles.first().unwrap()
81+
}
82+
83+
pub fn get_withdraw_withheld_authority_handle(&self) -> &DecryptHandle {
84+
// `FeeEncryption` is a wrapper for `GroupedElGamalCiphertext<2>`, which holds
85+
// exactly two decryption handles.
86+
self.0.handles.get(1).unwrap()
87+
}
88+
}

token/confidential-transfer/proof-generation/src/errors.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@ pub enum TokenProofGenerationError {
88
NotEnoughFunds,
99
#[error("illegal amount bit length")]
1010
IllegalAmountBitLength,
11+
#[error("fee calculation failed")]
12+
FeeCalculation,
1113
}

token/confidential-transfer/proof-generation/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use {
99
pub mod encryption;
1010
pub mod errors;
1111
pub mod transfer;
12+
pub mod transfer_with_fee;
1213

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

0 commit comments

Comments
 (0)