diff --git a/token/client/src/token.rs b/token/client/src/token.rs index a0ce89ecfdc..426c4821e12 100644 --- a/token/client/src/token.rs +++ b/token/client/src/token.rs @@ -1,6 +1,6 @@ use { crate::client::{ProgramClient, ProgramClientError, SendTransaction, SimulateTransaction}, - futures::future::join_all, + futures::{future::join_all, try_join}, futures_util::TryFutureExt, solana_program_test::tokio::time, solana_sdk::{ @@ -28,7 +28,7 @@ use { WithdrawAccountInfo, }, ciphertext_extraction::SourceDecryptHandles, - instruction::TransferContextStateAccounts, + instruction::TransferSplitContextStateAccounts, ConfidentialTransferAccount, DecryptableBalance, }, confidential_transfer_fee::{ @@ -46,13 +46,18 @@ use { auth_encryption::AeKey, elgamal::{ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey, ElGamalSecretKey}, }, + instruction::*, zk_token_elgamal::pod::ElGamalPubkey as PodElGamalPubkey, + zk_token_proof_instruction::{self, ContextStateInfo, ProofInstruction}, + zk_token_proof_program, + zk_token_proof_state::ProofContextState, }, state::{Account, AccountState, Mint, Multisig}, }, spl_token_metadata_interface::state::{Field, TokenMetadata}, std::{ fmt, io, + mem::size_of, sync::{Arc, RwLock}, time::{Duration, Instant}, }, @@ -2141,7 +2146,7 @@ where source_account: &Pubkey, destination_account: &Pubkey, source_authority: &Pubkey, - context_state_accounts: Option>, + context_state_account: Option<&Pubkey>, transfer_amount: u64, account_info: Option, source_elgamal_keypair: &ElGamalKeypair, @@ -2149,7 +2154,6 @@ where destination_elgamal_pubkey: &ElGamalPubkey, auditor_elgamal_pubkey: Option<&ElGamalPubkey>, signing_keypairs: &S, - source_decrypt_handles: Option<&SourceDecryptHandles>, ) -> TokenResult { let signing_pubkeys = signing_keypairs.pubkeys(); let multisig_signers = self.get_multisig_signers(source_authority, &signing_pubkeys); @@ -2163,7 +2167,7 @@ where TransferAccountInfo::new(confidential_transfer_account) }; - let proof_data = if context_state_accounts.is_some() { + let proof_data = if context_state_account.is_some() { None } else { Some( @@ -2179,23 +2183,11 @@ where ) }; - let mut split_context_state_accounts = Vec::with_capacity(3); let proof_location = if let Some(proof_data_temp) = proof_data.as_ref() { ProofLocation::InstructionOffset(1.try_into().unwrap(), proof_data_temp) } else { - let context_state_accounts = context_state_accounts.unwrap(); - match context_state_accounts { - TransferContextStateAccounts::SingleAccount(context_state_account) => { - ProofLocation::ContextStateAccount(context_state_account) - } - TransferContextStateAccounts::SplitAccounts(context_state_accounts) => { - split_context_state_accounts.push(context_state_accounts.equality_proof); - split_context_state_accounts - .push(context_state_accounts.ciphertext_validity_proof); - split_context_state_accounts.push(context_state_accounts.range_proof); - ProofLocation::SplitContextStateAccounts(&split_context_state_accounts) - } - } + let context_state_account = context_state_account.unwrap(); + ProofLocation::ContextStateAccount(context_state_account) }; let new_decryptable_available_balance = account_info @@ -2212,13 +2204,407 @@ where source_authority, &multisig_signers, proof_location, - source_decrypt_handles, )?, signing_keypairs, ) .await } + /// Transfer tokens confidentially using split proofs. + /// + /// This function assumes that proof context states have already been created. + #[allow(clippy::too_many_arguments)] + pub async fn confidential_transfer_transfer_with_split_proofs( + &self, + source_account: &Pubkey, + destination_account: &Pubkey, + source_authority: &Pubkey, + context_state_accounts: TransferSplitContextStateAccounts<'_>, + transfer_amount: u64, + account_info: Option, + source_aes_key: &AeKey, + source_authority_keypair: &S, + source_decrypt_handles: &SourceDecryptHandles, + ) -> TokenResult { + let account_info = if let Some(account_info) = account_info { + account_info + } else { + let account = self.get_account_info(source_account).await?; + let confidential_transfer_account = + account.get_extension::()?; + TransferAccountInfo::new(confidential_transfer_account) + }; + + let new_decryptable_available_balance = account_info + .new_decryptable_available_balance(transfer_amount, source_aes_key) + .map_err(|_| TokenError::AccountDecryption)?; + + self.process_ixs( + &[ + confidential_transfer::instruction::transfer_with_split_proofs( + &self.program_id, + source_account, + destination_account, + &self.pubkey, + new_decryptable_available_balance.into(), + source_authority, + context_state_accounts, + source_decrypt_handles, + )?, + ], + &[source_authority_keypair], + ) + .await + } + + /// Transfer tokens confidentially using split proofs in parallel + /// + /// This function internally generates the ZK Token proof instructions to create the necessary + /// proof context states. + #[allow(clippy::too_many_arguments)] + pub async fn confidential_transfer_transfer_with_split_proofs_in_parallel( + &self, + source_account: &Pubkey, + destination_account: &Pubkey, + source_authority: &Pubkey, + context_state_accounts: TransferSplitContextStateAccounts<'_>, + transfer_amount: u64, + account_info: Option, + source_elgamal_keypair: &ElGamalKeypair, + source_aes_key: &AeKey, + destination_elgamal_pubkey: &ElGamalPubkey, + auditor_elgamal_pubkey: Option<&ElGamalPubkey>, + source_authority_keypair: &S, + equality_proof_account_keypair: &S, + ciphertext_validity_proof_account_keypair: &S, + range_proof_account_keypair: &S, + context_state_authority_keypair: Option<&S>, + ) -> TokenResult<(T::Output, T::Output)> { + let account_info = if let Some(account_info) = account_info { + account_info + } else { + let account = self.get_account_info(source_account).await?; + let confidential_transfer_account = + account.get_extension::()?; + TransferAccountInfo::new(confidential_transfer_account) + }; + + let ( + equality_proof_data, + ciphertext_validity_proof_data, + range_proof_data, + source_decrypt_handles, + ) = account_info + .generate_split_transfer_proof_data( + transfer_amount, + source_elgamal_keypair, + source_aes_key, + destination_elgamal_pubkey, + auditor_elgamal_pubkey, + ) + .map_err(|_| TokenError::ProofGeneration)?; + + let new_decryptable_available_balance = account_info + .new_decryptable_available_balance(transfer_amount, source_aes_key) + .map_err(|_| TokenError::AccountDecryption)?; + + let transfer_instruction = confidential_transfer::instruction::transfer_with_split_proofs( + &self.program_id, + source_account, + destination_account, + &self.pubkey, + new_decryptable_available_balance.into(), + source_authority, + context_state_accounts, + &source_decrypt_handles, + )?; + + let transfer_with_equality_and_ciphertext_validity = self + .confidential_transfer_equality_and_ciphertext_validity_proof_context_states_and_transfer_parallel( + context_state_accounts, + &equality_proof_data, + &ciphertext_validity_proof_data, + &transfer_instruction, + Some(source_authority_keypair), + equality_proof_account_keypair, + ciphertext_validity_proof_account_keypair, + context_state_authority_keypair, + ); + + let transfer_with_range_proof = self + .confidential_transfer_range_proof_context_states_and_transfer_parallel( + context_state_accounts, + &range_proof_data, + &transfer_instruction, + Some(source_authority_keypair), + range_proof_account_keypair, + context_state_authority_keypair, + ); + + try_join!( + transfer_with_equality_and_ciphertext_validity, + transfer_with_range_proof + ) + } + + /// Create equality and ciphertext validity proof context state accounts for a confidential transfer. + #[allow(clippy::too_many_arguments)] + pub async fn confidential_transfer_equality_and_ciphertext_validity_proof_context_states_for_transfer< + S: Signer, + >( + &self, + context_state_accounts: TransferSplitContextStateAccounts<'_>, + equality_proof_data: &CiphertextCommitmentEqualityProofData, + ciphertext_validity_proof_data: &BatchedGroupedCiphertext2HandlesValidityProofData, + source_authority_keypair: Option<&S>, + equality_proof_account_keypair: &S, + ciphertext_validity_proof_account_keypair: &S, + context_state_authority_keypair: Option<&S>, + ) -> TokenResult { + self.confidential_transfer_equality_and_ciphertext_validity_proof_context_state_with_optional_transfer( + context_state_accounts, + equality_proof_data, + ciphertext_validity_proof_data, + None, + source_authority_keypair, + equality_proof_account_keypair, + ciphertext_validity_proof_account_keypair, + context_state_authority_keypair, + ).await + } + + /// Create equality and ciphertext validity proof context state accounts with a confidential + /// transfer instruction. + #[allow(clippy::too_many_arguments)] + pub async fn confidential_transfer_equality_and_ciphertext_validity_proof_context_states_and_transfer_parallel< + S: Signer, + >( + &self, + context_state_accounts: TransferSplitContextStateAccounts<'_>, + equality_proof_data: &CiphertextCommitmentEqualityProofData, + ciphertext_validity_proof_data: &BatchedGroupedCiphertext2HandlesValidityProofData, + transfer_instruction: &Instruction, + source_authority_keypair: Option<&S>, + equality_proof_account_keypair: &S, + ciphertext_validity_proof_account_keypair: &S, + context_state_authority_keypair: Option<&S>, + ) -> TokenResult { + self.confidential_transfer_equality_and_ciphertext_validity_proof_context_state_with_optional_transfer( + context_state_accounts, + equality_proof_data, + ciphertext_validity_proof_data, + Some(transfer_instruction), + source_authority_keypair, + equality_proof_account_keypair, + ciphertext_validity_proof_account_keypair, + context_state_authority_keypair, + ).await + } + + /// Create equality and ciphertext validity proof context states for a confidential transfer. + /// + /// If an optional transfer instruction is provided, then the transfer instruction is attached + /// to the same transaction. + #[allow(clippy::too_many_arguments)] + async fn confidential_transfer_equality_and_ciphertext_validity_proof_context_state_with_optional_transfer< + S: Signer, + >( + &self, + context_state_accounts: TransferSplitContextStateAccounts<'_>, + equality_proof_data: &CiphertextCommitmentEqualityProofData, + ciphertext_validity_proof_data: &BatchedGroupedCiphertext2HandlesValidityProofData, + transfer_instruction: Option<&Instruction>, + source_authority_keypair: Option<&S>, + equality_proof_account_keypair: &S, + ciphertext_validity_proof_account_keypair: &S, + context_state_authority_keypair: Option<&S>, + ) -> TokenResult { + let mut signers = vec![ + equality_proof_account_keypair, + ciphertext_validity_proof_account_keypair, + ]; + if let Some(source_authority_keypair) = source_authority_keypair { + signers.push(source_authority_keypair); + } + if let Some(context_state_authority_keypair) = context_state_authority_keypair { + signers.push(context_state_authority_keypair); + } + + let mut instructions = vec![]; + + // create equality proof context state + let instruction_type = ProofInstruction::VerifyCiphertextCommitmentEquality; + let space = size_of::>(); + let rent = self + .client + .get_minimum_balance_for_rent_exemption(space) + .await + .map_err(TokenError::Client)?; + instructions.push(system_instruction::create_account( + &self.payer.pubkey(), + context_state_accounts.equality_proof, + rent, + space as u64, + &zk_token_proof_program::id(), + )); + + let equality_proof_context_state_info = ContextStateInfo { + context_state_account: context_state_accounts.equality_proof, + context_state_authority: context_state_accounts.authority, + }; + instructions.push( + instruction_type + .encode_verify_proof(Some(equality_proof_context_state_info), equality_proof_data), + ); + + // create ciphertext validity proof context state + let instruction_type = ProofInstruction::VerifyBatchedGroupedCiphertext2HandlesValidity; + let space = + size_of::>(); + let rent = self + .client + .get_minimum_balance_for_rent_exemption(space) + .await + .map_err(TokenError::Client)?; + instructions.push(system_instruction::create_account( + &self.payer.pubkey(), + context_state_accounts.ciphertext_validity_proof, + rent, + space as u64, + &zk_token_proof_program::id(), + )); + + let ciphertext_validity_proof_context_state_info = ContextStateInfo { + context_state_account: context_state_accounts.ciphertext_validity_proof, + context_state_authority: context_state_accounts.authority, + }; + instructions.push(instruction_type.encode_verify_proof( + Some(ciphertext_validity_proof_context_state_info), + ciphertext_validity_proof_data, + )); + + // add transfer instruction + if let Some(transfer_instruction) = transfer_instruction { + instructions.push(transfer_instruction.clone()); + } + + self.process_ixs(&instructions, &signers).await + } + + /// Create a range proof context state account for a confidential transfer. + pub async fn confidential_transfer_range_proof_context_state_for_transfer( + &self, + context_state_accounts: TransferSplitContextStateAccounts<'_>, + range_proof_data: &BatchedRangeProofU128Data, + source_authority_keypair: Option<&S>, + range_proof_account_keypair: &S, + context_state_authority_keypair: Option<&S>, + ) -> TokenResult { + self.confidential_transfer_range_proof_context_state_with_optional_transfer( + context_state_accounts, + range_proof_data, + None, + source_authority_keypair, + range_proof_account_keypair, + context_state_authority_keypair, + ) + .await + } + + /// Create a range proof context state account with a confidential transfer instruction. + pub async fn confidential_transfer_range_proof_context_states_and_transfer_parallel< + S: Signer, + >( + &self, + context_state_accounts: TransferSplitContextStateAccounts<'_>, + range_proof_data: &BatchedRangeProofU128Data, + transfer_instruction: &Instruction, + source_authority_keypair: Option<&S>, + range_proof_account_keypair: &S, + context_state_authority_keypair: Option<&S>, + ) -> TokenResult { + self.confidential_transfer_range_proof_context_state_with_optional_transfer( + context_state_accounts, + range_proof_data, + Some(transfer_instruction), + source_authority_keypair, + range_proof_account_keypair, + context_state_authority_keypair, + ) + .await + } + + /// Create a range proof context state account and an optional confidential transfer instruction. + async fn confidential_transfer_range_proof_context_state_with_optional_transfer( + &self, + context_state_accounts: TransferSplitContextStateAccounts<'_>, + range_proof_data: &BatchedRangeProofU128Data, + transfer_instruction: Option<&Instruction>, + source_authority_keypair: Option<&S>, + range_proof_account_keypair: &S, + context_state_authority_keypair: Option<&S>, + ) -> TokenResult { + let mut signers = vec![range_proof_account_keypair]; + if let Some(source_authority_keypair) = source_authority_keypair { + signers.push(source_authority_keypair); + } + if let Some(context_state_authority_keypair) = context_state_authority_keypair { + signers.push(context_state_authority_keypair); + } + + let instruction_type = ProofInstruction::VerifyBatchedRangeProofU128; + let space = size_of::>(); + let rent = self + .client + .get_minimum_balance_for_rent_exemption(space) + .await + .map_err(TokenError::Client)?; + let range_proof_context_state_info = ContextStateInfo { + context_state_account: context_state_accounts.range_proof, + context_state_authority: context_state_accounts.authority, + }; + + let mut instructions = vec![ + system_instruction::create_account( + &self.payer.pubkey(), + context_state_accounts.range_proof, + rent, + space as u64, + &zk_token_proof_program::id(), + ), + instruction_type + .encode_verify_proof(Some(range_proof_context_state_info), range_proof_data), + ]; + + if let Some(transfer_instruction) = transfer_instruction { + instructions.push(transfer_instruction.clone()); + } + + self.process_ixs(&instructions, &signers).await + } + + /// Close a ZK Token proof program context state + pub async fn confidential_transfer_close_context_state( + &self, + context_state_account: &Pubkey, + lamport_destination_account: &Pubkey, + context_state_authority: &S, + ) -> TokenResult { + let context_state_info = ContextStateInfo { + context_state_account, + context_state_authority: &context_state_authority.pubkey(), + }; + + self.process_ixs( + &[zk_token_proof_instruction::close_context_state( + context_state_info, + lamport_destination_account, + )], + &[context_state_authority], + ) + .await + } + /// Transfer tokens confidentially with fee #[allow(clippy::too_many_arguments)] pub async fn confidential_transfer_transfer_with_fee( @@ -2237,7 +2623,6 @@ where fee_rate_basis_points: u16, maximum_fee: u64, signing_keypairs: &S, - source_decrypt_handles: Option<&SourceDecryptHandles>, ) -> TokenResult { let signing_pubkeys = signing_keypairs.pubkeys(); let multisig_signers = self.get_multisig_signers(source_authority, &signing_pubkeys); @@ -2291,7 +2676,6 @@ where source_authority, &multisig_signers, proof_location, - source_decrypt_handles, )?, signing_keypairs, ) diff --git a/token/program-2022-test/tests/confidential_transfer.rs b/token/program-2022-test/tests/confidential_transfer.rs index 45db13e50f6..bc94a8adb1b 100644 --- a/token/program-2022-test/tests/confidential_transfer.rs +++ b/token/program-2022-test/tests/confidential_transfer.rs @@ -18,10 +18,9 @@ use { error::TokenError, extension::{ confidential_transfer::{ - self, - account_info::TransferAccountInfo, - instruction::{TransferContextStateAccounts, TransferSplitContextStateAccounts}, - ConfidentialTransferAccount, MAXIMUM_DEPOSIT_TRANSFER_AMOUNT, + self, account_info::TransferAccountInfo, + instruction::TransferSplitContextStateAccounts, ConfidentialTransferAccount, + MAXIMUM_DEPOSIT_TRANSFER_AMOUNT, }, BaseStateWithExtensions, ExtensionType, }, @@ -976,7 +975,6 @@ async fn confidential_transfer_transfer() { alice_meta.elgamal_keypair.pubkey(), Some(auditor_elgamal_keypair.pubkey()), &[&alice], - None, ) .await .unwrap(); @@ -1007,7 +1005,6 @@ async fn confidential_transfer_transfer() { alice_meta.elgamal_keypair.pubkey(), Some(auditor_elgamal_keypair.pubkey()), &[&alice], - None, ) .await .unwrap(); @@ -1061,7 +1058,6 @@ async fn confidential_transfer_transfer() { bob_meta.elgamal_keypair.pubkey(), Some(auditor_elgamal_keypair.pubkey()), &[&alice], - None, ) .await .unwrap(); @@ -1103,7 +1099,6 @@ async fn confidential_transfer_transfer() { bob_meta.elgamal_keypair.pubkey(), Some(auditor_elgamal_keypair.pubkey()), &[&bob], - None, ) .await .unwrap(); @@ -1121,7 +1116,6 @@ async fn confidential_transfer_transfer() { bob_meta.elgamal_keypair.pubkey(), Some(auditor_elgamal_keypair.pubkey()), &[&bob], - None, ) .await .unwrap_err(); @@ -1241,7 +1235,6 @@ async fn confidential_transfer_transfer_with_fee() { TEST_FEE_BASIS_POINTS, TEST_MAXIMUM_FEE, &[&alice], - None, ) .await .unwrap(); @@ -1275,7 +1268,6 @@ async fn confidential_transfer_transfer_with_fee() { TEST_FEE_BASIS_POINTS, TEST_MAXIMUM_FEE, &[&alice], - None, ) .await .unwrap(); @@ -1332,7 +1324,6 @@ async fn confidential_transfer_transfer_with_fee() { TEST_FEE_BASIS_POINTS, TEST_MAXIMUM_FEE, &[&alice], - None, ) .await .unwrap(); @@ -1477,7 +1468,6 @@ async fn confidential_transfer_transfer_memo() { bob_meta.elgamal_keypair.pubkey(), Some(auditor_elgamal_keypair.pubkey()), &[&alice], - None, ) .await .unwrap_err(); @@ -1507,7 +1497,6 @@ async fn confidential_transfer_transfer_memo() { bob_meta.elgamal_keypair.pubkey(), Some(auditor_elgamal_keypair.pubkey()), &[&alice], - None, ) .await .unwrap(); @@ -1614,7 +1603,6 @@ async fn confidential_transfer_transfer_with_fee_and_memo() { TEST_FEE_BASIS_POINTS, TEST_MAXIMUM_FEE, &[&alice], - None, ) .await .unwrap_err(); @@ -1646,7 +1634,6 @@ async fn confidential_transfer_transfer_with_fee_and_memo() { TEST_FEE_BASIS_POINTS, TEST_MAXIMUM_FEE, &[&alice], - None, ) .await .unwrap(); @@ -2284,9 +2271,7 @@ async fn confidential_transfer_transfer_with_proof_context() { &alice_meta.token_account, &bob_meta.token_account, &alice.pubkey(), - Some(TransferContextStateAccounts::SingleAccount( - &context_state_account.pubkey(), - )), + Some(&context_state_account.pubkey()), 42, None, &alice_meta.elgamal_keypair, @@ -2294,7 +2279,6 @@ async fn confidential_transfer_transfer_with_proof_context() { bob_meta.elgamal_keypair.pubkey(), Some(auditor_elgamal_keypair.pubkey()), &[&alice], - None, ) .await .unwrap(); @@ -2358,9 +2342,7 @@ async fn confidential_transfer_transfer_with_proof_context() { &alice_meta.token_account, &bob_meta.token_account, &alice.pubkey(), - Some(TransferContextStateAccounts::SingleAccount( - &context_state_account.pubkey(), - )), + Some(&context_state_account.pubkey()), 0, None, &alice_meta.elgamal_keypair, @@ -2368,7 +2350,6 @@ async fn confidential_transfer_transfer_with_proof_context() { bob_meta.elgamal_keypair.pubkey(), Some(auditor_elgamal_keypair.pubkey()), &[&alice], - None, ) .await .unwrap_err(); @@ -2458,140 +2439,197 @@ async fn confidential_transfer_transfer_with_split_proof_context() { .unwrap(); let context_state_authority = Keypair::new(); - - let equality_proof_context_state_account = { - let context_state_account = Keypair::new(); - let instruction_type = ProofInstruction::VerifyCiphertextCommitmentEquality; - let space = size_of::>(); - - let context_state_info = ContextStateInfo { - context_state_account: &context_state_account.pubkey(), - context_state_authority: &context_state_authority.pubkey(), - }; - - let mut ctx = context.context.lock().await; - let rent = ctx.banks_client.get_rent().await.unwrap(); - - let instructions = vec![ - system_instruction::create_account( - &ctx.payer.pubkey(), - &context_state_account.pubkey(), - rent.minimum_balance(space), - space as u64, - &zk_token_proof_program::id(), - ), - instruction_type.encode_verify_proof(Some(context_state_info), &equality_proof_data), - ]; - - let tx = Transaction::new_signed_with_payer( - &instructions, - Some(&ctx.payer.pubkey()), - &[&ctx.payer, &context_state_account], - ctx.last_blockhash, - ); - ctx.banks_client.process_transaction(tx).await.unwrap(); - - context_state_account + let equality_proof_context_state_account = Keypair::new(); + let ciphertext_validity_proof_context_state_account = Keypair::new(); + let range_proof_context_state_account = Keypair::new(); + + let transfer_context_state_accounts = TransferSplitContextStateAccounts { + equality_proof: &equality_proof_context_state_account.pubkey(), + ciphertext_validity_proof: &ciphertext_validity_proof_context_state_account.pubkey(), + range_proof: &range_proof_context_state_account.pubkey(), + authority: &context_state_authority.pubkey(), + no_op_on_uninitialized_split_context_state: false, + close_split_context_state_accounts: None, }; - let ciphertext_validity_proof_context_state_account = { - let context_state_account = Keypair::new(); - let instruction_type = ProofInstruction::VerifyBatchedGroupedCiphertext2HandlesValidity; - let space = - size_of::>(); + // create context state accounts + token + .confidential_transfer_equality_and_ciphertext_validity_proof_context_states_for_transfer( + transfer_context_state_accounts, + &equality_proof_data, + &ciphertext_validity_proof_data, + None, + &equality_proof_context_state_account, + &ciphertext_validity_proof_context_state_account, + None, + ) + .await + .unwrap(); - let context_state_info = ContextStateInfo { - context_state_account: &context_state_account.pubkey(), - context_state_authority: &context_state_authority.pubkey(), - }; + token + .confidential_transfer_range_proof_context_state_for_transfer( + transfer_context_state_accounts, + &range_proof_data, + None, + &range_proof_context_state_account, + None, + ) + .await + .unwrap(); - let mut ctx = context.context.lock().await; - let rent = ctx.banks_client.get_rent().await.unwrap(); + // create token22 confidential transfer instruction + token + .confidential_transfer_transfer_with_split_proofs( + &alice_meta.token_account, + &bob_meta.token_account, + &alice.pubkey(), + transfer_context_state_accounts, + 42, + None, + &alice_meta.aes_key, + &alice, + &source_decrypt_handles, + ) + .await + .unwrap(); - let instructions = vec![ - system_instruction::create_account( - &ctx.payer.pubkey(), - &context_state_account.pubkey(), - rent.minimum_balance(space), - space as u64, - &zk_token_proof_program::id(), - ), - instruction_type - .encode_verify_proof(Some(context_state_info), &ciphertext_validity_proof_data), - ]; + // close context state accounts + token + .confidential_transfer_close_context_state( + &equality_proof_context_state_account.pubkey(), + &alice_meta.token_account, + &context_state_authority, + ) + .await + .unwrap(); - let tx = Transaction::new_signed_with_payer( - &instructions, - Some(&ctx.payer.pubkey()), - &[&ctx.payer, &context_state_account], - ctx.last_blockhash, - ); - ctx.banks_client.process_transaction(tx).await.unwrap(); + token + .confidential_transfer_close_context_state( + &ciphertext_validity_proof_context_state_account.pubkey(), + &alice_meta.token_account, + &context_state_authority, + ) + .await + .unwrap(); - context_state_account - }; + token + .confidential_transfer_close_context_state( + &range_proof_context_state_account.pubkey(), + &alice_meta.token_account, + &context_state_authority, + ) + .await + .unwrap(); - let range_proof_context_state_account = { - let context_state_account = Keypair::new(); - let instruction_type = ProofInstruction::VerifyBatchedRangeProofU128; - let space = size_of::>(); + // check balances + alice_meta + .check_balances( + &token, + ConfidentialTokenAccountBalances { + pending_balance_lo: 0, + pending_balance_hi: 0, + available_balance: 0, + decryptable_available_balance: 0, + }, + ) + .await; - let context_state_info = ContextStateInfo { - context_state_account: &context_state_account.pubkey(), - context_state_authority: &context_state_authority.pubkey(), - }; + bob_meta + .check_balances( + &token, + ConfidentialTokenAccountBalances { + pending_balance_lo: 42, + pending_balance_hi: 0, + available_balance: 0, + decryptable_available_balance: 0, + }, + ) + .await; +} - let mut ctx = context.context.lock().await; - let rent = ctx.banks_client.get_rent().await.unwrap(); +#[tokio::test] +async fn confidential_transfer_transfer_with_split_proof_contexts_in_parallel() { + let authority = Keypair::new(); + let auto_approve_new_accounts = true; + let auditor_elgamal_keypair = ElGamalKeypair::new_rand(); + let auditor_elgamal_pubkey = (*auditor_elgamal_keypair.pubkey()).into(); - let instructions = vec![ - system_instruction::create_account( - &ctx.payer.pubkey(), - &context_state_account.pubkey(), - rent.minimum_balance(space), - space as u64, - &zk_token_proof_program::id(), - ), - instruction_type.encode_verify_proof(Some(context_state_info), &range_proof_data), - ]; + let mut context = TestContext::new().await; + context + .init_token_with_mint(vec![ + ExtensionInitializationParams::ConfidentialTransferMint { + authority: Some(authority.pubkey()), + auto_approve_new_accounts, + auditor_elgamal_pubkey: Some(auditor_elgamal_pubkey), + }, + ]) + .await + .unwrap(); - let tx = Transaction::new_signed_with_payer( - &instructions, - Some(&ctx.payer.pubkey()), - &[&ctx.payer, &context_state_account], - ctx.last_blockhash, - ); - ctx.banks_client.process_transaction(tx).await.unwrap(); + let TokenContext { + token, + alice, + bob, + mint_authority, + decimals, + .. + } = context.token_context.unwrap(); - context_state_account - }; + let alice_meta = ConfidentialTokenAccountMeta::new_with_tokens( + &token, + &alice, + None, + false, + false, + &mint_authority, + 42, + decimals, + ) + .await; - let equality_proof_account_address = equality_proof_context_state_account.pubkey(); - let ciphertext_validity_proof_account_address = - ciphertext_validity_proof_context_state_account.pubkey(); - let range_proof_account_address = range_proof_context_state_account.pubkey(); + let bob_meta = ConfidentialTokenAccountMeta::new_with_tokens( + &token, + &bob, + None, + false, + false, + &mint_authority, + 0, + decimals, + ) + .await; - let transfer_context_state_accounts = - TransferContextStateAccounts::SplitAccounts(TransferSplitContextStateAccounts { - equality_proof: &equality_proof_account_address, - ciphertext_validity_proof: &ciphertext_validity_proof_account_address, - range_proof: &range_proof_account_address, - }); + let context_state_authority = Keypair::new(); + let equality_proof_context_state_account = Keypair::new(); + let ciphertext_validity_proof_context_state_account = Keypair::new(); + let range_proof_context_state_account = Keypair::new(); + + let transfer_context_state_accounts = TransferSplitContextStateAccounts { + equality_proof: &equality_proof_context_state_account.pubkey(), + ciphertext_validity_proof: &ciphertext_validity_proof_context_state_account.pubkey(), + range_proof: &range_proof_context_state_account.pubkey(), + authority: &context_state_authority.pubkey(), + no_op_on_uninitialized_split_context_state: true, + close_split_context_state_accounts: None, + }; token - .confidential_transfer_transfer( + .confidential_transfer_transfer_with_split_proofs_in_parallel( &alice_meta.token_account, &bob_meta.token_account, &alice.pubkey(), - Some(transfer_context_state_accounts), + transfer_context_state_accounts, 42, None, &alice_meta.elgamal_keypair, &alice_meta.aes_key, bob_meta.elgamal_keypair.pubkey(), Some(auditor_elgamal_keypair.pubkey()), - &[&alice], - Some(&source_decrypt_handles), + &alice, + &equality_proof_context_state_account, + &ciphertext_validity_proof_context_state_account, + &range_proof_context_state_account, + None, ) .await .unwrap(); diff --git a/token/program-2022-test/tests/confidential_transfer_fee.rs b/token/program-2022-test/tests/confidential_transfer_fee.rs index c0a1fe87b60..37437fa4d16 100644 --- a/token/program-2022-test/tests/confidential_transfer_fee.rs +++ b/token/program-2022-test/tests/confidential_transfer_fee.rs @@ -530,7 +530,6 @@ async fn confidential_transfer_withdraw_withheld_tokens_from_mint() { transfer_fee_parameters.transfer_fee_basis_points.into(), transfer_fee_parameters.maximum_fee.into(), &[&alice], - None, ) .await .unwrap(); @@ -688,7 +687,6 @@ async fn confidential_transfer_withdraw_withheld_tokens_from_accounts() { transfer_fee_parameters.transfer_fee_basis_points.into(), transfer_fee_parameters.maximum_fee.into(), &[&alice], - None, ) .await .unwrap(); @@ -819,7 +817,6 @@ async fn confidential_transfer_withdraw_withheld_tokens_from_mint_with_proof_con transfer_fee_parameters.transfer_fee_basis_points.into(), transfer_fee_parameters.maximum_fee.into(), &[&alice], - None, ) .await .unwrap(); @@ -987,7 +984,6 @@ async fn confidential_transfer_withdraw_withheld_tokens_from_accounts_with_proof transfer_fee_parameters.transfer_fee_basis_points.into(), transfer_fee_parameters.maximum_fee.into(), &[&alice], - None, ) .await .unwrap(); @@ -1178,7 +1174,6 @@ async fn confidential_transfer_harvest_withheld_tokens_to_mint() { transfer_fee_parameters.transfer_fee_basis_points.into(), transfer_fee_parameters.maximum_fee.into(), &[&alice], - None, ) .await .unwrap(); diff --git a/token/program-2022/src/extension/confidential_transfer/instruction.rs b/token/program-2022/src/extension/confidential_transfer/instruction.rs index 8c7d4a5e0fb..8bea904d0e5 100644 --- a/token/program-2022/src/extension/confidential_transfer/instruction.rs +++ b/token/program-2022/src/extension/confidential_transfer/instruction.rs @@ -228,26 +228,18 @@ pub enum ConfidentialTransferInstruction { /// 1. `[writable]` The source SPL Token account. /// 2. `[writable]` The destination SPL Token account. /// 3. `[]` The token mint. - /// 4. `[]` There are three possible choices for this account. If the proof instruction - /// `VerifyTransfer` or `VerifyTransferWithFee` is included in the same transaction, then - /// this account must be instructions sysvar. If `VerifyTransfer` or - /// `VerifyTransferWithFee` instructions are pre-verified in a context state account, then - /// this account must be the context state account. Finally, if the `VerifyTransfer` or - /// `VerifyTransferWithFee` instructions are split into smaller proof components that are - /// pre-verified in context state accounts, then these instructions must include the - /// following context state accounts: - /// 4.1. `[]` Context state account for `VerifyCiphertextCommitmentEqualityProof`. - /// 4.2. `[]` Context state account for `VerifyBatchedGroupedCiphertext2HandlesValidityProof`. - /// 4.3. `[]` Context state account for `VerifyBatchedRangeProofU128`. - /// 4.4. `[]` Context state account for `VerifyFeeSigmaProof` (if transferring with fee). + /// 4. `[]` Instructions sysvar if `VerifyTransfer` or `VerifyTransferWithFee` is included in + /// the same transaction or context state account if these proofs are pre-verified into a + /// context state account. /// 5. `[signer]` The single source account owner. /// /// * Multisignature owner/delegate /// 1. `[writable]` The source SPL Token account. /// 2. `[writable]` The destination SPL Token account. /// 3. `[]` The token mint. - /// 4. `[]` One of instructions sysvar, context state account for `VerifyTransfer` or - /// `VerifyTransferWithFee`, or the set of context state accounts listed above. + /// 4. `[]` Instructions sysvar if `VerifyTransfer` or `VerifyTransferWithFee` is included in + /// the same transaction or context state account if these proofs are pre-verified into a + /// context state account. /// 5. `[]` The multisig source account owner. /// 6.. `[signer]` Required M signer accounts for the SPL Token Multisig account. /// @@ -364,6 +356,50 @@ pub enum ConfidentialTransferInstruction { /// None /// DisableNonConfidentialCredits, + + /// Transfer tokens confidentially with zero-knowledge proofs that are split into smaller + /// components. + /// + /// In order for this instruction to be successfully processed, it must be accompanied by + /// suitable zero-knowledge proof context accounts listed below. + /// + /// The same restrictions for the `Transfer` applies to `TransferWithSplitProofs`. Namely, the + /// instruction fails if the associated mint is extended as `NonTransferable`. + /// + /// * Transfer without fee + /// 1. `[writable]` The source SPL Token account. + /// 2. `[writable]` The destination SPL Token account. + /// 3. `[]` The token mint. + /// 4. `[]` Context state account for `VerifyCiphertextCommitmentEqualityProof`. + /// 5. `[]` Context state account for `VerifyBatchedGroupedCiphertext2HandlesValidityProof`. + /// 6. `[]` Context state account for `VerifyBatchedRangeProofU128`. + /// 7. `[signer]` The source account owner. + /// If `close_split_context_state_on_execution` is set, all context state accounts must be + /// `writable` and the following additional sequence of accounts are needed: + /// 8. `[]` The destination account for lamports from the context state accounts. + /// 9. `[signer]` The context state account owner. + /// 10. `[]` The zk token proof program. + /// + /// * Transfer with fee + /// 1. `[writable]` The source SPL Token account. + /// 2. `[writable]` The destination SPL Token account. + /// 3. `[]` The token mint. + /// 4. `[]` Context state account for `VerifyCiphertextCommitmentEqualityProof`. + /// 5. `[]` Context state account for `VerifyBatchedGroupedCiphertext2HandlesValidityProof`. + /// 6. `[]` Context state account for `VerifyFeeSigmaProof`. + /// 7. `[]` Context state account for `VerifyBatchedGroupedCiphertext2HandlesValidityProof`. + /// 8. `[]` Context state account for `VerifyBatchedRangeProofU256`. + /// 9. `[signer]` The source account owner. + /// If `close_split_context_state_on_execution` is set, all context state accounts must be + /// `writable` and the following additional sequence of accounts are needed: + /// 10. `[]` The destination account for lamports from the context state accounts. + /// 11. `[signer]` The context state account owner. + /// 12. `[]` The zk token proof program. + /// + /// Data expected by this instruction: + /// `TransferWithSplitProofsInstructionData` + /// + TransferWithSplitProofs, } /// Data expected by `ConfidentialTransferInstruction::InitializeMint` @@ -462,14 +498,6 @@ pub struct TransferInstructionData { /// `Transfer` instruction in the transaction. If the offset is `0`, then use a context state /// account for the proof. pub proof_instruction_offset: i8, - /// Split the transfer proof into smaller components that are verified individually. - pub split_proof_context_state_accounts: PodBool, - /// The ElGamal decryption handle pertaining to the low and high bits of the transfer amount. - /// This field is used when the transfer proofs are split and verified as smaller components. - /// If the transfer proof is not split, this field should be zeroed out. - /// - /// NOTE: This field is to be removed in the next Solana upgrade. - pub source_decrypt_handles: SourceDecryptHandles, } /// Data expected by `ConfidentialTransferInstruction::ApplyPendingBalance` @@ -485,17 +513,29 @@ pub struct ApplyPendingBalanceData { pub new_decryptable_available_balance: DecryptableBalance, } -/// Type for transfer instruction proof context state account addresses intended to be used as -/// parameters to functions. -pub enum TransferContextStateAccounts<'a> { - /// The context state account address for a single transfer proof context. - SingleAccount(&'a Pubkey), - /// The context state account addresses for the context states of a split transfer proof. - SplitAccounts(TransferSplitContextStateAccounts<'a>), +/// Data expected by `ConfidentialTransferInstruction::TransferWithSplitProofs` +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] +#[repr(C)] +pub struct TransferWithSplitProofsInstructionData { + /// The new source decryptable balance if the transfer succeeds + #[cfg_attr(feature = "serde-traits", serde(with = "aeciphertext_fromstr"))] + pub new_source_decryptable_available_balance: DecryptableBalance, + /// If true, execute no op when an associated context state account is not initialized. + /// Otherwise, fail on an uninitialized context state account. + pub no_op_on_uninitialized_split_context_state: PodBool, + /// Close associated context states after a complete execution of the transfer instruction. + pub close_split_context_state_on_execution: PodBool, + /// The ElGamal decryption handle pertaining to the low and high bits of the transfer amount. + /// This field is used when the transfer proofs are split and verified as smaller components. + /// + /// NOTE: This field is to be removed in the next Solana upgrade. + pub source_decrypt_handles: SourceDecryptHandles, } /// Type for split transfer instruction proof context state account addresses intended to be used /// as parameters to functions. +#[derive(Clone, Copy)] pub struct TransferSplitContextStateAccounts<'a> { /// The context state account address for an equality proof needed for a transfer. pub equality_proof: &'a Pubkey, @@ -503,6 +543,21 @@ pub struct TransferSplitContextStateAccounts<'a> { pub ciphertext_validity_proof: &'a Pubkey, /// The context state account address for a range proof needed for a transfer. pub range_proof: &'a Pubkey, + /// The context state accounts authority + pub authority: &'a Pubkey, + /// No op if an associated split proof context state account is not initialized. + pub no_op_on_uninitialized_split_context_state: bool, + /// Accounts needed if `close_split_context_state_on_execution` flag is enabled. + pub close_split_context_state_accounts: Option>, +} + +/// Accounts needed if `close_split_context_state_on_execution` flag is enabled on a transfer. +#[derive(Clone, Copy)] +pub struct CloseSplitContextStateAccounts<'a> { + /// The lamport destination account. + pub lamport_destination: &'a Pubkey, + /// The ZK Token proof program. + pub zk_token_proof_program: &'a Pubkey, } /// Create a `InitializeMint` instruction @@ -591,9 +646,6 @@ pub fn inner_configure_account( accounts.push(AccountMeta::new_readonly(*context_state_account, false)); 0 } - ProofLocation::SplitContextStateAccounts(_) => { - return Err(TokenError::SplitProofContextStateAccountsNotSupported.into()) - } }; accounts.push(AccountMeta::new_readonly( @@ -707,9 +759,6 @@ pub fn inner_empty_account( accounts.push(AccountMeta::new_readonly(*context_state_account, false)); 0 } - ProofLocation::SplitContextStateAccounts(_) => { - return Err(TokenError::SplitProofContextStateAccountsNotSupported.into()) - } }; accounts.push(AccountMeta::new_readonly( @@ -828,9 +877,6 @@ pub fn inner_withdraw( accounts.push(AccountMeta::new_readonly(*context_state_account, false)); 0 } - ProofLocation::SplitContextStateAccounts(_) => { - return Err(TokenError::SplitProofContextStateAccountsNotSupported.into()) - } }; accounts.push(AccountMeta::new_readonly( @@ -911,7 +957,6 @@ pub fn inner_transfer( authority: &Pubkey, multisig_signers: &[&Pubkey], proof_data_location: ProofLocation, - source_decrypt_handles: Option<&SourceDecryptHandles>, ) -> Result { check_program_account(token_program_id)?; let mut accounts = vec![ @@ -920,28 +965,14 @@ pub fn inner_transfer( AccountMeta::new_readonly(*mint, false), ]; - let (proof_instruction_offset, split_proof_context_state_accounts) = match proof_data_location { + let proof_instruction_offset = match proof_data_location { ProofLocation::InstructionOffset(proof_instruction_offset, _) => { accounts.push(AccountMeta::new_readonly(sysvar::instructions::id(), false)); - (proof_instruction_offset.into(), false) + proof_instruction_offset.into() } ProofLocation::ContextStateAccount(context_state_account) => { accounts.push(AccountMeta::new_readonly(*context_state_account, false)); - (0, false) - } - ProofLocation::SplitContextStateAccounts(context_state_accounts) => { - // Split proof context state accounts must consist of: - // - `VerifyCiphertextCommitmentEqualityProof`, - // - `VerifyBatchedGroupedCiphertext2HandlesValidityProof` - // - `VerifyBatchedRangeProofU128` - if context_state_accounts.len() != 3 { - return Err(TokenError::NotEnoughProofContextStateAccounts.into()); - } - - for context_state_account in context_state_accounts { - accounts.push(AccountMeta::new_readonly(**context_state_account, false)); - } - (0, true) + 0 } }; @@ -954,12 +985,6 @@ pub fn inner_transfer( accounts.push(AccountMeta::new_readonly(**multisig_signer, true)); } - let source_decrypt_handles = if let Some(source_decrypt_handles) = source_decrypt_handles { - *source_decrypt_handles - } else { - SourceDecryptHandles::zeroed() - }; - Ok(encode_instruction( token_program_id, accounts, @@ -968,8 +993,6 @@ pub fn inner_transfer( &TransferInstructionData { new_source_decryptable_available_balance, proof_instruction_offset, - split_proof_context_state_accounts: split_proof_context_state_accounts.into(), - source_decrypt_handles, }, )) } @@ -986,7 +1009,6 @@ pub fn transfer( authority: &Pubkey, multisig_signers: &[&Pubkey], proof_data_location: ProofLocation, - source_decrypt_handles: Option<&SourceDecryptHandles>, ) -> Result, ProgramError> { let mut instructions = vec![inner_transfer( token_program_id, @@ -997,16 +1019,14 @@ pub fn transfer( authority, multisig_signers, proof_data_location, - source_decrypt_handles, )?]; if let ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) = proof_data_location { - // This constructor appends the proof instruction right after the `ConfigureAccount` - // instruction. This means that the proof instruction offset must be always be 1. To - // use an arbitrary proof instruction offset, use the `inner_configure_account` - // constructor. + // This constructor appends the proof instruction right after the `Transfer` instruction. + // This means that the proof instruction offset must be always be 1. To use an arbitrary + // proof instruction offset, use the `inner_transfer` constructor. let proof_instruction_offset: i8 = proof_instruction_offset.into(); if proof_instruction_offset != 1 { return Err(TokenError::InvalidProofInstructionOffset.into()); @@ -1017,7 +1037,7 @@ pub fn transfer( Ok(instructions) } -/// Create a inner `TransferWithFee` instruction +/// Create a inner `Transfer` instruction with fee /// /// This instruction is suitable for use with a cross-program `invoke` #[allow(clippy::too_many_arguments)] @@ -1030,7 +1050,6 @@ pub fn inner_transfer_with_fee( authority: &Pubkey, multisig_signers: &[&Pubkey], proof_data_location: ProofLocation, - source_decrypt_handles: Option<&SourceDecryptHandles>, ) -> Result { check_program_account(token_program_id)?; let mut accounts = vec![ @@ -1039,29 +1058,14 @@ pub fn inner_transfer_with_fee( AccountMeta::new_readonly(*mint, false), ]; - let (proof_instruction_offset, split_proof_context_state_accounts) = match proof_data_location { + let proof_instruction_offset = match proof_data_location { ProofLocation::InstructionOffset(proof_instruction_offset, _) => { accounts.push(AccountMeta::new_readonly(sysvar::instructions::id(), false)); - (proof_instruction_offset.into(), false) + proof_instruction_offset.into() } ProofLocation::ContextStateAccount(context_state_account) => { accounts.push(AccountMeta::new_readonly(*context_state_account, false)); - (0, false) - } - ProofLocation::SplitContextStateAccounts(context_state_accounts) => { - // Split proof context state accounts must consist of: - // - `VerifyCiphertextCommitmentEqualityProof`, - // - `VerifyBatchedGroupedCiphertext2HandlesValidityProof` - // - `VerifyBatchedRangeProofU128` - // - `VerifyFeeSigmaProof` - if context_state_accounts.len() != 4 { - return Err(TokenError::NotEnoughProofContextStateAccounts.into()); - } - - for context_state_account in context_state_accounts { - accounts.push(AccountMeta::new_readonly(**context_state_account, false)); - } - (0, true) + 0 } }; @@ -1074,12 +1078,6 @@ pub fn inner_transfer_with_fee( accounts.push(AccountMeta::new_readonly(**multisig_signer, true)); } - let source_decrypt_handles = if let Some(source_decrypt_handles) = source_decrypt_handles { - *source_decrypt_handles - } else { - SourceDecryptHandles::zeroed() - }; - Ok(encode_instruction( token_program_id, accounts, @@ -1088,8 +1086,6 @@ pub fn inner_transfer_with_fee( &TransferInstructionData { new_source_decryptable_available_balance, proof_instruction_offset, - split_proof_context_state_accounts: split_proof_context_state_accounts.into(), - source_decrypt_handles, }, )) } @@ -1106,7 +1102,6 @@ pub fn transfer_with_fee( authority: &Pubkey, multisig_signers: &[&Pubkey], proof_data_location: ProofLocation, - source_decrypt_handles: Option<&SourceDecryptHandles>, ) -> Result, ProgramError> { let mut instructions = vec![inner_transfer_with_fee( token_program_id, @@ -1117,7 +1112,6 @@ pub fn transfer_with_fee( authority, multisig_signers, proof_data_location, - source_decrypt_handles, )?]; if let ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) = @@ -1279,3 +1273,82 @@ pub fn disable_non_confidential_credits( multisig_signers, ) } + +/// Create a `TransferWithSplitProof` instruction without fee +#[allow(clippy::too_many_arguments)] +#[cfg(not(target_os = "solana"))] +pub fn transfer_with_split_proofs( + token_program_id: &Pubkey, + source_token_account: &Pubkey, + destination_token_account: &Pubkey, + mint: &Pubkey, + new_source_decryptable_available_balance: DecryptableBalance, + source_account_authority: &Pubkey, + context_accounts: TransferSplitContextStateAccounts, + source_decrypt_handles: &SourceDecryptHandles, +) -> Result { + check_program_account(token_program_id)?; + let mut accounts = vec![ + AccountMeta::new(*source_token_account, false), + AccountMeta::new(*destination_token_account, false), + AccountMeta::new_readonly(*mint, false), + ]; + + let close_split_context_state_on_execution = + if let Some(close_split_context_state_on_execution_accounts) = + context_accounts.close_split_context_state_accounts + { + // If `close_split_context_state_accounts` is set, then all context state accounts must + // be `writable`. + accounts.push(AccountMeta::new(*context_accounts.equality_proof, false)); + accounts.push(AccountMeta::new( + *context_accounts.ciphertext_validity_proof, + false, + )); + accounts.push(AccountMeta::new(*context_accounts.range_proof, false)); + accounts.push(AccountMeta::new_readonly(*source_account_authority, true)); + accounts.push(AccountMeta::new( + *close_split_context_state_on_execution_accounts.lamport_destination, + false, + )); + accounts.push(AccountMeta::new_readonly(*context_accounts.authority, true)); + accounts.push(AccountMeta::new_readonly( + *close_split_context_state_on_execution_accounts.zk_token_proof_program, + false, + )); + true + } else { + // If `close_split_context_state_accounts` is not set, then context state accounts can + // be read-only. + accounts.push(AccountMeta::new_readonly( + *context_accounts.equality_proof, + false, + )); + accounts.push(AccountMeta::new_readonly( + *context_accounts.ciphertext_validity_proof, + false, + )); + accounts.push(AccountMeta::new_readonly( + *context_accounts.range_proof, + false, + )); + accounts.push(AccountMeta::new_readonly(*source_account_authority, true)); + + false + }; + + Ok(encode_instruction( + token_program_id, + accounts, + TokenInstruction::ConfidentialTransferExtension, + ConfidentialTransferInstruction::TransferWithSplitProofs, + &TransferWithSplitProofsInstructionData { + new_source_decryptable_available_balance, + no_op_on_uninitialized_split_context_state: context_accounts + .no_op_on_uninitialized_split_context_state + .into(), + close_split_context_state_on_execution: close_split_context_state_on_execution.into(), + source_decrypt_handles: *source_decrypt_handles, + }, + )) +} diff --git a/token/program-2022/src/extension/confidential_transfer/processor.rs b/token/program-2022/src/extension/confidential_transfer/processor.rs index 0c0aee9ab5f..91616efc96d 100644 --- a/token/program-2022/src/extension/confidential_transfer/processor.rs +++ b/token/program-2022/src/extension/confidential_transfer/processor.rs @@ -417,7 +417,8 @@ fn process_withdraw( Ok(()) } -/// Processes an [Transfer] instruction. +/// Processes a [Transfer] or [TransferWithSplitProofs] instruction. +#[allow(clippy::too_many_arguments)] #[cfg(feature = "zk-ops")] fn process_transfer( program_id: &Pubkey, @@ -425,6 +426,8 @@ fn process_transfer( new_source_decryptable_available_balance: DecryptableBalance, proof_instruction_offset: i64, split_proof_context_state_accounts: bool, + no_op_on_uninitialized_split_context_state: bool, + close_split_context_state_on_execution: bool, source_decrypt_handles: &SourceDecryptHandles, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); @@ -454,55 +457,64 @@ fn process_transfer( // The zero-knowledge proof certifies that: // 1. the transfer amount is encrypted in the correct form // 2. the source account has enough balance to send the transfer amount - let proof_context = verify_transfer_proof( + let maybe_proof_context = verify_transfer_proof( account_info_iter, proof_instruction_offset, split_proof_context_state_accounts, + no_op_on_uninitialized_split_context_state, + close_split_context_state_on_execution, source_decrypt_handles, )?; + // If `maybe_proof_context` is `None`, then this means that + // `no_op_on_uninitialized_split_context_state` is true and a required context state + // account is not yet initialized. Even if this is the case, we follow through with the + // rest of the transfer logic to perform all the necessary checks for a transfer to be + // safe. + + // If `close_split_context_state_on_execution` is `true`, then the source account authority + // info is located after the lamport destination, context state authority, and zk token + // proof program account infos. Flush out these account infos. + if close_split_context_state_on_execution && maybe_proof_context.is_none() { + let _lamport_destination_account_info = next_account_info(account_info_iter)?; + let _context_state_authority_info = next_account_info(account_info_iter)?; + let _zk_token_proof_program_info = next_account_info(account_info_iter)?; + } let authority_info = next_account_info(account_info_iter)?; // Check that the auditor encryption public key associated wth the confidential mint is // consistent with what was actually used to generate the zkp. - if !confidential_transfer_mint - .auditor_elgamal_pubkey - .equals(&proof_context.transfer_pubkeys.auditor) - { - return Err(TokenError::ConfidentialTransferElGamalPubkeyMismatch.into()); + if let Some(ref proof_context) = maybe_proof_context { + if !confidential_transfer_mint + .auditor_elgamal_pubkey + .equals(&proof_context.transfer_pubkeys.auditor) + { + return Err(TokenError::ConfidentialTransferElGamalPubkeyMismatch.into()); + } } - let source_transfer_amount_lo = - transfer_amount_source_ciphertext(&proof_context.ciphertext_lo); - let source_transfer_amount_hi = - transfer_amount_source_ciphertext(&proof_context.ciphertext_hi); - process_source_for_transfer( program_id, source_account_info, mint_info, authority_info, account_info_iter.as_slice(), - &proof_context.transfer_pubkeys.source, - &source_transfer_amount_lo, - &source_transfer_amount_hi, - &proof_context.new_source_ciphertext, + maybe_proof_context.as_ref(), new_source_decryptable_available_balance, )?; - let destination_ciphertext_lo = - transfer_amount_destination_ciphertext(&proof_context.ciphertext_lo); - let destination_ciphertext_hi = - transfer_amount_destination_ciphertext(&proof_context.ciphertext_hi); - process_destination_for_transfer( destination_token_account_info, mint_info, - &proof_context.transfer_pubkeys.destination, - &destination_ciphertext_lo, - &destination_ciphertext_hi, - None, + maybe_proof_context.as_ref(), )?; + + if maybe_proof_context.is_none() { + msg!( + "Context states not fully initialized: returning with no op; transfer is NOT yet + executed" + ); + } } else { // Transfer fee is required. Decode the zero-knowledge proof as `TransferWithFeeData`. // @@ -556,7 +568,7 @@ fn process_transfer( let source_transfer_amount_hi = transfer_amount_source_ciphertext(&proof_context.ciphertext_hi); - process_source_for_transfer( + process_source_for_transfer_with_fee( program_id, source_account_info, mint_info, @@ -585,7 +597,7 @@ fn process_transfer( None }; - process_destination_for_transfer( + process_destination_for_transfer_with_fee( destination_token_account_info, mint_info, &proof_context.transfer_with_fee_pubkeys.destination, @@ -598,9 +610,134 @@ fn process_transfer( Ok(()) } -#[allow(clippy::too_many_arguments)] #[cfg(feature = "zk-ops")] fn process_source_for_transfer( + program_id: &Pubkey, + source_account_info: &AccountInfo, + mint_info: &AccountInfo, + authority_info: &AccountInfo, + signers: &[AccountInfo], + maybe_proof_context: Option<&TransferProofContextInfo>, + new_source_decryptable_available_balance: DecryptableBalance, +) -> ProgramResult { + check_program_account(source_account_info.owner)?; + let authority_info_data_len = authority_info.data_len(); + let token_account_data = &mut source_account_info.data.borrow_mut(); + let mut token_account = StateWithExtensionsMut::::unpack(token_account_data)?; + + Processor::validate_owner( + program_id, + &token_account.base.owner, + authority_info, + authority_info_data_len, + signers, + )?; + + if token_account.base.is_frozen() { + return Err(TokenError::AccountFrozen.into()); + } + + if token_account.base.mint != *mint_info.key { + return Err(TokenError::MintMismatch.into()); + } + + let mut confidential_transfer_account = + token_account.get_extension_mut::()?; + confidential_transfer_account.valid_as_source()?; + + if let Some(proof_context) = maybe_proof_context { + // Check that the source encryption public key is consistent with what was actually used to + // generate the zkp. + if proof_context.transfer_pubkeys.source != confidential_transfer_account.elgamal_pubkey { + return Err(TokenError::ConfidentialTransferElGamalPubkeyMismatch.into()); + } + + let source_transfer_amount_lo = + transfer_amount_source_ciphertext(&proof_context.ciphertext_lo); + let source_transfer_amount_hi = + transfer_amount_source_ciphertext(&proof_context.ciphertext_hi); + + let new_source_available_balance = syscall::subtract_with_lo_hi( + &confidential_transfer_account.available_balance, + &source_transfer_amount_lo, + &source_transfer_amount_hi, + ) + .ok_or(TokenError::CiphertextArithmeticFailed)?; + + // Check that the computed available balance is consistent with what was actually used to + // generate the zkp on the client side. + if new_source_available_balance != proof_context.new_source_ciphertext { + return Err(TokenError::ConfidentialTransferBalanceMismatch.into()); + } + + confidential_transfer_account.available_balance = new_source_available_balance; + confidential_transfer_account.decryptable_available_balance = + new_source_decryptable_available_balance; + } + + Ok(()) +} + +#[cfg(feature = "zk-ops")] +fn process_destination_for_transfer( + destination_token_account_info: &AccountInfo, + mint_info: &AccountInfo, + maybe_transfer_proof_context_info: Option<&TransferProofContextInfo>, +) -> ProgramResult { + check_program_account(destination_token_account_info.owner)?; + let destination_token_account_data = &mut destination_token_account_info.data.borrow_mut(); + let mut destination_token_account = + StateWithExtensionsMut::::unpack(destination_token_account_data)?; + + if destination_token_account.base.is_frozen() { + return Err(TokenError::AccountFrozen.into()); + } + + if destination_token_account.base.mint != *mint_info.key { + return Err(TokenError::MintMismatch.into()); + } + + if memo_required(&destination_token_account) { + check_previous_sibling_instruction_is_memo()?; + } + + let mut destination_confidential_transfer_account = + destination_token_account.get_extension_mut::()?; + destination_confidential_transfer_account.valid_as_destination()?; + + if let Some(proof_context) = maybe_transfer_proof_context_info { + if proof_context.transfer_pubkeys.destination + != destination_confidential_transfer_account.elgamal_pubkey + { + return Err(TokenError::ConfidentialTransferElGamalPubkeyMismatch.into()); + } + + let destination_ciphertext_lo = + transfer_amount_destination_ciphertext(&proof_context.ciphertext_lo); + let destination_ciphertext_hi = + transfer_amount_destination_ciphertext(&proof_context.ciphertext_hi); + + destination_confidential_transfer_account.pending_balance_lo = syscall::add( + &destination_confidential_transfer_account.pending_balance_lo, + &destination_ciphertext_lo, + ) + .ok_or(TokenError::CiphertextArithmeticFailed)?; + + destination_confidential_transfer_account.pending_balance_hi = syscall::add( + &destination_confidential_transfer_account.pending_balance_hi, + &destination_ciphertext_hi, + ) + .ok_or(TokenError::CiphertextArithmeticFailed)?; + + destination_confidential_transfer_account.increment_pending_balance_credit_counter()?; + } + + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +#[cfg(feature = "zk-ops")] +fn process_source_for_transfer_with_fee( program_id: &Pubkey, source_account_info: &AccountInfo, mint_info: &AccountInfo, @@ -664,7 +801,7 @@ fn process_source_for_transfer( } #[cfg(feature = "zk-ops")] -fn process_destination_for_transfer( +fn process_destination_for_transfer_with_fee( destination_token_account_info: &AccountInfo, mint_info: &AccountInfo, destination_encryption_pubkey: &ElGamalPubkey, @@ -944,8 +1081,10 @@ pub(crate) fn process_instruction( accounts, data.new_source_decryptable_available_balance, data.proof_instruction_offset as i64, - data.split_proof_context_state_accounts.into(), - &data.source_decrypt_handles, + false, + false, + false, + &SourceDecryptHandles::zeroed(), ) } ConfidentialTransferInstruction::ApplyPendingBalance => { @@ -979,5 +1118,27 @@ pub(crate) fn process_instruction( msg!("ConfidentialTransferInstruction::EnableNonConfidentialCredits"); process_allow_non_confidential_credits(program_id, accounts, true) } + ConfidentialTransferInstruction::TransferWithSplitProofs => { + msg!("ConfidentialTransferInstruction::TransferWithSplitProofs"); + #[cfg(feature = "zk-ops")] + let data = decode_instruction_data::(input)?; + + // Remove this error on the next Solana version upgrade. + if data.close_split_context_state_on_execution.into() { + msg!("Auto-close of context state account is not yet supported"); + return Err(ProgramError::InvalidInstructionData); + } + + process_transfer( + program_id, + accounts, + data.new_source_decryptable_available_balance, + 0, + true, + data.no_op_on_uninitialized_split_context_state.into(), + data.close_split_context_state_on_execution.into(), + &data.source_decrypt_handles, + ) + } } } diff --git a/token/program-2022/src/extension/confidential_transfer/verify_proof.rs b/token/program-2022/src/extension/confidential_transfer/verify_proof.rs index 662d27093c1..5050b09bbb2 100644 --- a/token/program-2022/src/extension/confidential_transfer/verify_proof.rs +++ b/token/program-2022/src/extension/confidential_transfer/verify_proof.rs @@ -1,14 +1,17 @@ use { crate::{ - check_zk_token_proof_program_account, + check_system_program_account, check_zk_token_proof_program_account, extension::confidential_transfer::{ciphertext_extraction::*, instruction::*, *}, proof::decode_proof_instruction_context, }, solana_program::{ account_info::{next_account_info, AccountInfo}, + msg, + program::invoke, program_error::ProgramError, sysvar::instructions::get_instruction_relative, }, + solana_zk_token_sdk::zk_token_proof_instruction::{self, ContextStateInfo}, std::slice::Iter, }; @@ -115,31 +118,118 @@ pub fn verify_withdraw_proof( /// Verify zero-knowledge proof needed for a [Transfer] instruction without fee and return the /// corresponding proof context. +/// +/// This returns a `Result` type for an `Option` type. If the proof +/// verification fails, then the function returns a suitable error variant. If the proof succeeds +/// to verify, then the function returns a `TransferProofContextInfo` that is wrapped inside +/// `Ok(Some(TransferProofContextInfo))`. If `no_op_on_split_proof_context_state` is `true` and +/// some a split context state account is not initialized, then it returns `Ok(None)`. pub fn verify_transfer_proof( account_info_iter: &mut Iter<'_, AccountInfo<'_>>, proof_instruction_offset: i64, split_proof_context_state_accounts: bool, + no_op_on_split_proof_context_state: bool, + close_split_context_state_on_execution: bool, source_decrypt_handles: &SourceDecryptHandles, -) -> Result { +) -> Result, ProgramError> { if proof_instruction_offset == 0 && split_proof_context_state_accounts { let equality_proof_context_state_account_info = next_account_info(account_info_iter)?; - let equality_proof_context = - verify_equality_proof(equality_proof_context_state_account_info)?; - let ciphertext_validity_proof_context_state_account_info = next_account_info(account_info_iter)?; + let range_proof_context_state_account_info = next_account_info(account_info_iter)?; + + if no_op_on_split_proof_context_state + && check_system_program_account(equality_proof_context_state_account_info.owner).is_ok() + { + msg!("Equality proof context state account not initialized"); + return Ok(None); + } + + if no_op_on_split_proof_context_state + && check_system_program_account( + ciphertext_validity_proof_context_state_account_info.owner, + ) + .is_ok() + { + msg!("Ciphertext validity proof context state account not initialized"); + return Ok(None); + } + + if no_op_on_split_proof_context_state + && check_system_program_account(range_proof_context_state_account_info.owner).is_ok() + { + msg!("Range proof context state account not initialized"); + return Ok(None); + } + + let equality_proof_context = + verify_equality_proof(equality_proof_context_state_account_info)?; let ciphertext_validity_proof_context = verify_ciphertext_validity_proof(ciphertext_validity_proof_context_state_account_info)?; - - let range_proof_context_state_account_info = next_account_info(account_info_iter)?; let range_proof_context = verify_range_proof(range_proof_context_state_account_info)?; - Ok(TransferProofContextInfo::new( + let transfer_proof_context = TransferProofContextInfo::new( &equality_proof_context, &ciphertext_validity_proof_context, &range_proof_context, source_decrypt_handles, - )?) + )?; + + if close_split_context_state_on_execution { + let lamport_destination_account_info = next_account_info(account_info_iter)?; + let context_state_account_authority_info = next_account_info(account_info_iter)?; + + msg!("Closing equality proof context state account"); + invoke( + &zk_token_proof_instruction::close_context_state( + ContextStateInfo { + context_state_account: equality_proof_context_state_account_info.key, + context_state_authority: context_state_account_authority_info.key, + }, + lamport_destination_account_info.key, + ), + &[ + equality_proof_context_state_account_info.clone(), + lamport_destination_account_info.clone(), + context_state_account_authority_info.clone(), + ], + )?; + + msg!("Closing ciphertext validity proof context state account"); + invoke( + &zk_token_proof_instruction::close_context_state( + ContextStateInfo { + context_state_account: ciphertext_validity_proof_context_state_account_info + .key, + context_state_authority: context_state_account_authority_info.key, + }, + lamport_destination_account_info.key, + ), + &[ + ciphertext_validity_proof_context_state_account_info.clone(), + lamport_destination_account_info.clone(), + context_state_account_authority_info.clone(), + ], + )?; + + msg!("Closing range proof context state account"); + invoke( + &zk_token_proof_instruction::close_context_state( + ContextStateInfo { + context_state_account: range_proof_context_state_account_info.key, + context_state_authority: context_state_account_authority_info.key, + }, + lamport_destination_account_info.key, + ), + &[ + range_proof_context_state_account_info.clone(), + lamport_destination_account_info.clone(), + context_state_account_authority_info.clone(), + ], + )?; + } + + Ok(Some(transfer_proof_context)) } else if proof_instruction_offset == 0 && !split_proof_context_state_accounts { // interpret `account_info` as a context state account let context_state_account_info = next_account_info(account_info_iter)?; @@ -152,19 +242,19 @@ pub fn verify_transfer_proof( return Err(ProgramError::InvalidInstructionData); } - Ok(context_state.proof_context.into()) + Ok(Some(context_state.proof_context.into())) } else { // interpret `account_info` as sysvar let sysvar_account_info = next_account_info(account_info_iter)?; let zkp_instruction = get_instruction_relative(proof_instruction_offset, sysvar_account_info)?; - Ok( - (*decode_proof_instruction_context::( - ProofInstruction::VerifyTransfer, - &zkp_instruction, - )?) - .into(), - ) + let proof_context = (*decode_proof_instruction_context::< + TransferData, + TransferProofContext, + >(ProofInstruction::VerifyTransfer, &zkp_instruction)?) + .into(); + + Ok(Some(proof_context)) } } diff --git a/token/program-2022/src/extension/confidential_transfer_fee/instruction.rs b/token/program-2022/src/extension/confidential_transfer_fee/instruction.rs index c22a7e2bfa9..650405179c2 100644 --- a/token/program-2022/src/extension/confidential_transfer_fee/instruction.rs +++ b/token/program-2022/src/extension/confidential_transfer_fee/instruction.rs @@ -288,9 +288,6 @@ pub fn inner_withdraw_withheld_tokens_from_mint( accounts.push(AccountMeta::new_readonly(*context_state_account, false)); 0 } - ProofLocation::SplitContextStateAccounts(_) => { - return Err(TokenError::SplitProofContextStateAccountsNotSupported.into()) - } }; accounts.push(AccountMeta::new_readonly( @@ -382,9 +379,6 @@ pub fn inner_withdraw_withheld_tokens_from_accounts( accounts.push(AccountMeta::new_readonly(*context_state_account, false)); 0 } - ProofLocation::SplitContextStateAccounts(_) => { - return Err(TokenError::SplitProofContextStateAccountsNotSupported.into()) - } }; accounts.push(AccountMeta::new_readonly( diff --git a/token/program-2022/src/lib.rs b/token/program-2022/src/lib.rs index ac0c6d241d4..783e2c597ea 100644 --- a/token/program-2022/src/lib.rs +++ b/token/program-2022/src/lib.rs @@ -28,6 +28,7 @@ use solana_program::{ program_error::ProgramError, program_memory::sol_memcmp, pubkey::{Pubkey, PUBKEY_BYTES}, + system_program, }; pub use solana_zk_token_sdk; @@ -117,6 +118,14 @@ pub fn check_zk_token_proof_program_account(zk_token_proof_program_id: &Pubkey) Ok(()) } +/// Checks if the spplied program ID is that of the system program +pub fn check_system_program_account(system_program_id: &Pubkey) -> ProgramResult { + if system_program_id != &system_program::id() { + return Err(ProgramError::IncorrectProgramId); + } + Ok(()) +} + /// Checks two pubkeys for equality in a computationally cheap way using /// `sol_memcmp` pub fn cmp_pubkeys(a: &Pubkey, b: &Pubkey) -> bool { diff --git a/token/program-2022/src/proof.rs b/token/program-2022/src/proof.rs index da335846f89..ac72c5020ec 100644 --- a/token/program-2022/src/proof.rs +++ b/token/program-2022/src/proof.rs @@ -34,7 +34,14 @@ pub enum ProofLocation<'a, T> { InstructionOffset(NonZeroI8, &'a T), /// The proof is pre-verified into a context state account. ContextStateAccount(&'a Pubkey), - /// The proof is split into multiple smaller components and are pre-verified into context state - /// accounts. - SplitContextStateAccounts(&'a [&'a Pubkey]), +} + +/// Instruction options for when using split context state accounts +#[derive(Clone, Copy)] +pub struct SplitContextStateAccountsConfig { + /// If true, execute no op when an associated split proof context state account is not + /// initialized. Otherwise, fail on an uninitialized context state account. + pub no_op_on_uninitialized_split_context_state: bool, + /// Close associated context states after a complete execution of the transfer instruction. + pub close_split_context_state_on_execution: bool, }