From 779eaba30d3cecb91ba2b890eca389555f8667f1 Mon Sep 17 00:00:00 2001 From: Sam Kim Date: Fri, 11 Aug 2023 16:51:02 +0900 Subject: [PATCH 01/14] refactor proof verification helper functions to a separate submodule --- .../extension/confidential_transfer/mod.rs | 3 + .../confidential_transfer/processor.rs | 160 +---------------- .../confidential_transfer/verify_proof.rs | 165 ++++++++++++++++++ 3 files changed, 170 insertions(+), 158 deletions(-) create mode 100644 token/program-2022/src/extension/confidential_transfer/verify_proof.rs diff --git a/token/program-2022/src/extension/confidential_transfer/mod.rs b/token/program-2022/src/extension/confidential_transfer/mod.rs index 28ebe37b137..41b1ef9b3a6 100644 --- a/token/program-2022/src/extension/confidential_transfer/mod.rs +++ b/token/program-2022/src/extension/confidential_transfer/mod.rs @@ -23,6 +23,9 @@ 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; + /// Confidential Transfer Extension account information needed for instructions #[cfg(not(target_os = "solana"))] pub mod account_info; diff --git a/token/program-2022/src/extension/confidential_transfer/processor.rs b/token/program-2022/src/extension/confidential_transfer/processor.rs index 08ea8a23793..f2faef98fcf 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::{instruction::*, verify_proof::*, *}, confidential_transfer_fee::{ ConfidentialTransferFeeAmount, ConfidentialTransferFeeConfig, EncryptedFee, EncryptedWithheldAmount, @@ -14,7 +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}, }, @@ -25,7 +24,6 @@ use { msg, program_error::ProgramError, pubkey::Pubkey, - sysvar::instructions::get_instruction_relative, sysvar::Sysvar, }, }; @@ -163,37 +161,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(); @@ -275,37 +242,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( @@ -490,36 +426,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( @@ -692,68 +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 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..137c116c4b8 --- /dev/null +++ b/token/program-2022/src/extension/confidential_transfer/verify_proof.rs @@ -0,0 +1,165 @@ +use { + crate::{ + check_zk_token_proof_program_account, + extension::confidential_transfer::{instruction::*, *}, + proof::decode_proof_instruction_context, + }, + solana_program::{ + account_info::AccountInfo, program_error::ProgramError, + sysvar::instructions::get_instruction_relative, + }, +}; + +/// Verify zero-knowledge proof needed for a [ConfigureAccount] instruction and return the +/// corresponding proof context. +pub 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 + )?) + } +} + +/// Verify zero-knowledge proof needed for a [EmptyAccount] instruction and return the +/// corresponding proof context. +pub 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 + )?) + } +} + +/// Verify zero-knowledge proof needed for a [Withdraw] instruction and return the +/// corresponding proof context. +pub 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 + )?) + } +} + +/// Verify zero-knowledge proof needed for a [Transfer] instruction without fee and return the +/// corresponding proof context. +pub 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. +pub 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, + )?) + } +} From 7ee73cb34e210440541ff63c04f82cc9e0059fea Mon Sep 17 00:00:00 2001 From: Sam Kim Date: Fri, 11 Aug 2023 17:28:07 +0900 Subject: [PATCH 02/14] add `ProofLocation::SplitContextStateAccounts` variant --- token/program-2022/src/error.rs | 12 +++ .../confidential_transfer/instruction.rs | 74 ++++++++++++++++--- .../confidential_transfer_fee/instruction.rs | 6 ++ token/program-2022/src/proof.rs | 3 + 4 files changed, 83 insertions(+), 12 deletions(-) diff --git a/token/program-2022/src/error.rs b/token/program-2022/src/error.rs index 27475c8233c..6b17348f985 100644 --- a/token/program-2022/src/error.rs +++ b/token/program-2022/src/error.rs @@ -218,6 +218,12 @@ 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, } impl From for ProgramError { fn from(e: TokenError) -> Self { @@ -381,6 +387,12 @@ 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") + } } } } diff --git a/token/program-2022/src/extension/confidential_transfer/instruction.rs b/token/program-2022/src/extension/confidential_transfer/instruction.rs index 989c1c00e47..309b828e999 100644 --- a/token/program-2022/src/extension/confidential_transfer/instruction.rs +++ b/token/program-2022/src/extension/confidential_transfer/instruction.rs @@ -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,8 @@ 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, } /// Data expected by `ConfidentialTransferInstruction::ApplyPendingBalance` @@ -555,6 +565,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 +681,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 +802,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( @@ -874,14 +893,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) } }; @@ -902,6 +935,7 @@ pub fn inner_transfer( &TransferInstructionData { new_source_decryptable_available_balance, proof_instruction_offset, + split_proof_context_state_accounts: split_proof_context_state_accounts.into(), }, )) } @@ -968,14 +1002,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) } }; @@ -996,6 +1045,7 @@ 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(), }, )) } 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]), } From c26ec15c93e4c6288e934c427e2c9caf42f1ad75 Mon Sep 17 00:00:00 2001 From: Sam Kim Date: Fri, 11 Aug 2023 18:05:24 +0900 Subject: [PATCH 03/14] plumbing for split context states in the processor --- .../confidential_transfer/processor.rs | 40 +++++----- .../confidential_transfer/verify_proof.rs | 75 ++++++++++++------- 2 files changed, 70 insertions(+), 45 deletions(-) diff --git a/token/program-2022/src/extension/confidential_transfer/processor.rs b/token/program-2022/src/extension/confidential_transfer/processor.rs index f2faef98fcf..00bd04ae32c 100644 --- a/token/program-2022/src/extension/confidential_transfer/processor.rs +++ b/token/program-2022/src/extension/confidential_transfer/processor.rs @@ -101,10 +101,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(); @@ -201,10 +199,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(); @@ -350,10 +345,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(); @@ -433,17 +425,13 @@ fn process_transfer( accounts: &[AccountInfo], new_source_decryptable_available_balance: DecryptableBalance, proof_instruction_offset: i64, + split_proof_context_state_accounts: bool, ) -> 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)?; @@ -466,7 +454,13 @@ 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, + )?; + + 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. @@ -515,8 +509,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 @@ -1041,6 +1040,7 @@ 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(), ) } ConfidentialTransferInstruction::ApplyPendingBalance => { 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 137c116c4b8..dbcb928af6f 100644 --- a/token/program-2022/src/extension/confidential_transfer/verify_proof.rs +++ b/token/program-2022/src/extension/confidential_transfer/verify_proof.rs @@ -5,21 +5,24 @@ use { proof::decode_proof_instruction_context, }, solana_program::{ - account_info::AccountInfo, program_error::ProgramError, + 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: &AccountInfo<'_>, + account_info_iter: &mut Iter<'_, 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_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, )?; @@ -31,7 +34,9 @@ pub fn verify_configure_account_proof( Ok(context_state.proof_context) } else { // interpret `account_info` as a sysvar - let zkp_instruction = get_instruction_relative(proof_instruction_offset, account_info)?; + 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, @@ -44,13 +49,14 @@ pub fn verify_configure_account_proof( /// Verify zero-knowledge proof needed for a [EmptyAccount] instruction and return the /// corresponding proof context. pub fn verify_empty_account_proof( - account_info: &AccountInfo<'_>, + account_info_iter: &mut Iter<'_, 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_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, )?; @@ -62,7 +68,9 @@ pub fn verify_empty_account_proof( Ok(context_state.proof_context) } else { // interpret `account_info` as a sysvar - let zkp_instruction = get_instruction_relative(proof_instruction_offset, account_info)?; + 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, @@ -75,13 +83,14 @@ pub fn verify_empty_account_proof( /// Verify zero-knowledge proof needed for a [Withdraw] instruction and return the /// corresponding proof context. pub fn verify_withdraw_proof( - account_info: &AccountInfo<'_>, + account_info_iter: &mut Iter<'_, 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_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)?; @@ -92,7 +101,9 @@ pub fn verify_withdraw_proof( Ok(context_state.proof_context) } else { // interpret `account_info` as a sysvar - let zkp_instruction = get_instruction_relative(proof_instruction_offset, account_info)?; + 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, @@ -105,13 +116,18 @@ pub fn verify_withdraw_proof( /// Verify zero-knowledge proof needed for a [Transfer] instruction without fee and return the /// corresponding proof context. pub fn verify_transfer_proof( - account_info: &AccountInfo<'_>, + account_info_iter: &mut Iter<'_, AccountInfo<'_>>, proof_instruction_offset: i64, + split_proof_context_state_accounts: bool, ) -> Result { - if proof_instruction_offset == 0 { + 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 - check_zk_token_proof_program_account(account_info.owner)?; - let context_state_account_data = account_info.data.borrow(); + 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)?; @@ -121,8 +137,10 @@ pub fn verify_transfer_proof( Ok(context_state.proof_context) } else { - // interpret `account_info` as a sysvar - let zkp_instruction = get_instruction_relative(proof_instruction_offset, account_info)?; + // 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::< TransferData, TransferProofContext, @@ -135,13 +153,18 @@ pub fn verify_transfer_proof( /// 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: &AccountInfo<'_>, + account_info_iter: &mut Iter<'_, AccountInfo<'_>>, proof_instruction_offset: i64, + split_proof_context_state_accounts: bool, ) -> Result { - if proof_instruction_offset == 0 { + 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 - check_zk_token_proof_program_account(account_info.owner)?; - let context_state_account_data = account_info.data.borrow(); + 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, )?; @@ -152,8 +175,10 @@ pub fn verify_transfer_with_fee_proof( Ok(context_state.proof_context) } else { - // interpret `account_info` as a sysvar - let zkp_instruction = get_instruction_relative(proof_instruction_offset, account_info)?; + // 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, From fee378f28427a78b7321447f60e377eafaee7f3f Mon Sep 17 00:00:00 2001 From: Sam Kim Date: Sun, 13 Aug 2023 07:46:21 +0900 Subject: [PATCH 04/14] refactor ciphertext extraction into a separate submodule --- .../ciphertext_extraction.rs | 137 ++++++++++++++++++ .../extension/confidential_transfer/mod.rs | 5 + .../confidential_transfer/processor.rs | 100 +------------ 3 files changed, 143 insertions(+), 99 deletions(-) create mode 100644 token/program-2022/src/extension/confidential_transfer/ciphertext_extraction.rs 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..73b9bba3f52 --- /dev/null +++ b/token/program-2022/src/extension/confidential_transfer/ciphertext_extraction.rs @@ -0,0 +1,137 @@ +//! 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_zk_token_sdk::{ + instruction::transfer::TransferProofContext, + zk_token_elgamal::pod::{ + DecryptHandle, GroupedElGamalCiphertext2Handles, GroupedElGamalCiphertext3Handles, + PedersenCommitment, TransferAmountCiphertext, + }, + }, +}; + +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, + )) +} diff --git a/token/program-2022/src/extension/confidential_transfer/mod.rs b/token/program-2022/src/extension/confidential_transfer/mod.rs index 41b1ef9b3a6..3c7fb38aaeb 100644 --- a/token/program-2022/src/extension/confidential_transfer/mod.rs +++ b/token/program-2022/src/extension/confidential_transfer/mod.rs @@ -30,6 +30,11 @@ pub mod verify_proof; #[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 00bd04ae32c..4583f92985d 100644 --- a/token/program-2022/src/extension/confidential_transfer/processor.rs +++ b/token/program-2022/src/extension/confidential_transfer/processor.rs @@ -3,7 +3,7 @@ use { check_program_account, error::TokenError, extension::{ - confidential_transfer::{instruction::*, verify_proof::*, *}, + confidential_transfer::{ciphertext_extraction::*, instruction::*, verify_proof::*, *}, confidential_transfer_fee::{ ConfidentialTransferFeeAmount, ConfidentialTransferFeeConfig, EncryptedFee, EncryptedWithheldAmount, @@ -14,7 +14,6 @@ use { }, instruction::{decode_instruction_data, decode_instruction_type}, processor::Processor, - solana_zk_token_sdk::zk_token_elgamal::pod::TransferAmountCiphertext, state::{Account, Mint}, }, solana_program::{ @@ -597,103 +596,6 @@ fn process_transfer( Ok(()) } -/// 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( From 4de36f4ed665ccd2b30bb16952c7b523423c9329 Mon Sep 17 00:00:00 2001 From: Sam Kim Date: Mon, 14 Aug 2023 18:39:03 +0900 Subject: [PATCH 05/14] add `TransferProofContextInfo` and split proof consistency logic --- .../ciphertext_extraction.rs | 153 +++++++++++++++++- .../confidential_transfer/verify_proof.rs | 90 +++++++++-- 2 files changed, 228 insertions(+), 15 deletions(-) diff --git a/token/program-2022/src/extension/confidential_transfer/ciphertext_extraction.rs b/token/program-2022/src/extension/confidential_transfer/ciphertext_extraction.rs index 73b9bba3f52..41cea30d7cc 100644 --- a/token/program-2022/src/extension/confidential_transfer/ciphertext_extraction.rs +++ b/token/program-2022/src/extension/confidential_transfer/ciphertext_extraction.rs @@ -4,11 +4,19 @@ use crate::{ extension::{confidential_transfer::*, confidential_transfer_fee::EncryptedFee}, + solana_program::program_error::ProgramError, solana_zk_token_sdk::{ - instruction::transfer::TransferProofContext, - zk_token_elgamal::pod::{ - DecryptHandle, GroupedElGamalCiphertext2Handles, GroupedElGamalCiphertext3Handles, - PedersenCommitment, TransferAmountCiphertext, + encryption::pedersen::Pedersen, + instruction::{ + transfer::TransferProofContext, BatchedGroupedCiphertext2HandlesValidityProofContext, + BatchedRangeProofContext, CiphertextCommitmentEqualityProofContext, + }, + zk_token_elgamal::{ + ops::subtract, + pod::{ + DecryptHandle, GroupedElGamalCiphertext2Handles, GroupedElGamalCiphertext3Handles, + PedersenCommitment, TransferAmountCiphertext, + }, }, }, }; @@ -135,3 +143,140 @@ pub(crate) fn transfer_amount_encryption_from_decrypt_handle( 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, + ) -> 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, + }; + + Ok(Self { + ciphertext_lo: *transfer_amount_ciphertext_lo, + ciphertext_hi: *transfer_amount_ciphertext_hi, + transfer_pubkeys, + new_source_ciphertext: *new_source_ciphertext, + }) + } +} 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 dbcb928af6f..88ef1b2d1bf 100644 --- a/token/program-2022/src/extension/confidential_transfer/verify_proof.rs +++ b/token/program-2022/src/extension/confidential_transfer/verify_proof.rs @@ -1,7 +1,7 @@ use { crate::{ check_zk_token_proof_program_account, - extension::confidential_transfer::{instruction::*, *}, + extension::confidential_transfer::{ciphertext_extraction::*, instruction::*, *}, proof::decode_proof_instruction_context, }, solana_program::{ @@ -119,10 +119,25 @@ pub fn verify_transfer_proof( account_info_iter: &mut Iter<'_, AccountInfo<'_>>, proof_instruction_offset: i64, split_proof_context_state_accounts: bool, -) -> Result { +) -> Result { if proof_instruction_offset == 0 && split_proof_context_state_accounts { - // TODO: decode each context state accounts and check consistency between them - unimplemented!() + 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, + )?) } 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)?; @@ -135,18 +150,19 @@ pub fn verify_transfer_proof( return Err(ProgramError::InvalidInstructionData); } - Ok(context_state.proof_context) + 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::< - TransferData, - TransferProofContext, - >( - ProofInstruction::VerifyTransfer, &zkp_instruction - )?) + Ok( + (*decode_proof_instruction_context::( + ProofInstruction::VerifyTransfer, + &zkp_instruction, + )?) + .into(), + ) } } @@ -188,3 +204,55 @@ pub fn verify_transfer_with_fee_proof( )?) } } + +/// 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) +} From f0049cec0c16723cf2c7b0cfb2b173d82bcd89fc Mon Sep 17 00:00:00 2001 From: Sam Kim Date: Mon, 14 Aug 2023 18:39:25 +0900 Subject: [PATCH 06/14] add temporary type `SourceDecryptHandles` --- .../ciphertext_extraction.rs | 32 +++++++++++++++++-- .../confidential_transfer/instruction.rs | 28 +++++++++++++++- .../confidential_transfer/processor.rs | 3 ++ .../confidential_transfer/verify_proof.rs | 2 ++ 4 files changed, 62 insertions(+), 3 deletions(-) diff --git a/token/program-2022/src/extension/confidential_transfer/ciphertext_extraction.rs b/token/program-2022/src/extension/confidential_transfer/ciphertext_extraction.rs index 41cea30d7cc..eff5363c423 100644 --- a/token/program-2022/src/extension/confidential_transfer/ciphertext_extraction.rs +++ b/token/program-2022/src/extension/confidential_transfer/ciphertext_extraction.rs @@ -192,6 +192,7 @@ impl TransferProofContextInfo { 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 @@ -272,11 +273,38 @@ impl TransferProofContextInfo { 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, + 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. +#[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. + pub lo: DecryptHandle, + /// The ElGamal decryption handle pertaining to the low 32 bits of the transfer amount. + 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 309b828e999..c361314460b 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, }, @@ -464,6 +464,12 @@ pub struct TransferInstructionData { 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` @@ -885,6 +891,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![ @@ -927,6 +934,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, @@ -936,6 +949,7 @@ pub fn inner_transfer( new_source_decryptable_available_balance, proof_instruction_offset, split_proof_context_state_accounts: split_proof_context_state_accounts.into(), + source_decrypt_handles, }, )) } @@ -952,6 +966,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, @@ -962,6 +977,7 @@ pub fn transfer( authority, multisig_signers, proof_data_location, + source_decrypt_handles, )?]; if let ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) = @@ -994,6 +1010,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![ @@ -1037,6 +1054,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, @@ -1046,6 +1069,7 @@ pub fn inner_transfer_with_fee( new_source_decryptable_available_balance, proof_instruction_offset, split_proof_context_state_accounts: split_proof_context_state_accounts.into(), + source_decrypt_handles, }, )) } @@ -1062,6 +1086,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, @@ -1072,6 +1097,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/processor.rs b/token/program-2022/src/extension/confidential_transfer/processor.rs index 4583f92985d..f84bd7e9a8b 100644 --- a/token/program-2022/src/extension/confidential_transfer/processor.rs +++ b/token/program-2022/src/extension/confidential_transfer/processor.rs @@ -425,6 +425,7 @@ fn process_transfer( 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)?; @@ -457,6 +458,7 @@ fn process_transfer( account_info_iter, proof_instruction_offset, split_proof_context_state_accounts, + source_decrypt_handles, )?; let authority_info = next_account_info(account_info_iter)?; @@ -943,6 +945,7 @@ pub(crate) fn process_instruction( 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/verify_proof.rs b/token/program-2022/src/extension/confidential_transfer/verify_proof.rs index 88ef1b2d1bf..662d27093c1 100644 --- a/token/program-2022/src/extension/confidential_transfer/verify_proof.rs +++ b/token/program-2022/src/extension/confidential_transfer/verify_proof.rs @@ -119,6 +119,7 @@ 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)?; @@ -137,6 +138,7 @@ pub fn verify_transfer_proof( &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 From 78ce6eb19d84979e7162a406fc271a12718d6395 Mon Sep 17 00:00:00 2001 From: Sam Kim Date: Mon, 14 Aug 2023 18:39:42 +0900 Subject: [PATCH 07/14] update client for split transfer proofs --- token/client/src/token.rs | 26 ++++++++++++++++--- .../tests/confidential_transfer.rs | 25 +++++++++++++++--- .../confidential_transfer/instruction.rs | 20 ++++++++++++++ 3 files changed, 64 insertions(+), 7 deletions(-) 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..8102f5c3642 100644 --- a/token/program-2022-test/tests/confidential_transfer.rs +++ b/token/program-2022-test/tests/confidential_transfer.rs @@ -18,7 +18,8 @@ use { error::TokenError, extension::{ confidential_transfer::{ - self, ConfidentialTransferAccount, MAXIMUM_DEPOSIT_TRANSFER_AMOUNT, + self, instruction::TransferContextStateAccounts, ConfidentialTransferAccount, + MAXIMUM_DEPOSIT_TRANSFER_AMOUNT, }, BaseStateWithExtensions, ExtensionType, }, @@ -972,6 +973,7 @@ async fn confidential_transfer_transfer() { alice_meta.elgamal_keypair.pubkey(), Some(auditor_elgamal_keypair.pubkey()), &[&alice], + None, ) .await .unwrap(); @@ -1002,6 +1004,7 @@ async fn confidential_transfer_transfer() { alice_meta.elgamal_keypair.pubkey(), Some(auditor_elgamal_keypair.pubkey()), &[&alice], + None, ) .await .unwrap(); @@ -1055,6 +1058,7 @@ async fn confidential_transfer_transfer() { bob_meta.elgamal_keypair.pubkey(), Some(auditor_elgamal_keypair.pubkey()), &[&alice], + None, ) .await .unwrap(); @@ -1096,6 +1100,7 @@ async fn confidential_transfer_transfer() { bob_meta.elgamal_keypair.pubkey(), Some(auditor_elgamal_keypair.pubkey()), &[&bob], + None, ) .await .unwrap(); @@ -1113,6 +1118,7 @@ async fn confidential_transfer_transfer() { bob_meta.elgamal_keypair.pubkey(), Some(auditor_elgamal_keypair.pubkey()), &[&bob], + None, ) .await .unwrap_err(); @@ -1232,6 +1238,7 @@ async fn confidential_transfer_transfer_with_fee() { TEST_FEE_BASIS_POINTS, TEST_MAXIMUM_FEE, &[&alice], + None, ) .await .unwrap(); @@ -1265,6 +1272,7 @@ async fn confidential_transfer_transfer_with_fee() { TEST_FEE_BASIS_POINTS, TEST_MAXIMUM_FEE, &[&alice], + None, ) .await .unwrap(); @@ -1321,6 +1329,7 @@ async fn confidential_transfer_transfer_with_fee() { TEST_FEE_BASIS_POINTS, TEST_MAXIMUM_FEE, &[&alice], + None, ) .await .unwrap(); @@ -1465,6 +1474,7 @@ async fn confidential_transfer_transfer_memo() { bob_meta.elgamal_keypair.pubkey(), Some(auditor_elgamal_keypair.pubkey()), &[&alice], + None, ) .await .unwrap_err(); @@ -1494,6 +1504,7 @@ async fn confidential_transfer_transfer_memo() { bob_meta.elgamal_keypair.pubkey(), Some(auditor_elgamal_keypair.pubkey()), &[&alice], + None, ) .await .unwrap(); @@ -1600,6 +1611,7 @@ async fn confidential_transfer_transfer_with_fee_and_memo() { TEST_FEE_BASIS_POINTS, TEST_MAXIMUM_FEE, &[&alice], + None, ) .await .unwrap_err(); @@ -1631,6 +1643,7 @@ async fn confidential_transfer_transfer_with_fee_and_memo() { TEST_FEE_BASIS_POINTS, TEST_MAXIMUM_FEE, &[&alice], + None, ) .await .unwrap(); @@ -2268,7 +2281,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 +2291,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 +2355,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 +2365,7 @@ async fn confidential_transfer_transfer_with_proof_context() { bob_meta.elgamal_keypair.pubkey(), Some(auditor_elgamal_keypair.pubkey()), &[&alice], + None, ) .await .unwrap_err(); diff --git a/token/program-2022/src/extension/confidential_transfer/instruction.rs b/token/program-2022/src/extension/confidential_transfer/instruction.rs index c361314460b..8c7d4a5e0fb 100644 --- a/token/program-2022/src/extension/confidential_transfer/instruction.rs +++ b/token/program-2022/src/extension/confidential_transfer/instruction.rs @@ -485,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( From 684ca3859da5fda79a8b1ebf1c97050427de36ca Mon Sep 17 00:00:00 2001 From: Sam Kim Date: Tue, 15 Aug 2023 07:41:43 +0900 Subject: [PATCH 08/14] add transfer split proof generation logic --- .../confidential_transfer/account_info.rs | 40 +++- .../ciphertext_extraction.rs | 10 +- .../extension/confidential_transfer/mod.rs | 7 + .../confidential_transfer/processor.rs | 2 +- .../split_proof_generation.rs | 188 ++++++++++++++++++ 5 files changed, 237 insertions(+), 10 deletions(-) create mode 100644 token/program-2022/src/extension/confidential_transfer/split_proof_generation.rs 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..b1bf3e22a41 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, }, }, }; @@ -264,6 +267,39 @@ 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().unwrap(); // TODO: + let current_decryptable_available_balance = + self.decryptable_available_balance.try_into().unwrap(); // TODO: replace with a + // suitable error type + 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( diff --git a/token/program-2022/src/extension/confidential_transfer/ciphertext_extraction.rs b/token/program-2022/src/extension/confidential_transfer/ciphertext_extraction.rs index eff5363c423..15189dc68f8 100644 --- a/token/program-2022/src/extension/confidential_transfer/ciphertext_extraction.rs +++ b/token/program-2022/src/extension/confidential_transfer/ciphertext_extraction.rs @@ -6,17 +6,13 @@ use crate::{ extension::{confidential_transfer::*, confidential_transfer_fee::EncryptedFee}, solana_program::program_error::ProgramError, solana_zk_token_sdk::{ - encryption::pedersen::Pedersen, instruction::{ transfer::TransferProofContext, BatchedGroupedCiphertext2HandlesValidityProofContext, BatchedRangeProofContext, CiphertextCommitmentEqualityProofContext, }, - zk_token_elgamal::{ - ops::subtract, - pod::{ - DecryptHandle, GroupedElGamalCiphertext2Handles, GroupedElGamalCiphertext3Handles, - PedersenCommitment, TransferAmountCiphertext, - }, + zk_token_elgamal::pod::{ + DecryptHandle, GroupedElGamalCiphertext2Handles, GroupedElGamalCiphertext3Handles, + PedersenCommitment, TransferAmountCiphertext, }, }, }; diff --git a/token/program-2022/src/extension/confidential_transfer/mod.rs b/token/program-2022/src/extension/confidential_transfer/mod.rs index 3c7fb38aaeb..3cf9842f66d 100644 --- a/token/program-2022/src/extension/confidential_transfer/mod.rs +++ b/token/program-2022/src/extension/confidential_transfer/mod.rs @@ -26,6 +26,13 @@ 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. +pub mod split_proof_generation; + /// Confidential Transfer Extension account information needed for instructions #[cfg(not(target_os = "solana"))] pub mod account_info; diff --git a/token/program-2022/src/extension/confidential_transfer/processor.rs b/token/program-2022/src/extension/confidential_transfer/processor.rs index f84bd7e9a8b..967fbe64a13 100644 --- a/token/program-2022/src/extension/confidential_transfer/processor.rs +++ b/token/program-2022/src/extension/confidential_transfer/processor.rs @@ -319,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); } 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..82688b20dee --- /dev/null +++ b/token/program-2022/src/extension/confidential_transfer/split_proof_generation.rs @@ -0,0 +1,188 @@ +//! 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, + ) + .unwrap(); // TODO: replace with a suitable error type + let new_available_balance_ciphertext: ElGamalCiphertext = + new_available_balance_ciphertext.try_into().unwrap(); // TODO: replace with a suitable error type + + // 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, + )) +} From c3be8faff5cc6f9ff117aee68ccaee49acdd6dd4 Mon Sep 17 00:00:00 2001 From: Sam Kim Date: Tue, 15 Aug 2023 07:41:58 +0900 Subject: [PATCH 09/14] add test for split proof transfer --- .../tests/confidential_transfer.rs | 248 +++++++++++++++++- 1 file changed, 245 insertions(+), 3 deletions(-) diff --git a/token/program-2022-test/tests/confidential_transfer.rs b/token/program-2022-test/tests/confidential_transfer.rs index 8102f5c3642..26468713e24 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,8 +18,10 @@ use { error::TokenError, extension::{ confidential_transfer::{ - self, instruction::TransferContextStateAccounts, ConfidentialTransferAccount, - MAXIMUM_DEPOSIT_TRANSFER_AMOUNT, + self, + account_info::TransferAccountInfo, + instruction::{TransferContextStateAccounts, TransferSplitContextStateAccounts}, + ConfidentialTransferAccount, MAXIMUM_DEPOSIT_TRANSFER_AMOUNT, }, BaseStateWithExtensions, ExtensionType, }, @@ -2377,3 +2379,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; +} From 5682484bc487344c68569eb27b1589fead04ac74 Mon Sep 17 00:00:00 2001 From: Sam Kim Date: Tue, 15 Aug 2023 08:18:00 +0900 Subject: [PATCH 10/14] clippy and target os --- token/program-2022-test/tests/confidential_transfer.rs | 3 ++- .../extension/confidential_transfer/ciphertext_extraction.rs | 2 +- token/program-2022/src/extension/confidential_transfer/mod.rs | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/token/program-2022-test/tests/confidential_transfer.rs b/token/program-2022-test/tests/confidential_transfer.rs index 26468713e24..45db13e50f6 100644 --- a/token/program-2022-test/tests/confidential_transfer.rs +++ b/token/program-2022-test/tests/confidential_transfer.rs @@ -113,6 +113,7 @@ impl ConfidentialTokenAccountMeta { } } + #[allow(clippy::too_many_arguments)] #[cfg(feature = "zk-ops")] async fn new_with_tokens( token: &Token, @@ -2439,7 +2440,7 @@ async fn confidential_transfer_transfer_with_split_proof_context() { let extension = state .get_extension::() .unwrap(); - let transfer_account_info = TransferAccountInfo::new(&extension); + let transfer_account_info = TransferAccountInfo::new(extension); let ( equality_proof_data, diff --git a/token/program-2022/src/extension/confidential_transfer/ciphertext_extraction.rs b/token/program-2022/src/extension/confidential_transfer/ciphertext_extraction.rs index 15189dc68f8..f232e8f2c03 100644 --- a/token/program-2022/src/extension/confidential_transfer/ciphertext_extraction.rs +++ b/token/program-2022/src/extension/confidential_transfer/ciphertext_extraction.rs @@ -132,7 +132,7 @@ pub(crate) fn transfer_amount_encryption_from_decrypt_handle( 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[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( diff --git a/token/program-2022/src/extension/confidential_transfer/mod.rs b/token/program-2022/src/extension/confidential_transfer/mod.rs index 3cf9842f66d..d69942241cf 100644 --- a/token/program-2022/src/extension/confidential_transfer/mod.rs +++ b/token/program-2022/src/extension/confidential_transfer/mod.rs @@ -31,6 +31,7 @@ pub mod verify_proof; /// /// 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 From 655bac546574e6b3be4b26d0d83ba87d5427d678 Mon Sep 17 00:00:00 2001 From: Sam Kim Date: Tue, 15 Aug 2023 12:32:46 +0900 Subject: [PATCH 11/14] fix proof check bool direction --- .../extension/confidential_transfer/ciphertext_extraction.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/token/program-2022/src/extension/confidential_transfer/ciphertext_extraction.rs b/token/program-2022/src/extension/confidential_transfer/ciphertext_extraction.rs index f232e8f2c03..c2e36f6d0fc 100644 --- a/token/program-2022/src/extension/confidential_transfer/ciphertext_extraction.rs +++ b/token/program-2022/src/extension/confidential_transfer/ciphertext_extraction.rs @@ -234,7 +234,7 @@ impl TransferProofContextInfo { // the fourth dummy commitment can be any commitment ]; - if range_proof_commitments + if !range_proof_commitments .iter() .zip(expected_commitments.iter()) .all(|(proof_commitment, expected_commitment)| proof_commitment == expected_commitment) @@ -255,7 +255,7 @@ impl TransferProofContextInfo { ] .iter(); - if range_proof_bit_lengths + if !range_proof_bit_lengths .iter() .zip(expected_bit_lengths) .all(|(proof_len, expected_len)| proof_len == expected_len) From 11c9c1fafb3de967b11e1ce0333ed10d6ef5fb02 Mon Sep 17 00:00:00 2001 From: Sam Kim Date: Tue, 15 Aug 2023 12:47:38 +0900 Subject: [PATCH 12/14] add `MalformedCiphertext` and `CiphertextArithmeticFailed` error variants --- token/program-2022/src/error.rs | 14 +++++++++ .../confidential_transfer/account_info.rs | 31 +++++++++++-------- .../confidential_transfer/processor.rs | 20 ++++++------ .../split_proof_generation.rs | 7 +++-- 4 files changed, 46 insertions(+), 26 deletions(-) diff --git a/token/program-2022/src/error.rs b/token/program-2022/src/error.rs index 6b17348f985..6da2fdb2b3d 100644 --- a/token/program-2022/src/error.rs +++ b/token/program-2022/src/error.rs @@ -224,6 +224,14 @@ pub enum TokenError { /// 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 { @@ -393,6 +401,12 @@ impl PrintProgramError for TokenError { 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 b1bf3e22a41..3e5a6ecdba3 100644 --- a/token/program-2022/src/extension/confidential_transfer/account_info.rs +++ b/token/program-2022/src/extension/confidential_transfer/account_info.rs @@ -47,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) @@ -93,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) @@ -106,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) @@ -116,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) @@ -165,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) @@ -181,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( @@ -230,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) @@ -248,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)?; @@ -285,10 +285,15 @@ impl TransferAccountInfo { ), TokenError, > { - let current_available_balance = self.available_balance.try_into().unwrap(); // TODO: - let current_decryptable_available_balance = - self.decryptable_available_balance.try_into().unwrap(); // TODO: replace with a - // suitable error type + 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, @@ -316,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/processor.rs b/token/program-2022/src/extension/confidential_transfer/processor.rs index 967fbe64a13..0c0aee9ab5f 100644 --- a/token/program-2022/src/extension/confidential_transfer/processor.rs +++ b/token/program-2022/src/extension/confidential_transfer/processor.rs @@ -303,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()?; @@ -398,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. @@ -648,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. @@ -701,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()?; @@ -722,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 @@ -745,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(()) @@ -786,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; 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 index 82688b20dee..6fc6111684a 100644 --- a/token/program-2022/src/extension/confidential_transfer/split_proof_generation.rs +++ b/token/program-2022/src/extension/confidential_transfer/split_proof_generation.rs @@ -93,9 +93,10 @@ pub fn transfer_split_proof_data( &transfer_amount_source_ciphertext_lo, &transfer_amount_source_ciphertext_hi, ) - .unwrap(); // TODO: replace with a suitable error type - let new_available_balance_ciphertext: ElGamalCiphertext = - new_available_balance_ciphertext.try_into().unwrap(); // TODO: replace with a suitable error type + .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( From 5018647be94f5af5ec9faaaabc9795fba7e4e1bd Mon Sep 17 00:00:00 2001 From: Sam Kim Date: Thu, 17 Aug 2023 19:04:04 +0900 Subject: [PATCH 13/14] update syntax for confidential transfer with fee tests --- token/program-2022-test/tests/confidential_transfer_fee.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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(); From 906f4b931f1ae16c31df4a19ee265f2848176db4 Mon Sep 17 00:00:00 2001 From: Jon Cinque Date: Fri, 18 Aug 2023 01:44:45 +0200 Subject: [PATCH 14/14] Add serializers for instructions --- .../ciphertext_extraction.rs | 9 ++++ token/program-2022/src/serialization.rs | 49 +++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/token/program-2022/src/extension/confidential_transfer/ciphertext_extraction.rs b/token/program-2022/src/extension/confidential_transfer/ciphertext_extraction.rs index c2e36f6d0fc..7af513911e2 100644 --- a/token/program-2022/src/extension/confidential_transfer/ciphertext_extraction.rs +++ b/token/program-2022/src/extension/confidential_transfer/ciphertext_extraction.rs @@ -17,6 +17,12 @@ use crate::{ }, }; +#[cfg(feature = "serde-traits")] +use { + crate::serialization::decrypthandle_fromstr, + serde::{Deserialize, Serialize}, +}; + pub(crate) fn transfer_amount_commitment( transfer_amount_ciphertext: &GroupedElGamalCiphertext2Handles, ) -> PedersenCommitment { @@ -296,11 +302,14 @@ impl TransferProofContextInfo { /// 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/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 {