spl_token_confidential_transfer_proof_generation/
burn.rs

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
37#[cfg(target_arch = "wasm32")]
38use solana_zk_sdk::encryption::grouped_elgamal::GroupedElGamalCiphertext3Handles;
39use {
40    crate::{
41        encryption::BurnAmountCiphertext, errors::TokenProofGenerationError,
42        try_combine_lo_hi_ciphertexts, try_split_u64, CiphertextValidityProofWithAuditorCiphertext,
43    },
44    solana_zk_sdk::{
45        encryption::{
46            auth_encryption::{AeCiphertext, AeKey},
47            elgamal::{ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey},
48            pedersen::Pedersen,
49        },
50        zk_elgamal_proof_program::proof_data::{
51            BatchedGroupedCiphertext3HandlesValidityProofData, BatchedRangeProofU128Data,
52            CiphertextCommitmentEqualityProofData, ZkProofData,
53        },
54    },
55};
56
57const REMAINING_BALANCE_BIT_LENGTH: usize = 64;
58const BURN_AMOUNT_LO_BIT_LENGTH: usize = 16;
59const BURN_AMOUNT_HI_BIT_LENGTH: usize = 32;
60/// The padding bit length in range proofs to make the bit-length power-of-2
61const RANGE_PROOF_PADDING_BIT_LENGTH: usize = 16;
62
63/// The proof data required for a confidential burn instruction
64pub struct BurnProofData {
65    pub equality_proof_data: CiphertextCommitmentEqualityProofData,
66    pub ciphertext_validity_proof_data_with_ciphertext:
67        CiphertextValidityProofWithAuditorCiphertext,
68    pub range_proof_data: BatchedRangeProofU128Data,
69}
70
71pub fn burn_split_proof_data(
72    current_available_balance_ciphertext: &ElGamalCiphertext,
73    current_decryptable_available_balance: &AeCiphertext,
74    burn_amount: u64,
75    source_elgamal_keypair: &ElGamalKeypair,
76    source_aes_key: &AeKey,
77    supply_elgamal_pubkey: &ElGamalPubkey,
78    auditor_elgamal_pubkey: Option<&ElGamalPubkey>,
79) -> Result<BurnProofData, TokenProofGenerationError> {
80    let default_auditor_pubkey = ElGamalPubkey::default();
81    let auditor_elgamal_pubkey = auditor_elgamal_pubkey.unwrap_or(&default_auditor_pubkey);
82
83    // split the burn amount into low and high bits
84    let (burn_amount_lo, burn_amount_hi) = try_split_u64(burn_amount, BURN_AMOUNT_LO_BIT_LENGTH)
85        .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?;
86
87    // encrypt the burn amount under the source and auditor's ElGamal public key
88    let (burn_amount_ciphertext_lo, burn_amount_opening_lo) = BurnAmountCiphertext::new(
89        burn_amount_lo,
90        source_elgamal_keypair.pubkey(),
91        supply_elgamal_pubkey,
92        auditor_elgamal_pubkey,
93    );
94    #[cfg(not(target_arch = "wasm32"))]
95    let grouped_ciphertext_lo = burn_amount_ciphertext_lo.0;
96    #[cfg(target_arch = "wasm32")]
97    let grouped_ciphertext_lo = GroupedElGamalCiphertext3Handles::encrypt_with_u64(
98        source_elgamal_keypair.pubkey(),
99        supply_elgamal_pubkey,
100        auditor_elgamal_pubkey,
101        burn_amount_lo,
102        &burn_amount_opening_lo,
103    );
104
105    let (burn_amount_ciphertext_hi, burn_amount_opening_hi) = BurnAmountCiphertext::new(
106        burn_amount_hi,
107        source_elgamal_keypair.pubkey(),
108        supply_elgamal_pubkey,
109        auditor_elgamal_pubkey,
110    );
111    #[cfg(not(target_arch = "wasm32"))]
112    let grouped_ciphertext_hi = burn_amount_ciphertext_hi.0;
113    #[cfg(target_arch = "wasm32")]
114    let grouped_ciphertext_hi = GroupedElGamalCiphertext3Handles::encrypt_with_u64(
115        source_elgamal_keypair.pubkey(),
116        supply_elgamal_pubkey,
117        auditor_elgamal_pubkey,
118        burn_amount_hi,
119        &burn_amount_opening_hi,
120    );
121    // decrypt the current available balance at the source
122    let current_decrypted_available_balance = current_decryptable_available_balance
123        .decrypt(source_aes_key)
124        .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?;
125
126    // compute the remaining balance ciphertext
127    let burn_amount_ciphertext_source_lo = burn_amount_ciphertext_lo
128        .0
129        .to_elgamal_ciphertext(0)
130        .unwrap();
131    let burn_amount_ciphertext_source_hi = burn_amount_ciphertext_hi
132        .0
133        .to_elgamal_ciphertext(0)
134        .unwrap();
135
136    #[allow(clippy::arithmetic_side_effects)]
137    let new_available_balance_ciphertext = current_available_balance_ciphertext
138        - try_combine_lo_hi_ciphertexts(
139            &burn_amount_ciphertext_source_lo,
140            &burn_amount_ciphertext_source_hi,
141            BURN_AMOUNT_LO_BIT_LENGTH,
142        )
143        .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?;
144
145    // compute the remaining balance at the source
146    let remaining_balance = current_decrypted_available_balance
147        .checked_sub(burn_amount)
148        .ok_or(TokenProofGenerationError::NotEnoughFunds)?;
149
150    let (new_available_balance_commitment, new_available_balance_opening) =
151        Pedersen::new(remaining_balance);
152
153    // generate equality proof data
154    let equality_proof_data = CiphertextCommitmentEqualityProofData::new(
155        source_elgamal_keypair,
156        &new_available_balance_ciphertext,
157        &new_available_balance_commitment,
158        &new_available_balance_opening,
159        remaining_balance,
160    )
161    .map_err(TokenProofGenerationError::from)?;
162
163    // generate ciphertext validity data
164    let ciphertext_validity_proof_data = BatchedGroupedCiphertext3HandlesValidityProofData::new(
165        source_elgamal_keypair.pubkey(),
166        supply_elgamal_pubkey,
167        auditor_elgamal_pubkey,
168        &grouped_ciphertext_lo,
169        &grouped_ciphertext_hi,
170        burn_amount_lo,
171        burn_amount_hi,
172        &burn_amount_opening_lo,
173        &burn_amount_opening_hi,
174    )
175    .map_err(TokenProofGenerationError::from)?;
176
177    let burn_amount_auditor_ciphertext_lo = ciphertext_validity_proof_data
178        .context_data()
179        .grouped_ciphertext_lo
180        .try_extract_ciphertext(2)
181        .map_err(|_| TokenProofGenerationError::CiphertextExtraction)?;
182
183    let burn_amount_auditor_ciphertext_hi = ciphertext_validity_proof_data
184        .context_data()
185        .grouped_ciphertext_hi
186        .try_extract_ciphertext(2)
187        .map_err(|_| TokenProofGenerationError::CiphertextExtraction)?;
188
189    let ciphertext_validity_proof_data_with_ciphertext =
190        CiphertextValidityProofWithAuditorCiphertext {
191            proof_data: ciphertext_validity_proof_data,
192            ciphertext_lo: burn_amount_auditor_ciphertext_lo,
193            ciphertext_hi: burn_amount_auditor_ciphertext_hi,
194        };
195
196    // 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
201    let (padding_commitment, padding_opening) = Pedersen::new(0_u64);
202    let range_proof_data = BatchedRangeProofU128Data::new(
203        vec![
204            &new_available_balance_commitment,
205            burn_amount_ciphertext_lo.get_commitment(),
206            burn_amount_ciphertext_hi.get_commitment(),
207            &padding_commitment,
208        ],
209        vec![remaining_balance, burn_amount_lo, burn_amount_hi, 0],
210        vec![
211            REMAINING_BALANCE_BIT_LENGTH,
212            BURN_AMOUNT_LO_BIT_LENGTH,
213            BURN_AMOUNT_HI_BIT_LENGTH,
214            RANGE_PROOF_PADDING_BIT_LENGTH,
215        ],
216        vec![
217            &new_available_balance_opening,
218            &burn_amount_opening_lo,
219            &burn_amount_opening_hi,
220            &padding_opening,
221        ],
222    )
223    .map_err(TokenProofGenerationError::from)?;
224
225    Ok(BurnProofData {
226        equality_proof_data,
227        ciphertext_validity_proof_data_with_ciphertext,
228        range_proof_data,
229    })
230}