spl_token_confidential_transfer_proof_generation/withdraw.rs
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
35use {
36 crate::errors::TokenProofGenerationError,
37 solana_zk_sdk::{
38 encryption::{
39 elgamal::{ElGamal, ElGamalCiphertext, ElGamalKeypair},
40 pedersen::Pedersen,
41 },
42 zk_elgamal_proof_program::proof_data::{
43 BatchedRangeProofU64Data, CiphertextCommitmentEqualityProofData,
44 },
45 },
46};
47
48const REMAINING_BALANCE_BIT_LENGTH: usize = 64;
49
50/// Proof data required for a withdraw instruction
51pub struct WithdrawProofData {
52 pub equality_proof_data: CiphertextCommitmentEqualityProofData,
53 pub range_proof_data: BatchedRangeProofU64Data,
54}
55
56pub fn withdraw_proof_data(
57 current_available_balance: &ElGamalCiphertext,
58 current_balance: u64,
59 withdraw_amount: u64,
60 elgamal_keypair: &ElGamalKeypair,
61) -> Result<WithdrawProofData, TokenProofGenerationError> {
62 // Calculate the remaining balance after withdraw
63 let remaining_balance = current_balance
64 .checked_sub(withdraw_amount)
65 .ok_or(TokenProofGenerationError::NotEnoughFunds)?;
66
67 // Generate a Pedersen commitment for the remaining balance
68 let (remaining_balance_commitment, remaining_balance_opening) =
69 Pedersen::new(remaining_balance);
70
71 // Compute the remaining balance ciphertext
72 #[allow(clippy::arithmetic_side_effects)]
73 let remaining_balance_ciphertext = current_available_balance - ElGamal::encode(withdraw_amount);
74
75 // Generate proof data
76 let equality_proof_data = CiphertextCommitmentEqualityProofData::new(
77 elgamal_keypair,
78 &remaining_balance_ciphertext,
79 &remaining_balance_commitment,
80 &remaining_balance_opening,
81 remaining_balance,
82 )
83 .map_err(TokenProofGenerationError::from)?;
84
85 let range_proof_data = BatchedRangeProofU64Data::new(
86 vec![&remaining_balance_commitment],
87 vec![remaining_balance],
88 vec![REMAINING_BALANCE_BIT_LENGTH],
89 vec![&remaining_balance_opening],
90 )
91 .map_err(TokenProofGenerationError::from)?;
92
93 Ok(WithdrawProofData {
94 equality_proof_data,
95 range_proof_data,
96 })
97}