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}