Skip to content

Commit 6f24733

Browse files
[confidential-transfer] Add docs and additional comments in the proof generation logic (#638)
1 parent bdfd809 commit 6f24733

File tree

9 files changed

+223
-12
lines changed

9 files changed

+223
-12
lines changed

confidential-transfer/proof-extraction/src/burn.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ impl BurnProofContext {
8383
*remaining_balance_commitment,
8484
burn_amount_commitment_lo,
8585
burn_amount_commitment_hi,
86+
// we don't care about the padding commitment, so ignore it
8687
];
8788

8889
// range proof context always contains 8 commitments and therefore,

confidential-transfer/proof-extraction/src/mint.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ impl MintProofContext {
8383
*new_supply_commitment,
8484
mint_amount_commitment_lo,
8585
mint_amount_commitment_hi,
86+
// we don't care about the padding commitment, so ignore it
8687
];
8788

8889
// range proof context always contains 8 commitments and therefore,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ impl TransferProofContext {
8888
*new_source_commitment,
8989
transfer_amount_commitment_lo,
9090
transfer_amount_commitment_hi,
91+
// we don't care about the padding commitment, so ignore it
9192
];
9293

9394
// range proof context always contains 8 commitments and therefore,

confidential-transfer/proof-generation/src/burn.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,39 @@
1+
//! Generates the zero-knowledge proofs required for a confidential burn.
2+
//!
3+
//! A confidential burn operation removes tokens from a user's confidential balance and decreases the
4+
//! token's total supply. This process requires three distinct zero-knowledge proofs to ensure the
5+
//! operation is valid, the user is solvent, and the token supply is updated correctly.
6+
//!
7+
//! ## Protocol Flow and Proof Components
8+
//!
9+
//! 1. **Encrypt Burn Amount**: The burn amount is encrypted in a grouped (twisted) ElGamal
10+
//! ciphertext. This single operation prepares the `burn_amount` to be simultaneously
11+
//! subtracted from the user's account and recorded in the mint's `pending_burn` accumulator,
12+
//! which will later be subtracted from the total supply.
13+
//!
14+
//! 2. **Homomorphic Calculation**: The client homomorphically computes their new encrypted balance
15+
//! by subtracting the source-encrypted component of the `burn_amount` from their current
16+
//! `available_balance` ciphertext.
17+
//!
18+
//! 3. **Generate Proofs**: The user generates three proofs:
19+
//!
20+
//! - **Batched Grouped Ciphertext Validity Proof**:
21+
//! This proof certifies that the grouped ElGamal ciphertext for the `burn_amount` is well-formed
22+
//! and was correctly encrypted for the source, supply, and auditor public keys.
23+
//!
24+
//! - **Ciphertext-Commitment Equality Proof**:
25+
//! This proof provides the cryptographic link needed for the solvency check. After the user's
26+
//! new balance is computed homomorphically, the prover no longer knows the associated
27+
//! Pedersen opening. To perform a range proof, the prover creates a *new* Pedersen commitment
28+
//! for their `remaining_balance` (for which they know the opening) and uses this proof to
29+
//! certify that the ciphertext and the new commitment hide the same value.
30+
//!
31+
//! - **Range Proof (`BatchedRangeProofU128`)**:
32+
//! This proof is the core solvency check. It certifies that the user's `remaining_balance`
33+
//! is non-negative (i.e., in the range `[0, 2^64)`), which makes it cryptographically
34+
//! impossible to burn more tokens than one possesses. It also proves the `burn_amount` itself
35+
//! is a valid 48-bit number.
36+
137
#[cfg(target_arch = "wasm32")]
238
use solana_zk_sdk::encryption::grouped_elgamal::GroupedElGamalCiphertext3Handles;
339
use {
@@ -158,6 +194,10 @@ pub fn burn_split_proof_data(
158194
};
159195

160196
// generate range proof data
197+
198+
// the total bit lengths for the range proof must be a power-of-2
199+
// therefore, create a Pedersen commitment to 0 and use it as a dummy commitment to a 16-bit
200+
// value
161201
let (padding_commitment, padding_opening) = Pedersen::new(0_u64);
162202
let range_proof_data = BatchedRangeProofU128Data::new(
163203
vec![

confidential-transfer/proof-generation/src/mint.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,39 @@
1+
//! Generates the zero-knowledge proofs required for a confidential mint.
2+
//!
3+
//! A confidential mint operation increases the total supply of a token and deposits the newly
4+
//! created tokens into a user's confidential account. This process requires three distinct
5+
//! zero-knowledge proofs:
6+
//!
7+
//! ## Protocol Flow and Proof Components
8+
//!
9+
//! 1. **Encrypt Mint Amount**: The mint amount is encrypted in a grouped (twisted) ElGamal
10+
//! ciphertext. This single cryptographic operation prepares the mint amount to be simultaneously
11+
//! added to the destination account's balance and the mint's total supply. It also includes an
12+
//! encryption for an optional auditor.
13+
//!
14+
//! 2. **Homomorphic Calculation**: The client homomorphically computes the new encrypted total supply
15+
//! by adding the supply-encrypted component of the mint amount to the mint's current
16+
//! `confidential_supply` ciphertext.
17+
//!
18+
//! 3. **Generate Proofs**: The user with mint authority generates three proofs:
19+
//!
20+
//! - **Batched Grouped Ciphertext Validity Proof**:
21+
//! This proof certifies that the grouped ElGamal ciphertext for the `mint_amount` is well-formed
22+
//! and was correctly encrypted for the destination, supply, and auditor public keys.
23+
//!
24+
//! - **Ciphertext-Commitment Equality Proof**:
25+
//! This proof provides a cryptographic link that enables the total supply to be range-checked.
26+
//! When the `new_supply_ciphertext` is computed homomorphically, the prover no longer knows
27+
//! the associated Pedersen opening (randomness). To perform a range proof, the prover creates a
28+
//! *new* Pedersen commitment for the `new_supply` value (for which they know the opening) and
29+
//! uses this proof to certify that the ciphertext and the new commitment hide the same value.
30+
//!
31+
//! - **Range Proof (`BatchedRangeProofU128`)**:
32+
//! This proof ensures supply integrity. It certifies that the `mint_amount` is a valid
33+
//! 48-bit number and, crucially, that the `new_supply` does not exceed the 64-bit limit.
34+
//! This makes it cryptographically impossible to mint tokens in a way that would cause the
35+
//! total supply to overflow.
36+
137
#[cfg(target_arch = "wasm32")]
238
use solana_zk_sdk::encryption::grouped_elgamal::GroupedElGamalCiphertext3Handles;
339
use {
@@ -152,6 +188,10 @@ pub fn mint_split_proof_data(
152188
};
153189

154190
// generate range proof data
191+
192+
// the total bit lengths for the range proof must be a power-of-2
193+
// therefore, create a Pedersen commitment to 0 and use it as a dummy commitment to a 16-bit
194+
// value
155195
let (padding_commitment, padding_opening) = Pedersen::new(0_u64);
156196
let range_proof_data = BatchedRangeProofU128Data::new(
157197
vec![

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,44 @@
1+
//! Generates the zero-knowledge proofs required for a confidential transfer.
2+
//!
3+
//! A confidential transfer requires a composition of three distinct zero-knowledge proofs to ensure
4+
//! correctness and security. This function orchestrates their generation.
5+
//!
6+
//! ## Protocol Flow and Proof Components
7+
//!
8+
//! 1. **Encrypt Transfer Amount**: The transfer amount is split into low (16-bit) and high (32-bit)
9+
//! components. This is done to facilitate efficient decryption, which would otherwise require
10+
//! solving the discrete logarithm problem over the entire 64-bit range. Each component is
11+
//! encrypted as a grouped (twisted) ElGamal ciphertext with decryption handles for the source,
12+
//! destination, and an optional auditor.
13+
//!
14+
//! 2. **Generate Proofs**: The sender then generates the following proofs in a specific logical order:
15+
//!
16+
//! - **Ciphertext Validity Proof (`BatchedGroupedCiphertext3HandlesValidityProofData`)**:
17+
//! This proof certifies that the grouped ElGamal ciphertexts for the transfer amount are
18+
//! well-formed (i.e., they are valid encryptions of the low and high bit components under
19+
//! the source, destination, and auditor keys).
20+
//!
21+
//! - **Range Proof (`BatchedRangeProofU128Data`)**:
22+
//! This proof ensures solvency and prevents the creation of tokens. It certifies that:
23+
//! 1. The sender's remaining balance is a non-negative 64-bit integer. This is ensures
24+
//! that `current_balance >= transfer_amount`.
25+
//! 2. The low and high components of the transfer amount are valid 16-bit and 32-bit
26+
//! integers, respectively.
27+
//!
28+
//! A range proof can only be generated from a Pedersen commitment for which the prover
29+
//! knows the opening. However, a sender does not necessarily know the Pedersen opening
30+
//! for the ciphertext associated with the sender's remaining balance ciphertext. This
31+
//! this necessitates the ciphertext-commitment equality proof below.
32+
//!
33+
//! - **Ciphertext-Commitment Equality Proof (`CiphertextCommitmentEqualityProofData`)**:
34+
//! We require that the sender create a *new* Pedersen commitment to their known plaintext
35+
//! remaining balance. This equality proof then certifies that the homomorphically computed
36+
//! `new_balance_ciphertext` and the new Pedersen commitment encrypt/commit to the exact same
37+
//! value.
38+
//!
39+
//! These three proofs, when verified together, allow the on-chain program to securely process the
40+
//! confidential transfer.
41+
142
#[cfg(target_arch = "wasm32")]
243
use solana_zk_sdk::encryption::grouped_elgamal::GroupedElGamalCiphertext3Handles;
344
use {
@@ -163,6 +204,10 @@ pub fn transfer_split_proof_data(
163204
};
164205

165206
// generate range proof data
207+
208+
// the total bit lengths for the range proof must be a power-of-2
209+
// therefore, create a Pedersen commitment to 0 and use it as a dummy commitment to a 16-bit
210+
// value
166211
let (padding_commitment, padding_opening) = Pedersen::new(0_u64);
167212
let range_proof_data = BatchedRangeProofU128Data::new(
168213
vec![

confidential-transfer/proof-generation/src/transfer_with_fee.rs

Lines changed: 59 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,51 @@
1+
//! Generates the zero-knowledge proofs required for a confidential transfer with a fee.
2+
//!
3+
//! A confidential transfer with a fee is more complex than a simple transfer. It requires five
4+
//! distinct zero-knowledge proofs to ensure the validity of the transfer, the solvency of the
5+
//! sender, and the correctness of the fee amount according to the on-chain mint configuration.
6+
//!
7+
//! ## Protocol Flow and Proof Components
8+
//!
9+
//! 1. **Fee Calculation**: The client first calculates the required fee based on the transfer
10+
//! amount and the on-chain fee parameters (rate and maximum cap).
11+
//!
12+
//! 2. **Encrypt Amounts**: The gross transfer amount and the fee amount are each split into low
13+
//! and high bit components. These components are then encrypted into separate grouped (twisted)
14+
//! ElGamal ciphertexts with the appropriate decryption handles for the involved parties (source,
15+
//! destination, auditor, and withdraw authority).
16+
//!
17+
//! 3. **Generate Proofs**: The sender generates five proofs that work in concert:
18+
//!
19+
//! - **Transfer Amount Ciphertext Validity Proof
20+
//! (`BatchedGroupedCiphertext3HandlesValidityProofData`)**: Certifies that the grouped
21+
//! ElGamal ciphertext for the gross transfer amount is well-formed.
22+
//!
23+
//! - **Fee Ciphertext Validity Proof
24+
//! (`BatchedGroupedCiphertext2HandlesValidityProofData`)**: Certifies that the grouped
25+
//! ElGamal ciphertext for the transfer fee is well-formed.
26+
//!
27+
//! - **Fee Calculation Proof (`PercentageWithCapProofData`)**:
28+
//! It's a "one-of-two" proof that certifies **either**:
29+
//! 1. The `fee_amount` is exactly equal to the on-chain `maximum_fee`.
30+
//! 2. The `fee_amount` was correctly calculated as a percentage of the
31+
//! `transfer_amount`, according to the on-chain `fee_rate_basis_points`.
32+
//!
33+
//! **Note**: The proof certifies that the transfer fee is a valid percentage of the
34+
//! transfer amount or that the fee is exactly the maximum fee. While the sender is
35+
//! expected to choose the lower of these two options, the proof does not enforce this
36+
//! choice.
37+
//!
38+
//! - **Range Proof (`BatchedRangeProofU256Data`)**:
39+
//! This expanded range proof ensures the solvency of the entire transaction by certifying
40+
//! that all critical monetary values are non-negative. This includes the sender's remaining
41+
//! balance, the gross transfer amount, the fee amount, and the net transfer amount that the
42+
//! destination receives.
43+
//!
44+
//! - **Ciphertext-Commitment Equality Proof (`CiphertextCommitmentEqualityProofData`)**:
45+
//! Identical in purpose to the simple transfer, this proof links the sender's remaining
46+
//! balance (as a homomorphically computed ElGamal ciphertext) to a new Pedersen commitment.
47+
//! This commitment is then used in the Range Proof to prove the sender's solvency.
48+
149
#[cfg(not(target_arch = "wasm32"))]
250
use solana_zk_sdk::encryption::grouped_elgamal::GroupedElGamal;
351
#[cfg(target_arch = "wasm32")]
@@ -40,11 +88,6 @@ const NET_TRANSFER_AMOUNT_BIT_LENGTH: usize = 64;
4088

4189
/// The proof data required for a confidential transfer instruction when the
4290
/// mint is extended for fees
43-
///
44-
/// NOTE: The proofs certify that the transfer fee is a valid percentage of the
45-
/// transfer amount, as determined by the fee basis points, or that the fee is
46-
/// exactly the maximum fee. While the sender is expected to choose the lower of
47-
/// these two options, the proof does not enforce this choice.
4891
pub struct TransferWithFeeProofData {
4992
pub equality_proof_data: CiphertextCommitmentEqualityProofData,
5093
pub transfer_amount_ciphertext_validity_proof_data_with_ciphertext:
@@ -193,12 +236,16 @@ pub fn transfer_with_fee_split_proof_data(
193236
// calculate fee
194237
let transfer_fee_basis_points = fee_rate_basis_points;
195238
let transfer_fee_maximum_fee = maximum_fee;
196-
let (raw_fee_amount, delta_fee) = calculate_fee(transfer_amount, transfer_fee_basis_points)
239+
let (raw_fee_amount, raw_delta_fee) = calculate_fee(transfer_amount, transfer_fee_basis_points)
197240
.ok_or(TokenProofGenerationError::FeeCalculation)?;
198241

199242
// if raw fee is greater than the maximum fee, then use the maximum fee for the
200-
// fee amount
201-
let fee_amount = std::cmp::min(transfer_fee_maximum_fee, raw_fee_amount);
243+
// fee amount and set the claimed delta fee to be 0 for simplicity
244+
let (fee_amount, claimed_delta_fee) = if transfer_fee_maximum_fee < raw_fee_amount {
245+
(transfer_fee_maximum_fee, 0)
246+
} else {
247+
(raw_fee_amount, raw_delta_fee)
248+
};
202249
let net_transfer_amount = transfer_amount
203250
.checked_sub(fee_amount)
204251
.ok_or(TokenProofGenerationError::FeeCalculation)?;
@@ -249,7 +296,7 @@ pub fn transfer_with_fee_split_proof_data(
249296
let net_transfer_amount_opening = &combined_transfer_amount_opening - &combined_fee_opening;
250297

251298
// compute claimed and real delta commitment
252-
let (claimed_commitment, claimed_opening) = Pedersen::new(delta_fee);
299+
let (claimed_commitment, claimed_opening) = Pedersen::new(claimed_delta_fee);
253300
let (delta_commitment, delta_opening) = compute_delta_commitment_and_opening(
254301
(
255302
&combined_transfer_amount_commitment,
@@ -266,7 +313,7 @@ pub fn transfer_with_fee_split_proof_data(
266313
fee_amount,
267314
&delta_commitment,
268315
&delta_opening,
269-
delta_fee,
316+
claimed_delta_fee,
270317
&claimed_commitment,
271318
&claimed_opening,
272319
transfer_fee_maximum_fee,
@@ -327,7 +374,7 @@ pub fn transfer_with_fee_split_proof_data(
327374

328375
// generate range proof data
329376
let delta_fee_complement = MAX_FEE_BASIS_POINTS_SUB_ONE
330-
.checked_sub(delta_fee)
377+
.checked_sub(claimed_delta_fee)
331378
.ok_or(TokenProofGenerationError::FeeCalculation)?;
332379

333380
let max_fee_basis_points_sub_one_commitment =
@@ -353,7 +400,7 @@ pub fn transfer_with_fee_split_proof_data(
353400
new_decrypted_available_balance,
354401
transfer_amount_lo,
355402
transfer_amount_hi,
356-
delta_fee,
403+
claimed_delta_fee,
357404
delta_fee_complement,
358405
fee_amount_lo,
359406
fee_amount_hi,

confidential-transfer/proof-generation/src/withdraw.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,37 @@
1+
//! Generates the zero-knowledge proofs required for a confidential withdraw.
2+
//!
3+
//! A confidential withdraw operation converts a user's encrypted, confidential token balance back into a
4+
//! standard, publicly-visible SPL token balance. To ensure this operation is valid and that a user
5+
//! cannot create tokens out of thin air, it requires two distinct zero-knowledge proofs.
6+
//!
7+
//! ## Protocol Flow and Proof Components
8+
//!
9+
//! 1. **Calculate Remaining Balance**: The client first calculates the remaining confidential balance
10+
//! by subtracting the desired `withdraw_amount` from their current known balance.
11+
//!
12+
//! 2. **Homomorphic Calculation**: The client homomorphically computes the new encrypted balance
13+
//! ciphertext. This is done by taking the current `available_balance` ciphertext and subtracting
14+
//! a newly-encoded ciphertext of the `withdraw_amount`.
15+
//!
16+
//! 3. **Generate Proofs**: The user generates two proofs to certify the validity of the operation:
17+
//!
18+
//! - **Ciphertext-Commitment Equality Proof (`CiphertextCommitmentEqualityProofData`)**:
19+
//! This proof provides a cryptographic link that enables the solvency check. When the
20+
//! `remaining_balance_ciphertext` is computed homomorphically, the prover may not know the
21+
//! corresponding Pedersen opening (randomness) for the resulting ciphertext. Performing a
22+
//! range proof requires knowledge of this opening.
23+
//!
24+
//! To solve this, the prover creates a *new* Pedersen commitment for the remaining balance,
25+
//! for which it knows the opening. The equality proof then certifies that the
26+
//! homomorphically-derived ciphertext and this new commitment hide the exact same numerical
27+
//! value. This allows the range proof to be performed on the new commitment.
28+
//!
29+
//! - **Range Proof (`BatchedRangeProofU64Data`)**:
30+
//! This proof certifies the user's **solvency**. By proving that the value inside the
31+
//! Pedersen commitment for the *remaining balance* is non-negative (i.e., it is in the range
32+
//! `[0, 2^64)`), it implicitly proves that the user's original balance was greater than or
33+
//! equal to the `withdraw_amount`.
34+
135
use {
236
crate::errors::TokenProofGenerationError,
337
solana_zk_sdk::{

scripts/solana.dic

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,5 @@ Pedersen
6161
aes
6262
plaintext
6363
pausable
64+
homomorphic
65+
homomorphically

0 commit comments

Comments
 (0)