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

Commit fbd7546

Browse files
[confidential-transfer] Add transfer with fee proof generation and extraction (#6945)
* add transfer with fee proof generation * add transfer with fee proof extraction * add test cases * cargo fmt * cargo clippy * add `solana-curve25519` to `patch.crates-io.sh` * Apply suggestions from code review Co-authored-by: Jon C <[email protected]> * add tests for large numbers * re-organize constants --------- Co-authored-by: Jon C <[email protected]>
1 parent 7d24772 commit fbd7546

File tree

12 files changed

+816
-10
lines changed

12 files changed

+816
-10
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.

patch.crates-io.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ crates_map+=("solana-udp-client udp-client")
111111
crates_map+=("solana-version version")
112112
crates_map+=("solana-zk-token-sdk zk-token-sdk")
113113
crates_map+=("solana-zk-sdk zk-sdk")
114+
crates_map+=("solana-curve25519 curves/curve25519")
114115

115116
patch_crates=()
116117
for map_entry in "${crates_map[@]}"; do

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.62"
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 parameters 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: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
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+
const REMAINING_BALANCE_BIT_LENGTH: u8 = 64;
26+
const TRANSFER_AMOUNT_LO_BIT_LENGTH: u8 = 16;
27+
const TRANSFER_AMOUNT_HI_BIT_LENGTH: u8 = 32;
28+
const DELTA_BIT_LENGTH: u8 = 48;
29+
const FEE_AMOUNT_LO_BIT_LENGTH: u8 = 16;
30+
const FEE_AMOUNT_HI_BIT_LENGTH: u8 = 32;
31+
32+
/// The transfer public keys associated with a transfer with fee.
33+
pub struct TransferWithFeePubkeys {
34+
/// Source ElGamal public key
35+
pub source: PodElGamalPubkey,
36+
/// Destination ElGamal public key
37+
pub destination: PodElGamalPubkey,
38+
/// Auditor ElGamal public key
39+
pub auditor: PodElGamalPubkey,
40+
/// Withdraw withheld authority public key
41+
pub withdraw_withheld_authority: PodElGamalPubkey,
42+
}
43+
44+
/// The proof context information needed to process a [Transfer] instruction
45+
/// with fee.
46+
pub struct TransferWithFeeProofContext {
47+
/// Group encryption of the low 16 bits of the transfer amount
48+
pub ciphertext_lo: PodTransferAmountCiphertext,
49+
/// Group encryption of the high 48 bits of the transfer amount
50+
pub ciphertext_hi: PodTransferAmountCiphertext,
51+
/// The public encryption keys associated with the transfer: source, dest,
52+
/// auditor, and withdraw withheld authority
53+
pub transfer_with_fee_pubkeys: TransferWithFeePubkeys,
54+
/// The final spendable ciphertext after the transfer,
55+
pub new_source_ciphertext: PodElGamalCiphertext,
56+
/// The transfer fee encryption of the low 16 bits of the transfer fee
57+
/// amount
58+
pub fee_ciphertext_lo: PodFeeCiphertext,
59+
/// The transfer fee encryption of the hi 32 bits of the transfer fee amount
60+
pub fee_ciphertext_hi: PodFeeCiphertext,
61+
}
62+
63+
impl 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+
// The equality proof context consists of the source ElGamal public key, the new
74+
// source available balance ciphertext, and the new source available
75+
// commitment. The public key and ciphertext should be returned as part
76+
// of `TransferWithFeeProofContextInfo` and the commitment should be
77+
// checked with range proof for consistency.
78+
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+
// The transfer amount ciphertext validity proof context consists of the
85+
// destination ElGamal public key, auditor ElGamal public key, and the
86+
// transfer amount ciphertexts. All of these fields should be returned
87+
// as part of `TransferWithFeeProofContextInfo`. In addition, the
88+
// commitments pertaining to the transfer amount ciphertexts should be
89+
// checked with range proof for consistency.
90+
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+
// The fee sigma proof context consists of the fee commitment, delta commitment,
99+
// claimed commitment, and max fee. The fee and claimed commitment
100+
// should be checked with range proof for consistency. The delta
101+
// commitment should be checked whether it is properly generated with
102+
// respect to the fee parameters. The max fee should be checked for
103+
// consistency with the fee parameters.
104+
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+
// The transfer fee ciphertext validity proof context consists of the
117+
// destination ElGamal public key, withdraw withheld authority ElGamal
118+
// public key, and the transfer fee ciphertexts. The rest of the fields
119+
// should be return as part of `TransferWithFeeProofContextInfo`. In
120+
// addition, the destination public key should be checked for
121+
// consistency with the destination public key contained in the transfer amount
122+
// ciphertext validity proof, and the commitments pertaining to the transfer fee
123+
// amount ciphertexts should be checked with range proof for
124+
// consistency.
125+
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+
// The range proof context consists of the Pedersen commitments and bit-lengths
137+
// for which the range proof is proved. The commitments must consist of
138+
// seven commitments pertaining to
139+
// - the new source available balance (64 bits)
140+
// - the low bits of the transfer amount (16 bits)
141+
// - the high bits of the transfer amount (32 bits)
142+
// - the delta amount for the fee (48 bits)
143+
// - the complement of the delta amount for the fee (48 bits)
144+
// - the low bits of the fee amount (16 bits)
145+
// - the high bits of the fee amount (32 bits)
146+
let BatchedRangeProofContext {
147+
commitments: range_proof_commitments,
148+
bit_lengths: range_proof_bit_lengths,
149+
} = range_proof_context;
150+
151+
// check that the range proof was created for the correct set of Pedersen
152+
// commitments
153+
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+
// check that the range proof was created for the correct number of bits
190+
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+
// check consistency between fee sigma and fee ciphertext validity proofs
210+
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+
// create transfer with fee proof context info and return
235+
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+
253+
/// Ristretto generator point for curve25519
254+
const 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+
259+
/// Convert a `u64` amount into a curve25519 scalar
260+
fn 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+
266+
/// Convert a `u16` amount into a curve25519 scalar
267+
fn 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+
273+
fn 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+
279+
/// Combine lo and hi Pedersen commitment points
280+
fn 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+
290+
/// Compute fee delta commitment
291+
fn 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+
}

0 commit comments

Comments
 (0)