diff --git a/token/client/src/token.rs b/token/client/src/token.rs index 606a5a2a432..a0ce89ecfdc 100644 --- a/token/client/src/token.rs +++ b/token/client/src/token.rs @@ -27,6 +27,8 @@ use { ApplyPendingBalanceAccountInfo, EmptyAccountAccountInfo, TransferAccountInfo, WithdrawAccountInfo, }, + ciphertext_extraction::SourceDecryptHandles, + instruction::TransferContextStateAccounts, ConfidentialTransferAccount, DecryptableBalance, }, confidential_transfer_fee::{ @@ -2139,7 +2141,7 @@ where source_account: &Pubkey, destination_account: &Pubkey, source_authority: &Pubkey, - context_state_account: Option<&Pubkey>, + context_state_accounts: Option>, transfer_amount: u64, account_info: Option, source_elgamal_keypair: &ElGamalKeypair, @@ -2147,6 +2149,7 @@ 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); @@ -2160,7 +2163,7 @@ where TransferAccountInfo::new(confidential_transfer_account) }; - let proof_data = if context_state_account.is_some() { + let proof_data = if context_state_accounts.is_some() { None } else { Some( @@ -2176,11 +2179,23 @@ 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_account = context_state_account.unwrap(); - ProofLocation::ContextStateAccount(context_state_account) + 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 new_decryptable_available_balance = account_info @@ -2197,6 +2212,7 @@ where source_authority, &multisig_signers, proof_location, + source_decrypt_handles, )?, signing_keypairs, ) @@ -2221,6 +2237,7 @@ 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); @@ -2274,6 +2291,7 @@ 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 69c2a7d6a17..45db13e50f6 100644 --- a/token/program-2022-test/tests/confidential_transfer.rs +++ b/token/program-2022-test/tests/confidential_transfer.rs @@ -1,5 +1,5 @@ #![cfg(all(feature = "test-sbf"))] -#![cfg(twoxtx)] +// #![cfg(twoxtx)] mod program_test; use { @@ -18,7 +18,10 @@ use { error::TokenError, extension::{ confidential_transfer::{ - self, ConfidentialTransferAccount, MAXIMUM_DEPOSIT_TRANSFER_AMOUNT, + self, + account_info::TransferAccountInfo, + instruction::{TransferContextStateAccounts, TransferSplitContextStateAccounts}, + ConfidentialTransferAccount, MAXIMUM_DEPOSIT_TRANSFER_AMOUNT, }, BaseStateWithExtensions, ExtensionType, }, @@ -110,6 +113,7 @@ impl ConfidentialTokenAccountMeta { } } + #[allow(clippy::too_many_arguments)] #[cfg(feature = "zk-ops")] async fn new_with_tokens( token: &Token, @@ -972,6 +976,7 @@ async fn confidential_transfer_transfer() { alice_meta.elgamal_keypair.pubkey(), Some(auditor_elgamal_keypair.pubkey()), &[&alice], + None, ) .await .unwrap(); @@ -1002,6 +1007,7 @@ async fn confidential_transfer_transfer() { alice_meta.elgamal_keypair.pubkey(), Some(auditor_elgamal_keypair.pubkey()), &[&alice], + None, ) .await .unwrap(); @@ -1055,6 +1061,7 @@ async fn confidential_transfer_transfer() { bob_meta.elgamal_keypair.pubkey(), Some(auditor_elgamal_keypair.pubkey()), &[&alice], + None, ) .await .unwrap(); @@ -1096,6 +1103,7 @@ async fn confidential_transfer_transfer() { bob_meta.elgamal_keypair.pubkey(), Some(auditor_elgamal_keypair.pubkey()), &[&bob], + None, ) .await .unwrap(); @@ -1113,6 +1121,7 @@ async fn confidential_transfer_transfer() { bob_meta.elgamal_keypair.pubkey(), Some(auditor_elgamal_keypair.pubkey()), &[&bob], + None, ) .await .unwrap_err(); @@ -1232,6 +1241,7 @@ async fn confidential_transfer_transfer_with_fee() { TEST_FEE_BASIS_POINTS, TEST_MAXIMUM_FEE, &[&alice], + None, ) .await .unwrap(); @@ -1265,6 +1275,7 @@ async fn confidential_transfer_transfer_with_fee() { TEST_FEE_BASIS_POINTS, TEST_MAXIMUM_FEE, &[&alice], + None, ) .await .unwrap(); @@ -1321,6 +1332,7 @@ async fn confidential_transfer_transfer_with_fee() { TEST_FEE_BASIS_POINTS, TEST_MAXIMUM_FEE, &[&alice], + None, ) .await .unwrap(); @@ -1465,6 +1477,7 @@ async fn confidential_transfer_transfer_memo() { bob_meta.elgamal_keypair.pubkey(), Some(auditor_elgamal_keypair.pubkey()), &[&alice], + None, ) .await .unwrap_err(); @@ -1494,6 +1507,7 @@ async fn confidential_transfer_transfer_memo() { bob_meta.elgamal_keypair.pubkey(), Some(auditor_elgamal_keypair.pubkey()), &[&alice], + None, ) .await .unwrap(); @@ -1600,6 +1614,7 @@ async fn confidential_transfer_transfer_with_fee_and_memo() { TEST_FEE_BASIS_POINTS, TEST_MAXIMUM_FEE, &[&alice], + None, ) .await .unwrap_err(); @@ -1631,6 +1646,7 @@ async fn confidential_transfer_transfer_with_fee_and_memo() { TEST_FEE_BASIS_POINTS, TEST_MAXIMUM_FEE, &[&alice], + None, ) .await .unwrap(); @@ -2268,7 +2284,9 @@ async fn confidential_transfer_transfer_with_proof_context() { &alice_meta.token_account, &bob_meta.token_account, &alice.pubkey(), - Some(&context_state_account.pubkey()), + Some(TransferContextStateAccounts::SingleAccount( + &context_state_account.pubkey(), + )), 42, None, &alice_meta.elgamal_keypair, @@ -2276,6 +2294,7 @@ async fn confidential_transfer_transfer_with_proof_context() { bob_meta.elgamal_keypair.pubkey(), Some(auditor_elgamal_keypair.pubkey()), &[&alice], + None, ) .await .unwrap(); @@ -2339,7 +2358,9 @@ async fn confidential_transfer_transfer_with_proof_context() { &alice_meta.token_account, &bob_meta.token_account, &alice.pubkey(), - Some(&context_state_account.pubkey()), + Some(TransferContextStateAccounts::SingleAccount( + &context_state_account.pubkey(), + )), 0, None, &alice_meta.elgamal_keypair, @@ -2347,6 +2368,7 @@ async fn confidential_transfer_transfer_with_proof_context() { bob_meta.elgamal_keypair.pubkey(), Some(auditor_elgamal_keypair.pubkey()), &[&alice], + None, ) .await .unwrap_err(); @@ -2358,3 +2380,243 @@ async fn confidential_transfer_transfer_with_proof_context() { ))) ) } + +#[tokio::test] +async fn confidential_transfer_transfer_with_split_proof_context() { + 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 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 TokenContext { + token, + alice, + bob, + mint_authority, + decimals, + .. + } = context.token_context.unwrap(); + + let alice_meta = ConfidentialTokenAccountMeta::new_with_tokens( + &token, + &alice, + None, + false, + false, + &mint_authority, + 42, + decimals, + ) + .await; + + let bob_meta = ConfidentialTokenAccountMeta::new_with_tokens( + &token, + &bob, + None, + false, + false, + &mint_authority, + 0, + decimals, + ) + .await; + + let state = token + .get_account_info(&alice_meta.token_account) + .await + .unwrap(); + let extension = state + .get_extension::() + .unwrap(); + let transfer_account_info = TransferAccountInfo::new(extension); + + let ( + equality_proof_data, + ciphertext_validity_proof_data, + range_proof_data, + source_decrypt_handles, + ) = transfer_account_info + .generate_split_transfer_proof_data( + 42, + &alice_meta.elgamal_keypair, + &alice_meta.aes_key, + bob_meta.elgamal_keypair.pubkey(), + Some(auditor_elgamal_keypair.pubkey()), + ) + .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 ciphertext_validity_proof_context_state_account = { + let context_state_account = Keypair::new(); + let instruction_type = ProofInstruction::VerifyBatchedGroupedCiphertext2HandlesValidity; + 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), &ciphertext_validity_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 range_proof_context_state_account = { + let context_state_account = Keypair::new(); + let instruction_type = ProofInstruction::VerifyBatchedRangeProofU128; + 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), &range_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_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 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, + }); + + token + .confidential_transfer_transfer( + &alice_meta.token_account, + &bob_meta.token_account, + &alice.pubkey(), + Some(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), + ) + .await + .unwrap(); + + alice_meta + .check_balances( + &token, + ConfidentialTokenAccountBalances { + pending_balance_lo: 0, + pending_balance_hi: 0, + available_balance: 0, + decryptable_available_balance: 0, + }, + ) + .await; + + bob_meta + .check_balances( + &token, + ConfidentialTokenAccountBalances { + pending_balance_lo: 42, + pending_balance_hi: 0, + available_balance: 0, + decryptable_available_balance: 0, + }, + ) + .await; +} diff --git a/token/program-2022-test/tests/confidential_transfer_fee.rs b/token/program-2022-test/tests/confidential_transfer_fee.rs index b9e5f93b367..c0a1fe87b60 100644 --- a/token/program-2022-test/tests/confidential_transfer_fee.rs +++ b/token/program-2022-test/tests/confidential_transfer_fee.rs @@ -1,5 +1,5 @@ #![cfg(all(feature = "test-sbf"))] -#![cfg(twoxtx)] +// #![cfg(twoxtx)] mod program_test; use { @@ -530,6 +530,7 @@ 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(); @@ -687,6 +688,7 @@ 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(); @@ -817,6 +819,7 @@ 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(); @@ -984,6 +987,7 @@ 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(); @@ -1174,6 +1178,7 @@ 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/error.rs b/token/program-2022/src/error.rs index 27475c8233c..6da2fdb2b3d 100644 --- a/token/program-2022/src/error.rs +++ b/token/program-2022/src/error.rs @@ -218,6 +218,20 @@ pub enum TokenError { /// Harvest of withheld tokens to mint is disabled #[error("Harvest of withheld tokens to mint is disabled")] HarvestToMintDisabled, + /// Split proof context state accounts not supported for instruction + #[error("Split proof context state accounts not supported for instruction")] + SplitProofContextStateAccountsNotSupported, + /// Not enough proof context state accounts provided + #[error("Not enough proof context state accounts provided")] + NotEnoughProofContextStateAccounts, + /// Ciphertext is malformed + #[error("Ciphertext is malformed")] + MalformedCiphertext, + + // 60 + /// Ciphertext arithmetic failed + #[error("Ciphertext arithmetic failed")] + CiphertextArithmeticFailed, } impl From for ProgramError { fn from(e: TokenError) -> Self { @@ -381,6 +395,18 @@ impl PrintProgramError for TokenError { TokenError::HarvestToMintDisabled => { msg!("Harvest of withheld tokens to mint is disabled") } + TokenError::SplitProofContextStateAccountsNotSupported => { + msg!("Split proof context state accounts not supported for instruction") + } + TokenError::NotEnoughProofContextStateAccounts => { + msg!("Not enough proof context state accounts provided") + } + TokenError::MalformedCiphertext => { + msg!("Ciphertext is malformed") + } + TokenError::CiphertextArithmeticFailed => { + msg!("Ciphertext arithmetic failed") + } } } } diff --git a/token/program-2022/src/extension/confidential_transfer/account_info.rs b/token/program-2022/src/extension/confidential_transfer/account_info.rs index 7df8d5cd532..3e5a6ecdba3 100644 --- a/token/program-2022/src/extension/confidential_transfer/account_info.rs +++ b/token/program-2022/src/extension/confidential_transfer/account_info.rs @@ -2,8 +2,9 @@ use { crate::{ error::TokenError, extension::confidential_transfer::{ - ConfidentialTransferAccount, DecryptableBalance, EncryptedBalance, - PENDING_BALANCE_LO_BIT_LENGTH, + ciphertext_extraction::SourceDecryptHandles, + split_proof_generation::transfer_split_proof_data, ConfidentialTransferAccount, + DecryptableBalance, EncryptedBalance, PENDING_BALANCE_LO_BIT_LENGTH, }, pod::*, }, @@ -17,6 +18,8 @@ use { transfer::{FeeParameters, TransferData, TransferWithFeeData}, withdraw::WithdrawData, zero_balance::ZeroBalanceProofData, + BatchedGroupedCiphertext2HandlesValidityProofData, BatchedRangeProofU128Data, + CiphertextCommitmentEqualityProofData, }, }, }; @@ -44,7 +47,7 @@ impl EmptyAccountAccountInfo { let available_balance = self .available_balance .try_into() - .map_err(|_| TokenError::AccountDecryption)?; + .map_err(|_| TokenError::MalformedCiphertext)?; ZeroBalanceProofData::new(elgamal_keypair, &available_balance) .map_err(|_| TokenError::ProofGeneration) @@ -90,7 +93,7 @@ impl ApplyPendingBalanceAccountInfo { let pending_balance_lo = self .pending_balance_lo .try_into() - .map_err(|_| TokenError::AccountDecryption)?; + .map_err(|_| TokenError::MalformedCiphertext)?; elgamal_secret_key .decrypt_u32(&pending_balance_lo) .ok_or(TokenError::AccountDecryption) @@ -103,7 +106,7 @@ impl ApplyPendingBalanceAccountInfo { let pending_balance_hi = self .pending_balance_hi .try_into() - .map_err(|_| TokenError::AccountDecryption)?; + .map_err(|_| TokenError::MalformedCiphertext)?; elgamal_secret_key .decrypt_u32(&pending_balance_hi) .ok_or(TokenError::AccountDecryption) @@ -113,7 +116,7 @@ impl ApplyPendingBalanceAccountInfo { let decryptable_available_balance = self .decryptable_available_balance .try_into() - .map_err(|_| TokenError::AccountDecryption)?; + .map_err(|_| TokenError::MalformedCiphertext)?; aes_key .decrypt(&decryptable_available_balance) .ok_or(TokenError::AccountDecryption) @@ -162,7 +165,7 @@ impl WithdrawAccountInfo { let decryptable_available_balance = self .decryptable_available_balance .try_into() - .map_err(|_| TokenError::AccountDecryption)?; + .map_err(|_| TokenError::MalformedCiphertext)?; aes_key .decrypt(&decryptable_available_balance) .ok_or(TokenError::AccountDecryption) @@ -178,7 +181,7 @@ impl WithdrawAccountInfo { let current_available_balance = self .available_balance .try_into() - .map_err(|_| TokenError::AccountDecryption)?; + .map_err(|_| TokenError::MalformedCiphertext)?; let current_decrypted_available_balance = self.decrypted_available_balance(aes_key)?; WithdrawData::new( @@ -227,7 +230,7 @@ impl TransferAccountInfo { let decryptable_available_balance = self .decryptable_available_balance .try_into() - .map_err(|_| TokenError::AccountDecryption)?; + .map_err(|_| TokenError::MalformedCiphertext)?; aes_key .decrypt(&decryptable_available_balance) .ok_or(TokenError::AccountDecryption) @@ -245,7 +248,7 @@ impl TransferAccountInfo { let current_source_available_balance = self .available_balance .try_into() - .map_err(|_| TokenError::AccountDecryption)?; + .map_err(|_| TokenError::MalformedCiphertext)?; let current_source_decrypted_available_balance = self.decrypted_available_balance(aes_key)?; @@ -264,6 +267,44 @@ impl TransferAccountInfo { .map_err(|_| TokenError::ProofGeneration) } + /// Create a transfer proof data that is split into equality, ciphertext validity, and range + /// proofs. + pub fn generate_split_transfer_proof_data( + &self, + transfer_amount: u64, + source_elgamal_keypair: &ElGamalKeypair, + aes_key: &AeKey, + destination_elgamal_pubkey: &ElGamalPubkey, + auditor_elgamal_pubkey: Option<&ElGamalPubkey>, + ) -> Result< + ( + CiphertextCommitmentEqualityProofData, + BatchedGroupedCiphertext2HandlesValidityProofData, + BatchedRangeProofU128Data, + SourceDecryptHandles, + ), + TokenError, + > { + let current_available_balance = self + .available_balance + .try_into() + .map_err(|_| TokenError::MalformedCiphertext)?; + let current_decryptable_available_balance = self + .decryptable_available_balance + .try_into() + .map_err(|_| TokenError::MalformedCiphertext)?; + + transfer_split_proof_data( + ¤t_available_balance, + ¤t_decryptable_available_balance, + transfer_amount, + source_elgamal_keypair, + aes_key, + destination_elgamal_pubkey, + auditor_elgamal_pubkey, + ) + } + /// Create a transfer with fee proof data #[allow(clippy::too_many_arguments)] pub fn generate_transfer_with_fee_proof_data( @@ -280,7 +321,7 @@ impl TransferAccountInfo { let current_source_available_balance = self .available_balance .try_into() - .map_err(|_| TokenError::AccountDecryption)?; + .map_err(|_| TokenError::MalformedCiphertext)?; let current_source_decrypted_available_balance = self.decrypted_available_balance(aes_key)?; diff --git a/token/program-2022/src/extension/confidential_transfer/ciphertext_extraction.rs b/token/program-2022/src/extension/confidential_transfer/ciphertext_extraction.rs new file mode 100644 index 00000000000..7af513911e2 --- /dev/null +++ b/token/program-2022/src/extension/confidential_transfer/ciphertext_extraction.rs @@ -0,0 +1,315 @@ +//! Ciphertext extraction and proof related helper logic +//! +//! This submodule should be removed with the next upgrade to the Solana program + +use crate::{ + extension::{confidential_transfer::*, confidential_transfer_fee::EncryptedFee}, + solana_program::program_error::ProgramError, + solana_zk_token_sdk::{ + instruction::{ + transfer::TransferProofContext, BatchedGroupedCiphertext2HandlesValidityProofContext, + BatchedRangeProofContext, CiphertextCommitmentEqualityProofContext, + }, + zk_token_elgamal::pod::{ + DecryptHandle, GroupedElGamalCiphertext2Handles, GroupedElGamalCiphertext3Handles, + PedersenCommitment, TransferAmountCiphertext, + }, + }, +}; + +#[cfg(feature = "serde-traits")] +use { + crate::serialization::decrypthandle_fromstr, + serde::{Deserialize, Serialize}, +}; + +pub(crate) fn transfer_amount_commitment( + transfer_amount_ciphertext: &GroupedElGamalCiphertext2Handles, +) -> PedersenCommitment { + let transfer_amount_ciphertext_bytes = bytemuck::bytes_of(transfer_amount_ciphertext); + let transfer_amount_commitment_bytes = + transfer_amount_ciphertext_bytes[..32].try_into().unwrap(); + PedersenCommitment(transfer_amount_commitment_bytes) +} + +/// Extract the transfer amount ciphertext encrypted under the source ElGamal public key. +/// +/// A transfer amount ciphertext consists of the following 32-byte components that are serialized +/// in order: +/// 1. The `commitment` component that encodes the transfer amount. +/// 2. The `decryption handle` component with respect to the source public key. +/// 3. The `decryption handle` component with respect to the destination public key. +/// 4. The `decryption handle` component with respect to the auditor public key. +/// +/// An ElGamal ciphertext for the source consists of the `commitment` component and the `decryption +/// handle` component with respect to the source. +pub(crate) fn transfer_amount_source_ciphertext( + transfer_amount_ciphertext: &TransferAmountCiphertext, +) -> ElGamalCiphertext { + let transfer_amount_ciphertext_bytes = bytemuck::bytes_of(transfer_amount_ciphertext); + + let mut source_ciphertext_bytes = [0u8; 64]; + source_ciphertext_bytes[..32].copy_from_slice(&transfer_amount_ciphertext_bytes[..32]); + source_ciphertext_bytes[32..].copy_from_slice(&transfer_amount_ciphertext_bytes[32..64]); + + ElGamalCiphertext(source_ciphertext_bytes) +} + +/// Extract the transfer amount ciphertext encrypted under the destination ElGamal public key. +/// +/// A transfer amount ciphertext consists of the following 32-byte components that are serialized +/// in order: +/// 1. The `commitment` component that encodes the transfer amount. +/// 2. The `decryption handle` component with respect to the source public key. +/// 3. The `decryption handle` component with respect to the destination public key. +/// 4. The `decryption handle` component with respect to the auditor public key. +/// +/// An ElGamal ciphertext for the destination consists of the `commitment` component and the +/// `decryption handle` component with respect to the destination public key. +#[cfg(feature = "zk-ops")] +pub(crate) fn transfer_amount_destination_ciphertext( + transfer_amount_ciphertext: &TransferAmountCiphertext, +) -> ElGamalCiphertext { + let transfer_amount_ciphertext_bytes = bytemuck::bytes_of(transfer_amount_ciphertext); + + let mut destination_ciphertext_bytes = [0u8; 64]; + destination_ciphertext_bytes[..32].copy_from_slice(&transfer_amount_ciphertext_bytes[..32]); + destination_ciphertext_bytes[32..].copy_from_slice(&transfer_amount_ciphertext_bytes[64..96]); + + ElGamalCiphertext(destination_ciphertext_bytes) +} + +/// Extract the fee amount ciphertext encrypted under the destination ElGamal public key. +/// +/// A fee encryption amount consists of the following 32-byte components that are serialized in +/// order: +/// 1. The `commitment` component that encodes the fee amount. +/// 2. The `decryption handle` component with respect to the destination public key. +/// 3. The `decryption handle` component with respect to the withdraw withheld authority public +/// key. +/// +/// An ElGamal ciphertext for the destination consists of the `commitment` component and the +/// `decryption handle` component with respect to the destination public key. +#[cfg(feature = "zk-ops")] +pub(crate) fn fee_amount_destination_ciphertext( + transfer_amount_ciphertext: &EncryptedFee, +) -> ElGamalCiphertext { + let transfer_amount_ciphertext_bytes = bytemuck::bytes_of(transfer_amount_ciphertext); + + let mut source_ciphertext_bytes = [0u8; 64]; + source_ciphertext_bytes[..32].copy_from_slice(&transfer_amount_ciphertext_bytes[..32]); + source_ciphertext_bytes[32..].copy_from_slice(&transfer_amount_ciphertext_bytes[32..64]); + + ElGamalCiphertext(source_ciphertext_bytes) +} + +/// Extract the transfer amount ciphertext encrypted under the withdraw withheld authority ElGamal +/// public key. +/// +/// A fee encryption amount consists of the following 32-byte components that are serialized in +/// order: +/// 1. The `commitment` component that encodes the fee amount. +/// 2. The `decryption handle` component with respect to the destination public key. +/// 3. The `decryption handle` component with respect to the withdraw withheld authority public +/// key. +/// +/// An ElGamal ciphertext for the destination consists of the `commitment` component and the +/// `decryption handle` component with respect to the withdraw withheld authority public key. +#[cfg(feature = "zk-ops")] +pub(crate) fn fee_amount_withdraw_withheld_authority_ciphertext( + transfer_amount_ciphertext: &EncryptedFee, +) -> ElGamalCiphertext { + let transfer_amount_ciphertext_bytes = bytemuck::bytes_of(transfer_amount_ciphertext); + + let mut destination_ciphertext_bytes = [0u8; 64]; + destination_ciphertext_bytes[..32].copy_from_slice(&transfer_amount_ciphertext_bytes[..32]); + destination_ciphertext_bytes[32..].copy_from_slice(&transfer_amount_ciphertext_bytes[64..96]); + + ElGamalCiphertext(destination_ciphertext_bytes) +} + +#[cfg(feature = "zk-ops")] +pub(crate) fn transfer_amount_encryption_from_decrypt_handle( + source_decrypt_handle: &DecryptHandle, + grouped_ciphertext: &GroupedElGamalCiphertext2Handles, +) -> TransferAmountCiphertext { + let source_decrypt_handle_bytes = bytemuck::bytes_of(source_decrypt_handle); + let grouped_ciphertext_bytes = bytemuck::bytes_of(grouped_ciphertext); + + let mut transfer_amount_ciphertext_bytes = [0u8; 128]; + transfer_amount_ciphertext_bytes[..32].copy_from_slice(&grouped_ciphertext_bytes[..32]); + transfer_amount_ciphertext_bytes[32..64].copy_from_slice(source_decrypt_handle_bytes); + transfer_amount_ciphertext_bytes[64..128].copy_from_slice(&grouped_ciphertext_bytes[32..96]); + + TransferAmountCiphertext(GroupedElGamalCiphertext3Handles( + transfer_amount_ciphertext_bytes, + )) +} + +/// The transfer public keys associated with a transfer. +#[cfg(feature = "zk-ops")] +pub struct TransferPubkeysInfo { + /// Source ElGamal public key + pub source: ElGamalPubkey, + /// Destination ElGamal public key + pub destination: ElGamalPubkey, + /// Auditor ElGamal public key + pub auditor: ElGamalPubkey, +} + +/// The proof context information needed to process a [Transfer] instruction. +#[cfg(feature = "zk-ops")] +pub struct TransferProofContextInfo { + /// Ciphertext containing the low 16 bits of the transafer amount + pub ciphertext_lo: TransferAmountCiphertext, + /// Ciphertext containing the high 32 bits of the transafer amount + pub ciphertext_hi: TransferAmountCiphertext, + /// The transfer public keys associated with a transfer + pub transfer_pubkeys: TransferPubkeysInfo, + /// The new source available balance ciphertext + pub new_source_ciphertext: ElGamalCiphertext, +} + +impl From for TransferProofContextInfo { + fn from(context: TransferProofContext) -> Self { + let transfer_pubkeys = TransferPubkeysInfo { + source: context.transfer_pubkeys.source, + destination: context.transfer_pubkeys.destination, + auditor: context.transfer_pubkeys.auditor, + }; + + TransferProofContextInfo { + ciphertext_lo: context.ciphertext_lo, + ciphertext_hi: context.ciphertext_hi, + transfer_pubkeys, + new_source_ciphertext: context.new_source_ciphertext, + } + } +} + +impl TransferProofContextInfo { + /// Create a transfer proof context information needed to process a [Transfer] instruction from + /// split proof contexts after verifying their consistency. + pub fn new( + equality_proof_context: &CiphertextCommitmentEqualityProofContext, + ciphertext_validity_proof_context: &BatchedGroupedCiphertext2HandlesValidityProofContext, + range_proof_context: &BatchedRangeProofContext, + source_decrypt_handles: &SourceDecryptHandles, + ) -> Result { + // The equality proof context consists of the source ElGamal public key, the new source + // available balance ciphertext, and the new source available commitment. The public key + // and ciphertext should be returned as parts of `TransferProofContextInfo` and the + // commitment should be checked with range proof for consistency. + let CiphertextCommitmentEqualityProofContext { + pubkey: source_pubkey, + ciphertext: new_source_ciphertext, + commitment: new_source_commitment, + } = equality_proof_context; + + // The ciphertext validity proof context consists of the destination ElGamal public key, + // auditor ElGamal public key, and the transfer amount ciphertexts. All of these fields + // should be returned as part of `TransferProofContextInfo`. In addition, the commitments + // pertaining to the transfer amount ciphertexts should be checked with range proof for + // consistency. + let BatchedGroupedCiphertext2HandlesValidityProofContext { + destination_pubkey, + auditor_pubkey, + grouped_ciphertext_lo: transfer_amount_ciphertext_lo, + grouped_ciphertext_hi: transfer_amount_ciphertext_hi, + } = ciphertext_validity_proof_context; + + // The range proof context consists of the Pedersen commitments and bit-lengths for which + // the range proof is proved. The commitments must consist of three commitments pertaining + // to the low bits of the transfer amount, high bits of the transfer amount, and the new + // source available balance. These commitments must be checked for `16`, `32`, `80`. + let BatchedRangeProofContext { + commitments: range_proof_commitments, + bit_lengths: range_proof_bit_lengths, + } = range_proof_context; + + // check that the range proof was created for the correct set of Pedersen commitments + let transfer_amount_commitment_lo = + transfer_amount_commitment(transfer_amount_ciphertext_lo); + let transfer_amount_commitment_hi = + transfer_amount_commitment(transfer_amount_ciphertext_hi); + + let expected_commitments = [ + *new_source_commitment, + transfer_amount_commitment_lo, + transfer_amount_commitment_hi, + // the fourth dummy commitment can be any commitment + ]; + + if !range_proof_commitments + .iter() + .zip(expected_commitments.iter()) + .all(|(proof_commitment, expected_commitment)| proof_commitment == expected_commitment) + { + return Err(ProgramError::InvalidInstructionData); + } + + // check that the range proof was created for the correct number of bits + const REMAINING_BALANCE_BIT_LENGTH: u8 = 64; + const TRANSFER_AMOUNT_LO_BIT_LENGTH: u8 = 16; + const TRANSFER_AMOUNT_HI_BIT_LENGTH: u8 = 32; + const PADDING_BIT_LENGTH: u8 = 16; + let expected_bit_lengths = [ + REMAINING_BALANCE_BIT_LENGTH, + TRANSFER_AMOUNT_LO_BIT_LENGTH, + TRANSFER_AMOUNT_HI_BIT_LENGTH, + PADDING_BIT_LENGTH, + ] + .iter(); + + if !range_proof_bit_lengths + .iter() + .zip(expected_bit_lengths) + .all(|(proof_len, expected_len)| proof_len == expected_len) + { + return Err(ProgramError::InvalidInstructionData); + } + + let transfer_pubkeys = TransferPubkeysInfo { + source: *source_pubkey, + destination: *destination_pubkey, + auditor: *auditor_pubkey, + }; + + let transfer_amount_ciphertext_lo = transfer_amount_encryption_from_decrypt_handle( + &source_decrypt_handles.lo, + transfer_amount_ciphertext_lo, + ); + + let transfer_amount_ciphertext_hi = transfer_amount_encryption_from_decrypt_handle( + &source_decrypt_handles.hi, + transfer_amount_ciphertext_hi, + ); + + Ok(Self { + ciphertext_lo: transfer_amount_ciphertext_lo, + ciphertext_hi: transfer_amount_ciphertext_hi, + transfer_pubkeys, + new_source_ciphertext: *new_source_ciphertext, + }) + } +} + +/// The ElGamal ciphertext decryption handle pertaining to the low and high bits of the transfer +/// amount under the source public key of the transfer. +/// +/// The `TransferProofContext` contains decryption handles for the low and high bits of the +/// transfer amount. Howver, these decryption handles were (mistakenly) removed from the split +/// proof contexts as a form of optimization. These components should be added back into these +/// split proofs in `zk-token-sdk`. Until this modifications is made, include `SourceDecryptHandle` +/// in the transfer instruction data. +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] +pub struct SourceDecryptHandles { + /// The ElGamal decryption handle pertaining to the low 16 bits of the transfer amount. + #[cfg_attr(feature = "serde-traits", serde(with = "decrypthandle_fromstr"))] + pub lo: DecryptHandle, + /// The ElGamal decryption handle pertaining to the low 32 bits of the transfer amount. + #[cfg_attr(feature = "serde-traits", serde(with = "decrypthandle_fromstr"))] + pub hi: DecryptHandle, +} diff --git a/token/program-2022/src/extension/confidential_transfer/instruction.rs b/token/program-2022/src/extension/confidential_transfer/instruction.rs index 989c1c00e47..8c7d4a5e0fb 100644 --- a/token/program-2022/src/extension/confidential_transfer/instruction.rs +++ b/token/program-2022/src/extension/confidential_transfer/instruction.rs @@ -6,7 +6,7 @@ pub use solana_zk_token_sdk::{ use { crate::{ check_program_account, - extension::confidential_transfer::*, + extension::confidential_transfer::{ciphertext_extraction::SourceDecryptHandles, *}, instruction::{encode_instruction, TokenInstruction}, proof::ProofLocation, }, @@ -228,18 +228,26 @@ pub enum ConfidentialTransferInstruction { /// 1. `[writable]` The source SPL Token account. /// 2. `[writable]` The destination SPL Token account. /// 3. `[]` The token mint. - /// 4. `[]` Instructions sysvar if `VerifyTransfer` or `VerifyTransferWithFee` is included in - /// the same transaction or context state account if the proof is pre-verified into a - /// context state account. + /// 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). /// 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. `[]` Instructions sysvar if `VerifyTransfer` or `VerifyTransferWithFee` is included in - /// the same transaction or context state account if the proof is pre-verified into a - /// context state account. + /// 4. `[]` One of instructions sysvar, context state account for `VerifyTransfer` or + /// `VerifyTransferWithFee`, or the set of context state accounts listed above. /// 5. `[]` The multisig source account owner. /// 6.. `[signer]` Required M signer accounts for the SPL Token Multisig account. /// @@ -454,6 +462,14 @@ 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` @@ -469,6 +485,26 @@ 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>), +} + +/// Type for split transfer instruction proof context state account addresses intended to be used +/// as parameters to functions. +pub struct TransferSplitContextStateAccounts<'a> { + /// The context state account address for an equality proof needed for a transfer. + pub equality_proof: &'a Pubkey, + /// The context state account address for a ciphertext validity proof needed for a transfer. + pub ciphertext_validity_proof: &'a Pubkey, + /// The context state account address for a range proof needed for a transfer. + pub range_proof: &'a Pubkey, +} + /// Create a `InitializeMint` instruction #[cfg(not(target_os = "solana"))] pub fn initialize_mint( @@ -555,6 +591,9 @@ 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( @@ -668,6 +707,9 @@ 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( @@ -786,6 +828,9 @@ 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( @@ -866,6 +911,7 @@ 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![ @@ -874,14 +920,28 @@ pub fn inner_transfer( AccountMeta::new_readonly(*mint, false), ]; - let proof_instruction_offset = match proof_data_location { + let (proof_instruction_offset, split_proof_context_state_accounts) = match proof_data_location { ProofLocation::InstructionOffset(proof_instruction_offset, _) => { accounts.push(AccountMeta::new_readonly(sysvar::instructions::id(), false)); - proof_instruction_offset.into() + (proof_instruction_offset.into(), false) } ProofLocation::ContextStateAccount(context_state_account) => { accounts.push(AccountMeta::new_readonly(*context_state_account, false)); - 0 + (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) } }; @@ -894,6 +954,12 @@ 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, @@ -902,6 +968,8 @@ 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, }, )) } @@ -918,6 +986,7 @@ 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, @@ -928,6 +997,7 @@ pub fn transfer( authority, multisig_signers, proof_data_location, + source_decrypt_handles, )?]; if let ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) = @@ -960,6 +1030,7 @@ 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![ @@ -968,14 +1039,29 @@ pub fn inner_transfer_with_fee( AccountMeta::new_readonly(*mint, false), ]; - let proof_instruction_offset = match proof_data_location { + let (proof_instruction_offset, split_proof_context_state_accounts) = match proof_data_location { ProofLocation::InstructionOffset(proof_instruction_offset, _) => { accounts.push(AccountMeta::new_readonly(sysvar::instructions::id(), false)); - proof_instruction_offset.into() + (proof_instruction_offset.into(), false) } ProofLocation::ContextStateAccount(context_state_account) => { accounts.push(AccountMeta::new_readonly(*context_state_account, false)); - 0 + (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) } }; @@ -988,6 +1074,12 @@ 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, @@ -996,6 +1088,8 @@ 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, }, )) } @@ -1012,6 +1106,7 @@ 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, @@ -1022,6 +1117,7 @@ pub fn transfer_with_fee( authority, multisig_signers, proof_data_location, + source_decrypt_handles, )?]; if let ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) = diff --git a/token/program-2022/src/extension/confidential_transfer/mod.rs b/token/program-2022/src/extension/confidential_transfer/mod.rs index 28ebe37b137..d69942241cf 100644 --- a/token/program-2022/src/extension/confidential_transfer/mod.rs +++ b/token/program-2022/src/extension/confidential_transfer/mod.rs @@ -23,10 +23,26 @@ pub mod instruction; /// Confidential Transfer Extension processor pub mod processor; +/// Helper functions to verify zero-knowledge proofs in the Confidential Transfer Extension +pub mod verify_proof; + +/// Helper functions to generate split zero-knowledge proofs for confidential transfers in the +/// Confidential Transfer Extension. +/// +/// The logic in this submodule should belong to the `solana-zk-token-sdk` and will be removed with +/// the next upgrade to the Solana program. +#[cfg(not(target_os = "solana"))] +pub mod split_proof_generation; + /// Confidential Transfer Extension account information needed for instructions #[cfg(not(target_os = "solana"))] pub mod account_info; +/// Ciphertext extraction and proof related helper logic +/// +/// This submodule should be removed with the next upgrade to the Solana program +pub mod ciphertext_extraction; + /// ElGamal ciphertext containing an account balance pub type EncryptedBalance = ElGamalCiphertext; /// Authenticated encryption containing an account balance diff --git a/token/program-2022/src/extension/confidential_transfer/processor.rs b/token/program-2022/src/extension/confidential_transfer/processor.rs index 08ea8a23793..0c0aee9ab5f 100644 --- a/token/program-2022/src/extension/confidential_transfer/processor.rs +++ b/token/program-2022/src/extension/confidential_transfer/processor.rs @@ -1,9 +1,9 @@ use { crate::{ - check_program_account, check_zk_token_proof_program_account, + check_program_account, error::TokenError, extension::{ - confidential_transfer::{instruction::*, *}, + confidential_transfer::{ciphertext_extraction::*, instruction::*, verify_proof::*, *}, confidential_transfer_fee::{ ConfidentialTransferFeeAmount, ConfidentialTransferFeeConfig, EncryptedFee, EncryptedWithheldAmount, @@ -14,8 +14,6 @@ use { }, instruction::{decode_instruction_data, decode_instruction_type}, processor::Processor, - proof::decode_proof_instruction_context, - solana_zk_token_sdk::zk_token_elgamal::pod::TransferAmountCiphertext, state::{Account, Mint}, }, solana_program::{ @@ -25,7 +23,6 @@ use { msg, program_error::ProgramError, pubkey::Pubkey, - sysvar::instructions::get_instruction_relative, sysvar::Sysvar, }, }; @@ -103,10 +100,8 @@ fn process_configure_account( let mint_info = next_account_info(account_info_iter)?; // zero-knowledge proof certifies that the supplied ElGamal public key is valid - let proof_context = verify_configure_account_proof( - next_account_info(account_info_iter)?, - proof_instruction_offset, - )?; + let proof_context = + verify_configure_account_proof(account_info_iter, proof_instruction_offset)?; let authority_info = next_account_info(account_info_iter)?; let authority_info_data_len = authority_info.data_len(); @@ -163,37 +158,6 @@ fn process_configure_account( Ok(()) } -/// Verify zero-knowledge proof needed for a [ConfigureAccount] instruction and return the -/// corresponding proof context. -fn verify_configure_account_proof( - account_info: &AccountInfo<'_>, - proof_instruction_offset: i64, -) -> Result { - if proof_instruction_offset == 0 { - // interpret `account_info` as a context state account - check_zk_token_proof_program_account(account_info.owner)?; - let context_state_account_data = account_info.data.borrow(); - let context_state = pod_from_bytes::>( - &context_state_account_data, - )?; - - if context_state.proof_type != ProofType::PubkeyValidity.into() { - return Err(ProgramError::InvalidInstructionData); - } - - Ok(context_state.proof_context) - } else { - // interpret `account_info` as a sysvar - let zkp_instruction = get_instruction_relative(proof_instruction_offset, account_info)?; - Ok(*decode_proof_instruction_context::< - PubkeyValidityData, - PubkeyValidityProofContext, - >( - ProofInstruction::VerifyPubkeyValidity, &zkp_instruction - )?) - } -} - /// Processes an [ApproveAccount] instruction. fn process_approve_account(accounts: &[AccountInfo]) -> ProgramResult { let account_info_iter = &mut accounts.iter(); @@ -234,10 +198,7 @@ fn process_empty_account( let token_account_info = next_account_info(account_info_iter)?; // zero-knowledge proof certifies that the available balance ciphertext holds the balance of 0. - let proof_context = verify_empty_account_proof( - next_account_info(account_info_iter)?, - proof_instruction_offset, - )?; + let proof_context = verify_empty_account_proof(account_info_iter, proof_instruction_offset)?; let authority_info = next_account_info(account_info_iter)?; let authority_info_data_len = authority_info.data_len(); @@ -275,37 +236,6 @@ fn process_empty_account( Ok(()) } -/// Verify zero-knowledge proof needed for a [EmptyAccount] instruction and return the -/// corresponding proof context. -fn verify_empty_account_proof( - account_info: &AccountInfo<'_>, - proof_instruction_offset: i64, -) -> Result { - if proof_instruction_offset == 0 { - // interpret `account_info` as a context state account - check_zk_token_proof_program_account(account_info.owner)?; - let context_state_account_data = account_info.data.borrow(); - let context_state = pod_from_bytes::>( - &context_state_account_data, - )?; - - if context_state.proof_type != ProofType::ZeroBalance.into() { - return Err(ProgramError::InvalidInstructionData); - } - - Ok(context_state.proof_context) - } else { - // interpret `account_info` as a sysvar - let zkp_instruction = get_instruction_relative(proof_instruction_offset, account_info)?; - Ok(*decode_proof_instruction_context::< - ZeroBalanceProofData, - ZeroBalanceProofContext, - >( - ProofInstruction::VerifyZeroBalance, &zkp_instruction - )?) - } -} - /// Processes a [Deposit] instruction. #[cfg(feature = "zk-ops")] fn process_deposit( @@ -373,12 +303,12 @@ fn process_deposit( if amount_lo > 0 { confidential_transfer_account.pending_balance_lo = syscall::add_to(&confidential_transfer_account.pending_balance_lo, amount_lo) - .ok_or(ProgramError::InvalidInstructionData)?; + .ok_or(TokenError::CiphertextArithmeticFailed)?; } if amount_hi > 0 { confidential_transfer_account.pending_balance_hi = syscall::add_to(&confidential_transfer_account.pending_balance_hi, amount_hi) - .ok_or(ProgramError::InvalidInstructionData)?; + .ok_or(TokenError::CiphertextArithmeticFailed)?; } confidential_transfer_account.increment_pending_balance_credit_counter()?; @@ -389,7 +319,7 @@ fn process_deposit( /// Verifies that a deposit amount is a 48-bit number and returns the least significant 16 bits and /// most significant 32 bits of the amount. #[cfg(feature = "zk-ops")] -fn verify_and_split_deposit_amount(amount: u64) -> Result<(u64, u64), TokenError> { +pub(crate) fn verify_and_split_deposit_amount(amount: u64) -> Result<(u64, u64), TokenError> { if amount > MAXIMUM_DEPOSIT_TRANSFER_AMOUNT { return Err(TokenError::MaximumDepositAmountExceeded); } @@ -414,10 +344,7 @@ fn process_withdraw( // zero-knowledge proof certifies that the account has enough available balance to withdraw the // amount. - let proof_context = verify_withdraw_proof( - next_account_info(account_info_iter)?, - proof_instruction_offset, - )?; + let proof_context = verify_withdraw_proof(account_info_iter, proof_instruction_offset)?; let authority_info = next_account_info(account_info_iter)?; let authority_info_data_len = authority_info.data_len(); @@ -471,7 +398,7 @@ fn process_withdraw( if amount > 0 { confidential_transfer_account.available_balance = syscall::subtract_from(&confidential_transfer_account.available_balance, amount) - .ok_or(ProgramError::InvalidInstructionData)?; + .ok_or(TokenError::CiphertextArithmeticFailed)?; } // Check that the final available balance ciphertext is consistent with the actual ciphertext // for which the zero-knowledge proof was generated for. @@ -490,36 +417,6 @@ fn process_withdraw( Ok(()) } -/// Verify zero-knowledge proof needed for a [Withdraw] instruction and return the -/// corresponding proof context. -fn verify_withdraw_proof( - account_info: &AccountInfo<'_>, - proof_instruction_offset: i64, -) -> Result { - if proof_instruction_offset == 0 { - // interpret `account_info` as a context state account - check_zk_token_proof_program_account(account_info.owner)?; - let context_state_account_data = account_info.data.borrow(); - let context_state = - pod_from_bytes::>(&context_state_account_data)?; - - if context_state.proof_type != ProofType::Withdraw.into() { - return Err(ProgramError::InvalidInstructionData); - } - - Ok(context_state.proof_context) - } else { - // interpret `account_info` as a sysvar - let zkp_instruction = get_instruction_relative(proof_instruction_offset, account_info)?; - Ok(*decode_proof_instruction_context::< - WithdrawData, - WithdrawProofContext, - >( - ProofInstruction::VerifyWithdraw, &zkp_instruction - )?) - } -} - /// Processes an [Transfer] instruction. #[cfg(feature = "zk-ops")] fn process_transfer( @@ -527,17 +424,14 @@ fn process_transfer( accounts: &[AccountInfo], new_source_decryptable_available_balance: DecryptableBalance, proof_instruction_offset: i64, + split_proof_context_state_accounts: bool, + source_decrypt_handles: &SourceDecryptHandles, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let source_account_info = next_account_info(account_info_iter)?; let destination_token_account_info = next_account_info(account_info_iter)?; let mint_info = next_account_info(account_info_iter)?; - // either sysvar or context state account depending on `proof_instruction_offset` - let proof_account_info = next_account_info(account_info_iter)?; - - let authority_info = next_account_info(account_info_iter)?; - check_program_account(mint_info.owner)?; let mint_data = &mint_info.data.borrow_mut(); let mint = StateWithExtensions::::unpack(mint_data)?; @@ -560,7 +454,14 @@ 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(proof_account_info, proof_instruction_offset)?; + let proof_context = verify_transfer_proof( + account_info_iter, + proof_instruction_offset, + split_proof_context_state_accounts, + source_decrypt_handles, + )?; + + 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. @@ -609,8 +510,13 @@ fn process_transfer( // 1. the transfer amount is encrypted in the correct form // 2. the source account has enough balance to send the transfer amount // 3. the transfer fee is computed correctly and encrypted in the correct form - let proof_context = - verify_transfer_with_fee_proof(proof_account_info, proof_instruction_offset)?; + let proof_context = verify_transfer_with_fee_proof( + account_info_iter, + proof_instruction_offset, + split_proof_context_state_accounts, + )?; + + let authority_info = next_account_info(account_info_iter)?; // Check that the encryption public keys associated with the mint confidential transfer and // confidential transfer fee extensions are consistent with the keys that were used to @@ -692,165 +598,6 @@ fn process_transfer( Ok(()) } -/// Verify zero-knowledge proof needed for a [Transfer] instruction without fee and return the -/// corresponding proof context. -fn verify_transfer_proof( - account_info: &AccountInfo<'_>, - proof_instruction_offset: i64, -) -> Result { - if proof_instruction_offset == 0 { - // interpret `account_info` as a context state account - check_zk_token_proof_program_account(account_info.owner)?; - let context_state_account_data = account_info.data.borrow(); - let context_state = - pod_from_bytes::>(&context_state_account_data)?; - - if context_state.proof_type != ProofType::Transfer.into() { - return Err(ProgramError::InvalidInstructionData); - } - - Ok(context_state.proof_context) - } else { - // interpret `account_info` as a sysvar - let zkp_instruction = get_instruction_relative(proof_instruction_offset, account_info)?; - Ok(*decode_proof_instruction_context::< - TransferData, - TransferProofContext, - >( - ProofInstruction::VerifyTransfer, &zkp_instruction - )?) - } -} - -/// Verify zero-knowledge proof needed for a [Transfer] instruction with fee and return the -/// corresponding proof context. -fn verify_transfer_with_fee_proof( - account_info: &AccountInfo<'_>, - proof_instruction_offset: i64, -) -> Result { - if proof_instruction_offset == 0 { - // interpret `account_info` as a context state account - check_zk_token_proof_program_account(account_info.owner)?; - let context_state_account_data = account_info.data.borrow(); - let context_state = pod_from_bytes::>( - &context_state_account_data, - )?; - - if context_state.proof_type != ProofType::TransferWithFee.into() { - return Err(ProgramError::InvalidInstructionData); - } - - Ok(context_state.proof_context) - } else { - // interpret `account_info` as a sysvar - let zkp_instruction = get_instruction_relative(proof_instruction_offset, account_info)?; - Ok(*decode_proof_instruction_context::< - TransferWithFeeData, - TransferWithFeeProofContext, - >( - ProofInstruction::VerifyTransferWithFee, - &zkp_instruction, - )?) - } -} - -/// Extract the transfer amount ciphertext encrypted under the source ElGamal public key. -/// -/// A transfer amount ciphertext consists of the following 32-byte components that are serialized -/// in order: -/// 1. The `commitment` component that encodes the transfer amount. -/// 2. The `decryption handle` component with respect to the source public key. -/// 3. The `decryption handle` component with respect to the destination public key. -/// 4. The `decryption handle` component with respect to the auditor public key. -/// -/// An ElGamal ciphertext for the source consists of the `commitment` component and the `decryption -/// handle` component with respect to the source. -#[cfg(feature = "zk-ops")] -fn transfer_amount_source_ciphertext( - transfer_amount_ciphertext: &TransferAmountCiphertext, -) -> ElGamalCiphertext { - let transfer_amount_ciphertext_bytes = bytemuck::bytes_of(transfer_amount_ciphertext); - - let mut source_ciphertext_bytes = [0u8; 64]; - source_ciphertext_bytes[..32].copy_from_slice(&transfer_amount_ciphertext_bytes[..32]); - source_ciphertext_bytes[32..].copy_from_slice(&transfer_amount_ciphertext_bytes[32..64]); - - ElGamalCiphertext(source_ciphertext_bytes) -} - -/// Extract the transfer amount ciphertext encrypted under the destination ElGamal public key. -/// -/// A transfer amount ciphertext consists of the following 32-byte components that are serialized -/// in order: -/// 1. The `commitment` component that encodes the transfer amount. -/// 2. The `decryption handle` component with respect to the source public key. -/// 3. The `decryption handle` component with respect to the destination public key. -/// 4. The `decryption handle` component with respect to the auditor public key. -/// -/// An ElGamal ciphertext for the destination consists of the `commitment` component and the -/// `decryption handle` component with respect to the destination public key. -#[cfg(feature = "zk-ops")] -fn transfer_amount_destination_ciphertext( - transfer_amount_ciphertext: &TransferAmountCiphertext, -) -> ElGamalCiphertext { - let transfer_amount_ciphertext_bytes = bytemuck::bytes_of(transfer_amount_ciphertext); - - let mut destination_ciphertext_bytes = [0u8; 64]; - destination_ciphertext_bytes[..32].copy_from_slice(&transfer_amount_ciphertext_bytes[..32]); - destination_ciphertext_bytes[32..].copy_from_slice(&transfer_amount_ciphertext_bytes[64..96]); - - ElGamalCiphertext(destination_ciphertext_bytes) -} - -/// Extract the fee amount ciphertext encrypted under the destination ElGamal public key. -/// -/// A fee encryption amount consists of the following 32-byte components that are serialized in -/// order: -/// 1. The `commitment` component that encodes the fee amount. -/// 2. The `decryption handle` component with respect to the destination public key. -/// 3. The `decryption handle` component with respect to the withdraw withheld authority public -/// key. -/// -/// An ElGamal ciphertext for the destination consists of the `commitment` component and the -/// `decryption handle` component with respect to the destination public key. -#[cfg(feature = "zk-ops")] -fn fee_amount_destination_ciphertext( - transfer_amount_ciphertext: &EncryptedFee, -) -> ElGamalCiphertext { - let transfer_amount_ciphertext_bytes = bytemuck::bytes_of(transfer_amount_ciphertext); - - let mut source_ciphertext_bytes = [0u8; 64]; - source_ciphertext_bytes[..32].copy_from_slice(&transfer_amount_ciphertext_bytes[..32]); - source_ciphertext_bytes[32..].copy_from_slice(&transfer_amount_ciphertext_bytes[32..64]); - - ElGamalCiphertext(source_ciphertext_bytes) -} - -/// Extract the transfer amount ciphertext encrypted under the withdraw withheld authority ElGamal -/// public key. -/// -/// A fee encryption amount consists of the following 32-byte components that are serialized in -/// order: -/// 1. The `commitment` component that encodes the fee amount. -/// 2. The `decryption handle` component with respect to the destination public key. -/// 3. The `decryption handle` component with respect to the withdraw withheld authority public -/// key. -/// -/// An ElGamal ciphertext for the destination consists of the `commitment` component and the -/// `decryption handle` component with respect to the withdraw withheld authority public key. -#[cfg(feature = "zk-ops")] -fn fee_amount_withdraw_withheld_authority_ciphertext( - transfer_amount_ciphertext: &EncryptedFee, -) -> ElGamalCiphertext { - let transfer_amount_ciphertext_bytes = bytemuck::bytes_of(transfer_amount_ciphertext); - - let mut destination_ciphertext_bytes = [0u8; 64]; - destination_ciphertext_bytes[..32].copy_from_slice(&transfer_amount_ciphertext_bytes[..32]); - destination_ciphertext_bytes[32..].copy_from_slice(&transfer_amount_ciphertext_bytes[64..96]); - - ElGamalCiphertext(destination_ciphertext_bytes) -} - #[allow(clippy::too_many_arguments)] #[cfg(feature = "zk-ops")] fn process_source_for_transfer( @@ -901,7 +648,7 @@ fn process_source_for_transfer( source_transfer_amount_lo, source_transfer_amount_hi, ) - .ok_or(ProgramError::InvalidInstructionData)?; + .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. @@ -954,13 +701,13 @@ fn process_destination_for_transfer( &destination_confidential_transfer_account.pending_balance_lo, destination_transfer_amount_lo, ) - .ok_or(ProgramError::InvalidInstructionData)?; + .ok_or(TokenError::CiphertextArithmeticFailed)?; destination_confidential_transfer_account.pending_balance_hi = syscall::add( &destination_confidential_transfer_account.pending_balance_hi, destination_transfer_amount_hi, ) - .ok_or(ProgramError::InvalidInstructionData)?; + .ok_or(TokenError::CiphertextArithmeticFailed)?; destination_confidential_transfer_account.increment_pending_balance_credit_counter()?; @@ -975,12 +722,12 @@ fn process_destination_for_transfer( &destination_confidential_transfer_account.pending_balance_lo, &destination_fee_lo, ) - .ok_or(ProgramError::InvalidInstructionData)?; + .ok_or(TokenError::CiphertextArithmeticFailed)?; destination_confidential_transfer_account.pending_balance_hi = syscall::subtract( &destination_confidential_transfer_account.pending_balance_hi, &destination_fee_hi, ) - .ok_or(ProgramError::InvalidInstructionData)?; + .ok_or(TokenError::CiphertextArithmeticFailed)?; // Decode lo and hi fee amounts encrypted under the withdraw authority encryption public // key @@ -998,7 +745,7 @@ fn process_destination_for_transfer( &withdraw_withheld_authority_fee_lo, &withdraw_withheld_authority_fee_hi, ) - .ok_or(ProgramError::InvalidInstructionData)?; + .ok_or(TokenError::CiphertextArithmeticFailed)?; } Ok(()) @@ -1039,7 +786,7 @@ fn process_apply_pending_balance( &confidential_transfer_account.pending_balance_lo, &confidential_transfer_account.pending_balance_hi, ) - .ok_or(ProgramError::InvalidInstructionData)?; + .ok_or(TokenError::CiphertextArithmeticFailed)?; confidential_transfer_account.actual_pending_balance_credit_counter = confidential_transfer_account.pending_balance_credit_counter; @@ -1197,6 +944,8 @@ 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, ) } ConfidentialTransferInstruction::ApplyPendingBalance => { diff --git a/token/program-2022/src/extension/confidential_transfer/split_proof_generation.rs b/token/program-2022/src/extension/confidential_transfer/split_proof_generation.rs new file mode 100644 index 00000000000..6fc6111684a --- /dev/null +++ b/token/program-2022/src/extension/confidential_transfer/split_proof_generation.rs @@ -0,0 +1,189 @@ +//! Helper functions to generate split zero-knowledge proofs for confidential transfers in the +//! Confidential Transfer Extension. +//! +//! The logic in this submodule should belong to the `solana-zk-token-sdk` and will be removed with +//! the next upgrade to the Solana program. + +use crate::{ + extension::confidential_transfer::{ + ciphertext_extraction::{transfer_amount_source_ciphertext, SourceDecryptHandles}, + processor::verify_and_split_deposit_amount, + *, + }, + solana_zk_token_sdk::{ + encryption::{ + auth_encryption::{AeCiphertext, AeKey}, + elgamal::{DecryptHandle, ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey}, + grouped_elgamal::GroupedElGamal, + pedersen::Pedersen, + }, + instruction::{ + transfer::TransferAmountCiphertext, BatchedGroupedCiphertext2HandlesValidityProofData, + BatchedRangeProofU128Data, CiphertextCommitmentEqualityProofData, + }, + zk_token_elgamal::ops::subtract_with_lo_hi, + }, +}; + +/// The main logic to create the three split proof data for a transfer. +pub fn transfer_split_proof_data( + current_available_balance: &ElGamalCiphertext, + current_decryptable_available_balance: &AeCiphertext, + transfer_amount: u64, + source_elgamal_keypair: &ElGamalKeypair, + aes_key: &AeKey, + destination_elgamal_pubkey: &ElGamalPubkey, + auditor_elgamal_pubkey: Option<&ElGamalPubkey>, +) -> Result< + ( + CiphertextCommitmentEqualityProofData, + BatchedGroupedCiphertext2HandlesValidityProofData, + BatchedRangeProofU128Data, + SourceDecryptHandles, + ), + TokenError, +> { + let default_auditor_pubkey = ElGamalPubkey::default(); + let auditor_elgamal_pubkey = auditor_elgamal_pubkey.unwrap_or(&default_auditor_pubkey); + + // Split the transfer amount into the low and high bit components. + let (transfer_amount_lo, transfer_amount_hi) = + verify_and_split_deposit_amount(transfer_amount)?; + + // Encrypt the `lo` and `hi` transfer amounts. + let (transfer_amount_grouped_ciphertext_lo, transfer_amount_opening_lo) = + TransferAmountCiphertext::new( + transfer_amount_lo, + source_elgamal_keypair.pubkey(), + destination_elgamal_pubkey, + auditor_elgamal_pubkey, + ); + + let (transfer_amount_grouped_ciphertext_hi, transfer_amount_opening_hi) = + TransferAmountCiphertext::new( + transfer_amount_hi, + source_elgamal_keypair.pubkey(), + destination_elgamal_pubkey, + auditor_elgamal_pubkey, + ); + + // Decrypt the current available balance at the source + let current_decrypted_available_balance = current_decryptable_available_balance + .decrypt(aes_key) + .ok_or(TokenError::AccountDecryption)?; + + // Compute the remaining balance at the source + let new_decrypted_available_balance = current_decrypted_available_balance + .checked_sub(transfer_amount) + .ok_or(TokenError::InsufficientFunds)?; + + // Create a new Pedersen commitment for the remaining balance at the source + let (new_available_balance_commitment, new_source_opening) = + Pedersen::new(new_decrypted_available_balance); + + // Compute the remaining balance at the source as ElGamal ciphertexts + let transfer_amount_source_ciphertext_lo = + transfer_amount_source_ciphertext(&transfer_amount_grouped_ciphertext_lo.into()); + let transfer_amount_source_ciphertext_hi = + transfer_amount_source_ciphertext(&transfer_amount_grouped_ciphertext_hi.into()); + + let current_available_balance = (*current_available_balance).into(); + let new_available_balance_ciphertext = subtract_with_lo_hi( + ¤t_available_balance, + &transfer_amount_source_ciphertext_lo, + &transfer_amount_source_ciphertext_hi, + ) + .ok_or(TokenError::CiphertextArithmeticFailed)?; + let new_available_balance_ciphertext: ElGamalCiphertext = new_available_balance_ciphertext + .try_into() + .map_err(|_| TokenError::MalformedCiphertext)?; + + // generate equality proof data + let equality_proof_data = CiphertextCommitmentEqualityProofData::new( + source_elgamal_keypair, + &new_available_balance_ciphertext, + &new_available_balance_commitment, + &new_source_opening, + new_decrypted_available_balance, + ) + .map_err(|_| TokenError::ProofGeneration)?; + + // create source decrypt handle + let source_decrypt_handle_lo = + DecryptHandle::new(source_elgamal_keypair.pubkey(), &transfer_amount_opening_lo); + let source_decrypt_handle_hi = + DecryptHandle::new(source_elgamal_keypair.pubkey(), &transfer_amount_opening_hi); + + let source_decrypt_handles = SourceDecryptHandles { + lo: source_decrypt_handle_lo.into(), + hi: source_decrypt_handle_hi.into(), + }; + + // encrypt the transfer amount under the destination and auditor ElGamal public key + let transfer_amount_destination_auditor_ciphertext_lo = GroupedElGamal::<2>::encrypt_with( + [destination_elgamal_pubkey, auditor_elgamal_pubkey], + transfer_amount_lo, + &transfer_amount_opening_lo, + ); + let transfer_amount_destination_auditor_ciphertext_hi = GroupedElGamal::<2>::encrypt_with( + [destination_elgamal_pubkey, auditor_elgamal_pubkey], + transfer_amount_hi, + &transfer_amount_opening_hi, + ); + + // generate ciphertext validity data + let ciphertext_validity_proof_data = BatchedGroupedCiphertext2HandlesValidityProofData::new( + destination_elgamal_pubkey, + auditor_elgamal_pubkey, + &transfer_amount_destination_auditor_ciphertext_lo, + &transfer_amount_destination_auditor_ciphertext_hi, + transfer_amount_lo, + transfer_amount_hi, + &transfer_amount_opening_lo, + &transfer_amount_opening_hi, + ) + .map_err(|_| TokenError::ProofGeneration)?; + + // generate range proof data + const REMAINING_BALANCE_BIT_LENGTH: usize = 64; + const TRANSFER_AMOUNT_LO_BIT_LENGTH: usize = 16; + const TRANSFER_AMOUNT_HI_BIT_LENGTH: usize = 32; + const PADDING_BIT_LENGTH: usize = 16; + + let (padding_commitment, padding_opening) = Pedersen::new(0_u64); + + let range_proof_data = BatchedRangeProofU128Data::new( + vec![ + &new_available_balance_commitment, + transfer_amount_grouped_ciphertext_lo.get_commitment(), + transfer_amount_grouped_ciphertext_hi.get_commitment(), + &padding_commitment, + ], + vec![ + new_decrypted_available_balance, + transfer_amount_lo, + transfer_amount_hi, + 0, + ], + vec![ + REMAINING_BALANCE_BIT_LENGTH, + TRANSFER_AMOUNT_LO_BIT_LENGTH, + TRANSFER_AMOUNT_HI_BIT_LENGTH, + PADDING_BIT_LENGTH, + ], + vec![ + &new_source_opening, + &transfer_amount_opening_lo, + &transfer_amount_opening_hi, + &padding_opening, + ], + ) + .map_err(|_| TokenError::ProofGeneration)?; + + Ok(( + equality_proof_data, + ciphertext_validity_proof_data, + range_proof_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 new file mode 100644 index 00000000000..662d27093c1 --- /dev/null +++ b/token/program-2022/src/extension/confidential_transfer/verify_proof.rs @@ -0,0 +1,260 @@ +use { + crate::{ + 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}, + program_error::ProgramError, + sysvar::instructions::get_instruction_relative, + }, + std::slice::Iter, +}; + +/// Verify zero-knowledge proof needed for a [ConfigureAccount] instruction and return the +/// corresponding proof context. +pub fn verify_configure_account_proof( + account_info_iter: &mut Iter<'_, AccountInfo<'_>>, + proof_instruction_offset: i64, +) -> Result { + if proof_instruction_offset == 0 { + // interpret `account_info` as a context state account + let context_state_account_info = next_account_info(account_info_iter)?; + check_zk_token_proof_program_account(context_state_account_info.owner)?; + let context_state_account_data = context_state_account_info.data.borrow(); + let context_state = pod_from_bytes::>( + &context_state_account_data, + )?; + + if context_state.proof_type != ProofType::PubkeyValidity.into() { + return Err(ProgramError::InvalidInstructionData); + } + + Ok(context_state.proof_context) + } else { + // interpret `account_info` as a 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::< + PubkeyValidityData, + PubkeyValidityProofContext, + >( + ProofInstruction::VerifyPubkeyValidity, &zkp_instruction + )?) + } +} + +/// Verify zero-knowledge proof needed for a [EmptyAccount] instruction and return the +/// corresponding proof context. +pub fn verify_empty_account_proof( + account_info_iter: &mut Iter<'_, AccountInfo<'_>>, + proof_instruction_offset: i64, +) -> Result { + if proof_instruction_offset == 0 { + // interpret `account_info` as a context state account + let context_state_account_info = next_account_info(account_info_iter)?; + check_zk_token_proof_program_account(context_state_account_info.owner)?; + let context_state_account_data = context_state_account_info.data.borrow(); + let context_state = pod_from_bytes::>( + &context_state_account_data, + )?; + + if context_state.proof_type != ProofType::ZeroBalance.into() { + return Err(ProgramError::InvalidInstructionData); + } + + Ok(context_state.proof_context) + } else { + // interpret `account_info` as a 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::< + ZeroBalanceProofData, + ZeroBalanceProofContext, + >( + ProofInstruction::VerifyZeroBalance, &zkp_instruction + )?) + } +} + +/// Verify zero-knowledge proof needed for a [Withdraw] instruction and return the +/// corresponding proof context. +pub fn verify_withdraw_proof( + account_info_iter: &mut Iter<'_, AccountInfo<'_>>, + proof_instruction_offset: i64, +) -> Result { + if proof_instruction_offset == 0 { + // interpret `account_info` as a context state account + let context_state_account_info = next_account_info(account_info_iter)?; + check_zk_token_proof_program_account(context_state_account_info.owner)?; + let context_state_account_data = context_state_account_info.data.borrow(); + let context_state = + pod_from_bytes::>(&context_state_account_data)?; + + if context_state.proof_type != ProofType::Withdraw.into() { + return Err(ProgramError::InvalidInstructionData); + } + + Ok(context_state.proof_context) + } else { + // interpret `account_info` as a 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::< + WithdrawData, + WithdrawProofContext, + >( + ProofInstruction::VerifyWithdraw, &zkp_instruction + )?) + } +} + +/// Verify zero-knowledge proof needed for a [Transfer] instruction without fee and return the +/// corresponding proof context. +pub fn verify_transfer_proof( + account_info_iter: &mut Iter<'_, AccountInfo<'_>>, + proof_instruction_offset: i64, + split_proof_context_state_accounts: bool, + source_decrypt_handles: &SourceDecryptHandles, +) -> Result { + 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 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( + &equality_proof_context, + &ciphertext_validity_proof_context, + &range_proof_context, + source_decrypt_handles, + )?) + } 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)?; + check_zk_token_proof_program_account(context_state_account_info.owner)?; + let context_state_account_data = context_state_account_info.data.borrow(); + let context_state = + pod_from_bytes::>(&context_state_account_data)?; + + if context_state.proof_type != ProofType::Transfer.into() { + return Err(ProgramError::InvalidInstructionData); + } + + Ok(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(), + ) + } +} + +/// Verify zero-knowledge proof needed for a [Transfer] instruction with fee and return the +/// corresponding proof context. +pub fn verify_transfer_with_fee_proof( + account_info_iter: &mut Iter<'_, AccountInfo<'_>>, + proof_instruction_offset: i64, + split_proof_context_state_accounts: bool, +) -> Result { + if proof_instruction_offset == 0 && split_proof_context_state_accounts { + // TODO: decode each context state accounts and check consistency between them + unimplemented!() + } 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)?; + check_zk_token_proof_program_account(context_state_account_info.owner)?; + let context_state_account_data = context_state_account_info.data.borrow(); + let context_state = pod_from_bytes::>( + &context_state_account_data, + )?; + + if context_state.proof_type != ProofType::TransferWithFee.into() { + return Err(ProgramError::InvalidInstructionData); + } + + Ok(context_state.proof_context) + } 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::< + TransferWithFeeData, + TransferWithFeeProofContext, + >( + ProofInstruction::VerifyTransferWithFee, + &zkp_instruction, + )?) + } +} + +/// Verify and process equality proof for [Transfer] and [TransferWithFee] instructions. +fn verify_equality_proof( + account_info: &AccountInfo<'_>, +) -> Result { + check_zk_token_proof_program_account(account_info.owner)?; + let context_state_account_data = account_info.data.borrow(); + let equality_proof_context_state = pod_from_bytes::< + ProofContextState, + >(&context_state_account_data)?; + + if equality_proof_context_state.proof_type != ProofType::CiphertextCommitmentEquality.into() { + return Err(ProgramError::InvalidInstructionData); + } + + Ok(equality_proof_context_state.proof_context) +} + +/// Verify and process ciphertext validity proof for [Transfer] and [TransferWithFee] instructions. +fn verify_ciphertext_validity_proof( + account_info: &AccountInfo<'_>, +) -> Result { + check_zk_token_proof_program_account(account_info.owner)?; + let context_state_account_data = account_info.data.borrow(); + let ciphertext_validity_proof_context_state = pod_from_bytes::< + ProofContextState, + >(&context_state_account_data)?; + + if ciphertext_validity_proof_context_state.proof_type + != ProofType::BatchedGroupedCiphertext2HandlesValidity.into() + { + return Err(ProgramError::InvalidInstructionData); + } + + Ok(ciphertext_validity_proof_context_state.proof_context) +} + +/// Verify and process range proof for [Transfer] and [TransferWithFee] instructions. +fn verify_range_proof( + account_info: &AccountInfo<'_>, +) -> Result { + check_zk_token_proof_program_account(account_info.owner)?; + let context_state_account_data = account_info.data.borrow(); + let range_proof_context_state = + pod_from_bytes::>(&context_state_account_data)?; + + if range_proof_context_state.proof_type != ProofType::BatchedRangeProofU128.into() { + return Err(ProgramError::InvalidInstructionData); + } + + Ok(range_proof_context_state.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 650405179c2..c22a7e2bfa9 100644 --- a/token/program-2022/src/extension/confidential_transfer_fee/instruction.rs +++ b/token/program-2022/src/extension/confidential_transfer_fee/instruction.rs @@ -288,6 +288,9 @@ 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( @@ -379,6 +382,9 @@ 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/proof.rs b/token/program-2022/src/proof.rs index 62fe33837ce..da335846f89 100644 --- a/token/program-2022/src/proof.rs +++ b/token/program-2022/src/proof.rs @@ -34,4 +34,7 @@ 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]), } diff --git a/token/program-2022/src/serialization.rs b/token/program-2022/src/serialization.rs index e76d3c72341..ffc99a98a25 100644 --- a/token/program-2022/src/serialization.rs +++ b/token/program-2022/src/serialization.rs @@ -194,6 +194,55 @@ pub mod elgamalpubkey_fromstr { } } +/// helper to ser/deser pod::DecryptHandle values +pub mod decrypthandle_fromstr { + use { + base64::{prelude::BASE64_STANDARD, Engine}, + serde::{ + de::{Error, Visitor}, + Deserializer, Serializer, + }, + solana_zk_token_sdk::zk_token_elgamal::pod::DecryptHandle, + std::fmt, + }; + + const DECRYPT_HANDLE_LEN: usize = 32; + + /// Serialize a decrypt handle as a base64 string + pub fn serialize(x: &DecryptHandle, s: S) -> Result + where + S: Serializer, + { + s.serialize_str(&BASE64_STANDARD.encode(x.0)) + } + + struct DecryptHandleVisitor; + + impl<'de> Visitor<'de> for DecryptHandleVisitor { + type Value = DecryptHandle; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a FromStr type") + } + + fn visit_str(self, v: &str) -> Result + where + E: Error, + { + let array = super::base64_to_bytes::(v)?; + Ok(DecryptHandle(array)) + } + } + + /// Deserialize a DecryptHandle from a base64 string + pub fn deserialize<'de, D>(d: D) -> Result + where + D: Deserializer<'de>, + { + d.deserialize_str(DecryptHandleVisitor) + } +} + /// deserialization Visitors for local types pub mod visitors { use {