spl_token_confidential_transfer_proof_generation/
transfer.rs

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
42#[cfg(target_arch = "wasm32")]
43use solana_zk_sdk::encryption::grouped_elgamal::GroupedElGamalCiphertext3Handles;
44use {
45    crate::{
46        encryption::TransferAmountCiphertext, errors::TokenProofGenerationError,
47        try_combine_lo_hi_ciphertexts, try_split_u64, CiphertextValidityProofWithAuditorCiphertext,
48        REMAINING_BALANCE_BIT_LENGTH, TRANSFER_AMOUNT_HI_BITS, TRANSFER_AMOUNT_LO_BITS,
49    },
50    solana_zk_sdk::{
51        encryption::{
52            auth_encryption::{AeCiphertext, AeKey},
53            elgamal::{ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey},
54            pedersen::Pedersen,
55        },
56        zk_elgamal_proof_program::proof_data::{
57            BatchedGroupedCiphertext3HandlesValidityProofData, BatchedRangeProofU128Data,
58            CiphertextCommitmentEqualityProofData, ZkProofData,
59        },
60    },
61};
62
63/// The padding bit length in range proofs that are used for a confidential
64/// token transfer
65const RANGE_PROOF_PADDING_BIT_LENGTH: usize = 16;
66
67/// The proof data required for a confidential transfer instruction when the
68/// mint is not extended for fees
69pub struct TransferProofData {
70    pub equality_proof_data: CiphertextCommitmentEqualityProofData,
71    pub ciphertext_validity_proof_data_with_ciphertext:
72        CiphertextValidityProofWithAuditorCiphertext,
73    pub range_proof_data: BatchedRangeProofU128Data,
74}
75
76pub fn transfer_split_proof_data(
77    current_available_balance: &ElGamalCiphertext,
78    current_decryptable_available_balance: &AeCiphertext,
79    transfer_amount: u64,
80    source_elgamal_keypair: &ElGamalKeypair,
81    aes_key: &AeKey,
82    destination_elgamal_pubkey: &ElGamalPubkey,
83    auditor_elgamal_pubkey: Option<&ElGamalPubkey>,
84) -> Result<TransferProofData, TokenProofGenerationError> {
85    let default_auditor_pubkey = ElGamalPubkey::default();
86    let auditor_elgamal_pubkey = auditor_elgamal_pubkey.unwrap_or(&default_auditor_pubkey);
87
88    // Split the transfer amount into the low and high bit components
89    let (transfer_amount_lo, transfer_amount_hi) =
90        try_split_u64(transfer_amount, TRANSFER_AMOUNT_LO_BITS)
91            .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?;
92
93    // Encrypt the `lo` and `hi` transfer amounts
94    let (transfer_amount_grouped_ciphertext_lo, transfer_amount_opening_lo) =
95        TransferAmountCiphertext::new(
96            transfer_amount_lo,
97            source_elgamal_keypair.pubkey(),
98            destination_elgamal_pubkey,
99            auditor_elgamal_pubkey,
100        );
101    #[cfg(not(target_arch = "wasm32"))]
102    let grouped_ciphertext_lo = transfer_amount_grouped_ciphertext_lo.0;
103    #[cfg(target_arch = "wasm32")]
104    let grouped_ciphertext_lo = GroupedElGamalCiphertext3Handles::encrypt_with_u64(
105        source_elgamal_keypair.pubkey(),
106        destination_elgamal_pubkey,
107        auditor_elgamal_pubkey,
108        transfer_amount_lo,
109        &transfer_amount_opening_lo,
110    );
111
112    let (transfer_amount_grouped_ciphertext_hi, transfer_amount_opening_hi) =
113        TransferAmountCiphertext::new(
114            transfer_amount_hi,
115            source_elgamal_keypair.pubkey(),
116            destination_elgamal_pubkey,
117            auditor_elgamal_pubkey,
118        );
119    #[cfg(not(target_arch = "wasm32"))]
120    let grouped_ciphertext_hi = transfer_amount_grouped_ciphertext_hi.0;
121    #[cfg(target_arch = "wasm32")]
122    let grouped_ciphertext_hi = GroupedElGamalCiphertext3Handles::encrypt_with_u64(
123        source_elgamal_keypair.pubkey(),
124        destination_elgamal_pubkey,
125        auditor_elgamal_pubkey,
126        transfer_amount_hi,
127        &transfer_amount_opening_hi,
128    );
129
130    // Decrypt the current available balance at the source
131    let current_decrypted_available_balance = current_decryptable_available_balance
132        .decrypt(aes_key)
133        .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?;
134
135    // Compute the remaining balance at the source
136    let new_decrypted_available_balance = current_decrypted_available_balance
137        .checked_sub(transfer_amount)
138        .ok_or(TokenProofGenerationError::NotEnoughFunds)?;
139
140    // Create a new Pedersen commitment for the remaining balance at the source
141    let (new_available_balance_commitment, new_source_opening) =
142        Pedersen::new(new_decrypted_available_balance);
143
144    // Compute the remaining balance at the source as ElGamal ciphertexts
145    let transfer_amount_source_ciphertext_lo = transfer_amount_grouped_ciphertext_lo
146        .0
147        .to_elgamal_ciphertext(0)
148        .unwrap();
149    let transfer_amount_source_ciphertext_hi = transfer_amount_grouped_ciphertext_hi
150        .0
151        .to_elgamal_ciphertext(0)
152        .unwrap();
153
154    #[allow(clippy::arithmetic_side_effects)]
155    let new_available_balance_ciphertext = current_available_balance
156        - try_combine_lo_hi_ciphertexts(
157            &transfer_amount_source_ciphertext_lo,
158            &transfer_amount_source_ciphertext_hi,
159            TRANSFER_AMOUNT_LO_BITS,
160        )
161        .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?;
162
163    // generate equality proof data
164    let equality_proof_data = CiphertextCommitmentEqualityProofData::new(
165        source_elgamal_keypair,
166        &new_available_balance_ciphertext,
167        &new_available_balance_commitment,
168        &new_source_opening,
169        new_decrypted_available_balance,
170    )
171    .map_err(TokenProofGenerationError::from)?;
172
173    // generate ciphertext validity data
174    let ciphertext_validity_proof_data = BatchedGroupedCiphertext3HandlesValidityProofData::new(
175        source_elgamal_keypair.pubkey(),
176        destination_elgamal_pubkey,
177        auditor_elgamal_pubkey,
178        &grouped_ciphertext_lo,
179        &grouped_ciphertext_hi,
180        transfer_amount_lo,
181        transfer_amount_hi,
182        &transfer_amount_opening_lo,
183        &transfer_amount_opening_hi,
184    )
185    .map_err(TokenProofGenerationError::from)?;
186
187    let transfer_amount_auditor_ciphertext_lo = ciphertext_validity_proof_data
188        .context_data()
189        .grouped_ciphertext_lo
190        .try_extract_ciphertext(2)
191        .map_err(|_| TokenProofGenerationError::CiphertextExtraction)?;
192
193    let transfer_amount_auditor_ciphertext_hi = ciphertext_validity_proof_data
194        .context_data()
195        .grouped_ciphertext_hi
196        .try_extract_ciphertext(2)
197        .map_err(|_| TokenProofGenerationError::CiphertextExtraction)?;
198
199    let ciphertext_validity_proof_data_with_ciphertext =
200        CiphertextValidityProofWithAuditorCiphertext {
201            proof_data: ciphertext_validity_proof_data,
202            ciphertext_lo: transfer_amount_auditor_ciphertext_lo,
203            ciphertext_hi: transfer_amount_auditor_ciphertext_hi,
204        };
205
206    // 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
211    let (padding_commitment, padding_opening) = Pedersen::new(0_u64);
212    let range_proof_data = BatchedRangeProofU128Data::new(
213        vec![
214            &new_available_balance_commitment,
215            transfer_amount_grouped_ciphertext_lo.get_commitment(),
216            transfer_amount_grouped_ciphertext_hi.get_commitment(),
217            &padding_commitment,
218        ],
219        vec![
220            new_decrypted_available_balance,
221            transfer_amount_lo,
222            transfer_amount_hi,
223            0,
224        ],
225        vec![
226            REMAINING_BALANCE_BIT_LENGTH,
227            TRANSFER_AMOUNT_LO_BITS,
228            TRANSFER_AMOUNT_HI_BITS,
229            RANGE_PROOF_PADDING_BIT_LENGTH,
230        ],
231        vec![
232            &new_source_opening,
233            &transfer_amount_opening_lo,
234            &transfer_amount_opening_hi,
235            &padding_opening,
236        ],
237    )
238    .map_err(TokenProofGenerationError::from)?;
239
240    Ok(TransferProofData {
241        equality_proof_data,
242        ciphertext_validity_proof_data_with_ciphertext,
243        range_proof_data,
244    })
245}