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

Commit ad35e70

Browse files
committed
add transfer with fee proof extraction
1 parent 09195d9 commit ad35e70

File tree

6 files changed

+340
-1
lines changed

6 files changed

+340
-1
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

token/confidential-transfer/proof-extraction/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ license = "Apache-2.0"
88
edition = "2021"
99

1010
[dependencies]
11+
bytemuck = "1.16.1"
12+
solana-curve25519 = "2.0.0"
1113
solana-zk-sdk = "2.0.0"
1214
thiserror = "1.0.61"
1315

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1-
use solana_zk_sdk::encryption::pod::grouped_elgamal::PodGroupedElGamalCiphertext3Handles;
1+
use solana_zk_sdk::encryption::pod::grouped_elgamal::{
2+
PodGroupedElGamalCiphertext2Handles, PodGroupedElGamalCiphertext3Handles,
3+
};
24

35
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
46
#[repr(C)]
57
pub struct PodTransferAmountCiphertext(pub(crate) PodGroupedElGamalCiphertext3Handles);
8+
9+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
10+
#[repr(C)]
11+
pub struct PodFeeCiphertext(pub(crate) PodGroupedElGamalCiphertext2Handles);

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,8 @@ pub enum TokenProofExtractionError {
88
PedersenCommitmentMismatch,
99
#[error("Range proof length mismatch")]
1010
RangeProofLengthMismatch,
11+
#[error("Fee pparameters mismatch")]
12+
FeeParametersMismatch,
13+
#[error("Curve arithmetic failed")]
14+
CurveArithmetic,
1115
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pub mod encryption;
22
pub mod errors;
33
pub mod transfer;
4+
pub mod transfer_with_fee;
Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
use {
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+
24+
const MAX_FEE_BASIS_POINTS: u64 = 10_000;
25+
26+
/// The transfer public keys associated with a transfer with fee.
27+
pub struct TransferWithFeePubkeys {
28+
/// Source ElGamal public key
29+
pub source: PodElGamalPubkey,
30+
/// Destination ElGamal public key
31+
pub destination: PodElGamalPubkey,
32+
/// Auditor ElGamal public key
33+
pub auditor: PodElGamalPubkey,
34+
/// Withdraw withheld authority public key
35+
pub withdraw_withheld_authority: PodElGamalPubkey,
36+
}
37+
38+
/// The proof context information needed to process a [Transfer] instruction
39+
/// with fee.
40+
pub struct TransferWithFeeProofContext {
41+
/// Group encryption of the low 16 bits of the transfer amount
42+
pub ciphertext_lo: PodTransferAmountCiphertext,
43+
/// Group encryption of the high 48 bits of the transfer amount
44+
pub ciphertext_hi: PodTransferAmountCiphertext,
45+
/// The public encryption keys associated with the transfer: source, dest,
46+
/// auditor, and withdraw withheld authority
47+
pub transfer_with_fee_pubkeys: TransferWithFeePubkeys,
48+
/// The final spendable ciphertext after the transfer,
49+
pub new_source_ciphertext: PodElGamalCiphertext,
50+
/// The transfer fee encryption of the low 16 bits of the transfer fee
51+
/// amount
52+
pub fee_ciphertext_lo: PodFeeCiphertext,
53+
/// The transfer fee encryption of the hi 32 bits of the transfer fee amount
54+
pub fee_ciphertext_hi: PodFeeCiphertext,
55+
}
56+
57+
impl TransferWithFeeProofContext {
58+
pub fn verify_and_extract(
59+
equality_proof_context: &CiphertextCommitmentEqualityProofContext,
60+
transfer_amount_ciphertext_validity_proof_context: &BatchedGroupedCiphertext3HandlesValidityProofContext,
61+
fee_sigma_proof_context: &PercentageWithCapProofContext,
62+
fee_ciphertext_validity_proof_context: &BatchedGroupedCiphertext2HandlesValidityProofContext,
63+
range_proof_context: &BatchedRangeProofContext,
64+
expected_fee_rate_basis_points: u16,
65+
expected_maximum_fee: u64,
66+
) -> Result<Self, TokenProofExtractionError> {
67+
// The equality proof context consists of the source ElGamal public key, the new
68+
// source available balance ciphertext, and the new source available
69+
// commitment. The public key and ciphertext should be returned as part
70+
// of `TransferWithFeeProofContextInfo` and the commitment should be
71+
// checked with range proof for consistency.
72+
let CiphertextCommitmentEqualityProofContext {
73+
pubkey: source_pubkey_from_equality_proof,
74+
ciphertext: new_source_ciphertext,
75+
commitment: new_source_commitment,
76+
} = equality_proof_context;
77+
78+
// The transfer amount ciphertext validity proof context consists of the
79+
// destination ElGamal public key, auditor ElGamal public key, and the
80+
// transfer amount ciphertexts. All of these fields should be returned
81+
// as part of `TransferWithFeeProofContextInfo`. In addition, the
82+
// commitments pertaining to the transfer amount ciphertexts should be
83+
// checked with range proof for consistency.
84+
let BatchedGroupedCiphertext3HandlesValidityProofContext {
85+
first_pubkey: source_pubkey_from_validity_proof,
86+
second_pubkey: destination_pubkey,
87+
third_pubkey: auditor_pubkey,
88+
grouped_ciphertext_lo: transfer_amount_ciphertext_lo,
89+
grouped_ciphertext_hi: transfer_amount_ciphertext_hi,
90+
} = transfer_amount_ciphertext_validity_proof_context;
91+
92+
// The fee sigma proof context consists of the fee commitment, delta commitment,
93+
// claimed commitment, and max fee. The fee and claimed commitment
94+
// should be checked with range proof for consistency. The delta
95+
// commitment should be checked whether it is properly generated with
96+
// respect to the fee parameters. The max fee should be checked for
97+
// consistency with the fee parameters.
98+
let PercentageWithCapProofContext {
99+
percentage_commitment: fee_commitment,
100+
delta_commitment,
101+
claimed_commitment,
102+
max_value: proof_maximum_fee,
103+
} = fee_sigma_proof_context;
104+
105+
let proof_maximum_fee: u64 = (*proof_maximum_fee).into();
106+
if expected_maximum_fee != proof_maximum_fee {
107+
return Err(TokenProofExtractionError::FeeParametersMismatch);
108+
}
109+
110+
// The transfer fee ciphertext validity proof context consists of the
111+
// destination ElGamal public key, withdraw withheld authority ElGamal
112+
// public key, and the transfer fee ciphertexts. The rest of the fields
113+
// should be return as part of `TransferWithFeeProofContextInfo`. In
114+
// addition, the destination public key should be checked for
115+
// consistency with the destination public key contained in the transfer amount
116+
// ciphertext validity proof, and the commitments pertaining to the transfer fee
117+
// amount ciphertexts should be checked with range proof for
118+
// consistency.
119+
let BatchedGroupedCiphertext2HandlesValidityProofContext {
120+
first_pubkey: destination_pubkey_from_transfer_fee_validity_proof,
121+
second_pubkey: withdraw_withheld_authority_pubkey,
122+
grouped_ciphertext_lo: fee_ciphertext_lo,
123+
grouped_ciphertext_hi: fee_ciphertext_hi,
124+
} = fee_ciphertext_validity_proof_context;
125+
126+
if destination_pubkey != destination_pubkey_from_transfer_fee_validity_proof {
127+
return Err(TokenProofExtractionError::ElGamalPubkeyMismatch);
128+
}
129+
130+
// The range proof context consists of the Pedersen commitments and bit-lengths
131+
// for which the range proof is proved. The commitments must consist of
132+
// seven commitments pertaining to
133+
// - the new source available balance (64 bits)
134+
// - the low bits of the transfer amount (16 bits)
135+
// - the high bits of the transfer amount (32 bits)
136+
// - the delta amount for the fee (48 bits)
137+
// - the complement of the delta amount for the fee (48 bits)
138+
// - the low bits of the fee amount (16 bits)
139+
// - the high bits of the fee amount (32 bits)
140+
let BatchedRangeProofContext {
141+
commitments: range_proof_commitments,
142+
bit_lengths: range_proof_bit_lengths,
143+
} = range_proof_context;
144+
145+
// check that the range proof was created for the correct set of Pedersen
146+
// commitments
147+
let transfer_amount_commitment_lo = transfer_amount_ciphertext_lo.extract_commitment();
148+
let transfer_amount_commitment_hi = transfer_amount_ciphertext_hi.extract_commitment();
149+
150+
let fee_commitment_lo = fee_ciphertext_lo.extract_commitment();
151+
let fee_commitment_hi = fee_ciphertext_hi.extract_commitment();
152+
153+
let max_fee_basis_points_scalar = u64_to_scalar(MAX_FEE_BASIS_POINTS);
154+
let max_fee_basis_points_commitment =
155+
ristretto::multiply_ristretto(&max_fee_basis_points_scalar, &G)
156+
.ok_or(TokenProofExtractionError::CurveArithmetic)?;
157+
let claimed_complement_commitment = ristretto::subtract_ristretto(
158+
&max_fee_basis_points_commitment,
159+
&commitment_to_ristretto(claimed_commitment),
160+
)
161+
.ok_or(TokenProofExtractionError::CurveArithmetic)?;
162+
163+
let expected_commitments = [
164+
bytes_of(new_source_commitment),
165+
bytes_of(&transfer_amount_commitment_lo),
166+
bytes_of(&transfer_amount_commitment_hi),
167+
bytes_of(claimed_commitment),
168+
bytes_of(&claimed_complement_commitment),
169+
bytes_of(&fee_commitment_lo),
170+
bytes_of(&fee_commitment_hi),
171+
];
172+
173+
if !range_proof_commitments
174+
.iter()
175+
.zip(expected_commitments.into_iter())
176+
.all(|(proof_commitment, expected_commitment)| {
177+
bytes_of(proof_commitment) == expected_commitment
178+
})
179+
{
180+
return Err(TokenProofExtractionError::PedersenCommitmentMismatch);
181+
}
182+
183+
// check that the range proof was created for the correct number of bits
184+
const REMAINING_BALANCE_BIT_LENGTH: u8 = 64;
185+
const TRANSFER_AMOUNT_LO_BIT_LENGTH: u8 = 16;
186+
const TRANSFER_AMOUNT_HI_BIT_LENGTH: u8 = 32;
187+
const DELTA_BIT_LENGTH: u8 = 48;
188+
const FEE_AMOUNT_LO_BIT_LENGTH: u8 = 16;
189+
const FEE_AMOUNT_HI_BIT_LENGTH: u8 = 32;
190+
191+
let expected_bit_lengths = [
192+
REMAINING_BALANCE_BIT_LENGTH,
193+
TRANSFER_AMOUNT_LO_BIT_LENGTH,
194+
TRANSFER_AMOUNT_HI_BIT_LENGTH,
195+
DELTA_BIT_LENGTH,
196+
DELTA_BIT_LENGTH,
197+
FEE_AMOUNT_LO_BIT_LENGTH,
198+
FEE_AMOUNT_HI_BIT_LENGTH,
199+
]
200+
.iter();
201+
202+
if !range_proof_bit_lengths
203+
.iter()
204+
.zip(expected_bit_lengths)
205+
.all(|(proof_len, expected_len)| proof_len == expected_len)
206+
{
207+
return Err(TokenProofExtractionError::RangeProofLengthMismatch);
208+
}
209+
210+
// check consistency between fee sigma and fee ciphertext validity proofs
211+
let sigma_proof_fee_commitment_point: PodRistrettoPoint =
212+
commitment_to_ristretto(fee_commitment);
213+
let validity_proof_fee_point = combine_lo_hi_pedersen_points(
214+
&commitment_to_ristretto(&fee_commitment_lo),
215+
&commitment_to_ristretto(&fee_commitment_hi),
216+
)
217+
.ok_or(TokenProofExtractionError::CurveArithmetic)?;
218+
219+
if source_pubkey_from_equality_proof != source_pubkey_from_validity_proof {
220+
return Err(TokenProofExtractionError::ElGamalPubkeyMismatch);
221+
}
222+
223+
if validity_proof_fee_point != sigma_proof_fee_commitment_point {
224+
return Err(TokenProofExtractionError::FeeParametersMismatch);
225+
}
226+
227+
verify_delta_commitment(
228+
&transfer_amount_commitment_lo,
229+
&transfer_amount_commitment_hi,
230+
fee_commitment,
231+
delta_commitment,
232+
expected_fee_rate_basis_points,
233+
)?;
234+
235+
// create transfer with fee proof context info and return
236+
let transfer_with_fee_pubkeys = TransferWithFeePubkeys {
237+
source: *source_pubkey_from_equality_proof,
238+
destination: *destination_pubkey,
239+
auditor: *auditor_pubkey,
240+
withdraw_withheld_authority: *withdraw_withheld_authority_pubkey,
241+
};
242+
243+
Ok(Self {
244+
ciphertext_lo: PodTransferAmountCiphertext(*transfer_amount_ciphertext_lo),
245+
ciphertext_hi: PodTransferAmountCiphertext(*transfer_amount_ciphertext_hi),
246+
transfer_with_fee_pubkeys,
247+
new_source_ciphertext: *new_source_ciphertext,
248+
fee_ciphertext_lo: PodFeeCiphertext(*fee_ciphertext_lo),
249+
fee_ciphertext_hi: PodFeeCiphertext(*fee_ciphertext_hi),
250+
})
251+
}
252+
}
253+
254+
/// Ristretto generator point for curve25519
255+
const G: PodRistrettoPoint = PodRistrettoPoint([
256+
226, 242, 174, 10, 106, 188, 78, 113, 168, 132, 169, 97, 197, 0, 81, 95, 88, 227, 11, 106, 165,
257+
130, 221, 141, 182, 166, 89, 69, 224, 141, 45, 118,
258+
]);
259+
260+
/// Convert a `u64` amount into a curve25519 scalar
261+
fn u64_to_scalar(amount: u64) -> PodScalar {
262+
let mut bytes = [0u8; 32];
263+
bytes[..8].copy_from_slice(&amount.to_le_bytes());
264+
PodScalar(bytes)
265+
}
266+
267+
/// Convert a `u16` amount into a curve25519 scalar
268+
fn u16_to_scalar(amount: u16) -> PodScalar {
269+
let mut bytes = [0u8; 32];
270+
bytes[..2].copy_from_slice(&amount.to_le_bytes());
271+
PodScalar(bytes)
272+
}
273+
274+
fn commitment_to_ristretto(commitment: &PodPedersenCommitment) -> PodRistrettoPoint {
275+
let mut bytes = [0u8; 32];
276+
bytes.copy_from_slice(bytes_of(commitment));
277+
PodRistrettoPoint(bytes)
278+
}
279+
280+
/// Combine lo and hi Pedersen commitment points
281+
fn combine_lo_hi_pedersen_points(
282+
point_lo: &PodRistrettoPoint,
283+
point_hi: &PodRistrettoPoint,
284+
) -> Option<PodRistrettoPoint> {
285+
const SCALING_CONSTANT: u64 = 65536;
286+
let scaling_constant_scalar = u64_to_scalar(SCALING_CONSTANT);
287+
let scaled_point_hi = ristretto::multiply_ristretto(&scaling_constant_scalar, point_hi)?;
288+
ristretto::add_ristretto(point_lo, &scaled_point_hi)
289+
}
290+
291+
/// Compute fee delta commitment
292+
fn verify_delta_commitment(
293+
transfer_amount_commitment_lo: &PodPedersenCommitment,
294+
transfer_amount_commitment_hi: &PodPedersenCommitment,
295+
fee_commitment: &PodPedersenCommitment,
296+
proof_delta_commitment: &PodPedersenCommitment,
297+
transfer_fee_basis_points: u16,
298+
) -> Result<(), TokenProofExtractionError> {
299+
let transfer_amount_point = combine_lo_hi_pedersen_points(
300+
&commitment_to_ristretto(transfer_amount_commitment_lo),
301+
&commitment_to_ristretto(transfer_amount_commitment_hi),
302+
)
303+
.ok_or(TokenProofExtractionError::CurveArithmetic)?;
304+
let transfer_fee_basis_points_scalar = u16_to_scalar(transfer_fee_basis_points);
305+
let scaled_transfer_amount_point =
306+
ristretto::multiply_ristretto(&transfer_fee_basis_points_scalar, &transfer_amount_point)
307+
.ok_or(TokenProofExtractionError::CurveArithmetic)?;
308+
309+
const MAX_FEE_BASIS_POINTS: u64 = 10_000;
310+
let max_fee_basis_points_scalar = u64_to_scalar(MAX_FEE_BASIS_POINTS);
311+
let fee_point: PodRistrettoPoint = commitment_to_ristretto(fee_commitment);
312+
let scaled_fee_point = ristretto::multiply_ristretto(&max_fee_basis_points_scalar, &fee_point)
313+
.ok_or(TokenProofExtractionError::CurveArithmetic)?;
314+
315+
let expected_delta_commitment_point =
316+
ristretto::subtract_ristretto(&scaled_fee_point, &scaled_transfer_amount_point)
317+
.ok_or(TokenProofExtractionError::CurveArithmetic)?;
318+
319+
let proof_delta_commitment_point = commitment_to_ristretto(proof_delta_commitment);
320+
if expected_delta_commitment_point != proof_delta_commitment_point {
321+
return Err(TokenProofExtractionError::CurveArithmetic);
322+
}
323+
Ok(())
324+
}

0 commit comments

Comments
 (0)