spl_token_confidential_transfer_proof_generation/
mint.rs

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
37#[cfg(target_arch = "wasm32")]
38use solana_zk_sdk::encryption::grouped_elgamal::GroupedElGamalCiphertext3Handles;
39use {
40    crate::{
41        encryption::MintAmountCiphertext, errors::TokenProofGenerationError,
42        try_combine_lo_hi_ciphertexts, try_split_u64, CiphertextValidityProofWithAuditorCiphertext,
43    },
44    solana_zk_sdk::{
45        encryption::{
46            elgamal::{ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey},
47            pedersen::Pedersen,
48        },
49        zk_elgamal_proof_program::proof_data::{
50            BatchedGroupedCiphertext3HandlesValidityProofData, BatchedRangeProofU128Data,
51            CiphertextCommitmentEqualityProofData, ZkProofData,
52        },
53    },
54};
55
56const NEW_SUPPLY_BIT_LENGTH: usize = 64;
57const MINT_AMOUNT_LO_BIT_LENGTH: usize = 16;
58const MINT_AMOUNT_HI_BIT_LENGTH: usize = 32;
59/// The padding bit length in range proofs to make the bit-length power-of-2
60const RANGE_PROOF_PADDING_BIT_LENGTH: usize = 16;
61
62/// The proof data required for a confidential mint instruction
63pub struct MintProofData {
64    pub equality_proof_data: CiphertextCommitmentEqualityProofData,
65    pub ciphertext_validity_proof_data_with_ciphertext:
66        CiphertextValidityProofWithAuditorCiphertext,
67    pub range_proof_data: BatchedRangeProofU128Data,
68}
69
70pub fn mint_split_proof_data(
71    current_supply_ciphertext: &ElGamalCiphertext,
72    mint_amount: u64,
73    current_supply: u64,
74    supply_elgamal_keypair: &ElGamalKeypair,
75    destination_elgamal_pubkey: &ElGamalPubkey,
76    auditor_elgamal_pubkey: Option<&ElGamalPubkey>,
77) -> Result<MintProofData, TokenProofGenerationError> {
78    let default_auditor_pubkey = ElGamalPubkey::default();
79    let auditor_elgamal_pubkey = auditor_elgamal_pubkey.unwrap_or(&default_auditor_pubkey);
80
81    // split the mint amount into low and high bits
82    let (mint_amount_lo, mint_amount_hi) = try_split_u64(mint_amount, MINT_AMOUNT_LO_BIT_LENGTH)
83        .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?;
84
85    // encrypt the mint amount under the destination and auditor's ElGamal public
86    // keys
87    let (mint_amount_grouped_ciphertext_lo, mint_amount_opening_lo) = MintAmountCiphertext::new(
88        mint_amount_lo,
89        destination_elgamal_pubkey,
90        supply_elgamal_keypair.pubkey(),
91        auditor_elgamal_pubkey,
92    );
93    #[cfg(not(target_arch = "wasm32"))]
94    let grouped_ciphertext_lo = mint_amount_grouped_ciphertext_lo.0;
95    #[cfg(target_arch = "wasm32")]
96    let grouped_ciphertext_lo = GroupedElGamalCiphertext3Handles::encrypt_with_u64(
97        destination_elgamal_pubkey,
98        supply_elgamal_keypair.pubkey(),
99        auditor_elgamal_pubkey,
100        mint_amount_lo,
101        &mint_amount_opening_lo,
102    );
103
104    let (mint_amount_grouped_ciphertext_hi, mint_amount_opening_hi) = MintAmountCiphertext::new(
105        mint_amount_hi,
106        destination_elgamal_pubkey,
107        supply_elgamal_keypair.pubkey(),
108        auditor_elgamal_pubkey,
109    );
110    #[cfg(not(target_arch = "wasm32"))]
111    let grouped_ciphertext_hi = mint_amount_grouped_ciphertext_hi.0;
112    #[cfg(target_arch = "wasm32")]
113    let grouped_ciphertext_hi = GroupedElGamalCiphertext3Handles::encrypt_with_u64(
114        destination_elgamal_pubkey,
115        supply_elgamal_keypair.pubkey(),
116        auditor_elgamal_pubkey,
117        mint_amount_hi,
118        &mint_amount_opening_hi,
119    );
120
121    // compute the new supply ciphertext
122    let mint_amount_ciphertext_supply_lo = mint_amount_grouped_ciphertext_lo
123        .0
124        .to_elgamal_ciphertext(1)
125        .unwrap();
126    let mint_amount_ciphertext_supply_hi = mint_amount_grouped_ciphertext_hi
127        .0
128        .to_elgamal_ciphertext(1)
129        .unwrap();
130
131    #[allow(clippy::arithmetic_side_effects)]
132    let new_supply_ciphertext = current_supply_ciphertext
133        + try_combine_lo_hi_ciphertexts(
134            &mint_amount_ciphertext_supply_lo,
135            &mint_amount_ciphertext_supply_hi,
136            MINT_AMOUNT_LO_BIT_LENGTH,
137        )
138        .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?;
139
140    // compute the new supply
141    let new_supply = current_supply
142        .checked_add(mint_amount)
143        .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?;
144
145    let (new_supply_commitment, new_supply_opening) = Pedersen::new(new_supply);
146
147    // generate equality proof data
148    let equality_proof_data = CiphertextCommitmentEqualityProofData::new(
149        supply_elgamal_keypair,
150        &new_supply_ciphertext,
151        &new_supply_commitment,
152        &new_supply_opening,
153        new_supply,
154    )
155    .map_err(TokenProofGenerationError::from)?;
156
157    // generate ciphertext validity proof data
158    let ciphertext_validity_proof_data = BatchedGroupedCiphertext3HandlesValidityProofData::new(
159        destination_elgamal_pubkey,
160        supply_elgamal_keypair.pubkey(),
161        auditor_elgamal_pubkey,
162        &grouped_ciphertext_lo,
163        &grouped_ciphertext_hi,
164        mint_amount_lo,
165        mint_amount_hi,
166        &mint_amount_opening_lo,
167        &mint_amount_opening_hi,
168    )
169    .map_err(TokenProofGenerationError::from)?;
170
171    let mint_amount_auditor_ciphertext_lo = ciphertext_validity_proof_data
172        .context_data()
173        .grouped_ciphertext_lo
174        .try_extract_ciphertext(2)
175        .map_err(|_| TokenProofGenerationError::CiphertextExtraction)?;
176
177    let mint_amount_auditor_ciphertext_hi = ciphertext_validity_proof_data
178        .context_data()
179        .grouped_ciphertext_hi
180        .try_extract_ciphertext(2)
181        .map_err(|_| TokenProofGenerationError::CiphertextExtraction)?;
182
183    let ciphertext_validity_proof_data_with_ciphertext =
184        CiphertextValidityProofWithAuditorCiphertext {
185            proof_data: ciphertext_validity_proof_data,
186            ciphertext_lo: mint_amount_auditor_ciphertext_lo,
187            ciphertext_hi: mint_amount_auditor_ciphertext_hi,
188        };
189
190    // 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
195    let (padding_commitment, padding_opening) = Pedersen::new(0_u64);
196    let range_proof_data = BatchedRangeProofU128Data::new(
197        vec![
198            &new_supply_commitment,
199            mint_amount_grouped_ciphertext_lo.get_commitment(),
200            mint_amount_grouped_ciphertext_hi.get_commitment(),
201            &padding_commitment,
202        ],
203        vec![new_supply, mint_amount_lo, mint_amount_hi, 0],
204        vec![
205            NEW_SUPPLY_BIT_LENGTH,
206            MINT_AMOUNT_LO_BIT_LENGTH,
207            MINT_AMOUNT_HI_BIT_LENGTH,
208            RANGE_PROOF_PADDING_BIT_LENGTH,
209        ],
210        vec![
211            &new_supply_opening,
212            &mint_amount_opening_lo,
213            &mint_amount_opening_hi,
214            &padding_opening,
215        ],
216    )
217    .map_err(TokenProofGenerationError::from)?;
218
219    Ok(MintProofData {
220        equality_proof_data,
221        ciphertext_validity_proof_data_with_ciphertext,
222        range_proof_data,
223    })
224}