From b296bab50dc25f0fa5180276af1a2051f772c02c Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Fri, 26 Jul 2024 19:04:10 +0900 Subject: [PATCH 01/16] add support for reading proofs from record program --- Cargo.lock | 1 + token/program-2022/Cargo.toml | 1 + .../confidential_transfer/instruction.rs | 109 ++++++++++++++---- .../confidential_transfer/verify_proof.rs | 42 ++++--- .../confidential_transfer_fee/instruction.rs | 60 +++++++--- .../confidential_transfer_fee/processor.rs | 27 ++--- token/program-2022/src/lib.rs | 8 ++ token/program-2022/src/proof.rs | 56 +++++++-- 8 files changed, 230 insertions(+), 74 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4398205ad89..4351981064d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7571,6 +7571,7 @@ dependencies = [ "solana-zk-token-sdk", "spl-memo 5.0.0", "spl-pod 0.3.0", + "spl-record", "spl-tlv-account-resolution 0.7.0", "spl-token 6.0.0", "spl-token-group-interface 0.3.0", diff --git a/token/program-2022/Cargo.toml b/token/program-2022/Cargo.toml index 18e3f95531b..dd911d216fe 100644 --- a/token/program-2022/Cargo.toml +++ b/token/program-2022/Cargo.toml @@ -26,6 +26,7 @@ solana-program = "2.0.3" solana-security-txt = "1.1.1" solana-zk-token-sdk = "2.0.3" spl-memo = { version = "5.0", path = "../../memo/program", features = [ "no-entrypoint" ] } +spl-record = { version = "0.2.0", path = "../../record/program", features = [ "no-entrypoint" ]} spl-token = { version = "6.0", path = "../program", features = ["no-entrypoint"] } spl-token-group-interface = { version = "0.3.0", path = "../../token-group/interface" } spl-token-metadata-interface = { version = "0.4.0", path = "../../token-metadata/interface" } diff --git a/token/program-2022/src/extension/confidential_transfer/instruction.rs b/token/program-2022/src/extension/confidential_transfer/instruction.rs index ac73574f5a9..4b6dfa61f73 100644 --- a/token/program-2022/src/extension/confidential_transfer/instruction.rs +++ b/token/program-2022/src/extension/confidential_transfer/instruction.rs @@ -13,7 +13,7 @@ use { check_program_account, extension::confidential_transfer::{ciphertext_extraction::SourceDecryptHandles, *}, instruction::{encode_instruction, TokenInstruction}, - proof::ProofLocation, + proof::{ProofData, ProofLocation}, }, bytemuck::Zeroable, // `Pod` comes from zk_token_proof_instruction num_enum::{IntoPrimitive, TryFromPrimitive}, @@ -90,7 +90,9 @@ pub enum ConfidentialTransferInstruction { /// in the same transaction or context state account if /// `VerifyPubkeyValidityProof` is pre-verified into a context state /// account. - /// 3. `[signer]` The single source account owner. + /// 3. `[]` (Optional) Record account if the accompanying proof is to be + /// read from a record account. + /// 4. `[signer]` The single source account owner. /// /// * Multisignature owner/delegate /// 0. `[writeable]` The SPL Token account. @@ -99,8 +101,10 @@ pub enum ConfidentialTransferInstruction { /// in the same transaction or context state account if /// `VerifyPubkeyValidityProof` is pre-verified into a context state /// account. - /// 3. `[]` The multisig source account owner. - /// 4.. `[signer]` Required M signer accounts for the SPL Token Multisig + /// 3. `[]` (Optional) Record account if the accompanying proof is to be + /// read from a record account. + /// 4. `[]` The multisig source account owner. + /// 5.. `[signer]` Required M signer accounts for the SPL Token Multisig /// account. /// /// Data expected by this instruction: @@ -151,7 +155,9 @@ pub enum ConfidentialTransferInstruction { /// the same transaction or context state account if /// `VerifyZeroBalanceProof` is pre-verified into a context state /// account. - /// 2. `[signer]` The single account owner. + /// 2. `[]` (Optional) Record account if the accompanying proof is to be + /// read from a record account. + /// 3. `[signer]` The single account owner. /// /// * Multisignature owner/delegate /// 0. `[writable]` The SPL Token account. @@ -159,8 +165,10 @@ pub enum ConfidentialTransferInstruction { /// the same transaction or context state account if /// `VerifyZeroBalanceProof` is pre-verified into a context state /// account. - /// 2. `[]` The multisig account owner. - /// 3.. `[signer]` Required M signer accounts for the SPL Token Multisig + /// 2. `[]` (Optional) Record account if the accompanying proof is to be + /// read from a record account. + /// 3. `[]` The multisig account owner. + /// 4.. `[signer]` Required M signer accounts for the SPL Token Multisig /// account. /// /// Data expected by this instruction: @@ -214,7 +222,9 @@ pub enum ConfidentialTransferInstruction { /// 2. `[]` Instructions sysvar if `VerifyWithdraw` is included in the /// same transaction or context state account if `VerifyWithdraw` is /// pre-verified into a context state account. - /// 3. `[signer]` The single source account owner. + /// 3. `[]` (Optional) Record account if the accompanying proof is to be + /// read from a record account. + /// 4. `[signer]` The single source account owner. /// /// * Multisignature owner/delegate /// 0. `[writable]` The SPL Token account. @@ -222,8 +232,10 @@ pub enum ConfidentialTransferInstruction { /// 2. `[]` Instructions sysvar if `VerifyWithdraw` is included in the /// same transaction or context state account if `VerifyWithdraw` is /// pre-verified into a context state account. - /// 3. `[]` The multisig source account owner. - /// 4.. `[signer]` Required M signer accounts for the SPL Token Multisig + /// 3. `[]` (Optional) Record account if the accompanying proof is to be + /// read from a record account. + /// 4. `[]` The multisig source account owner. + /// 5.. `[signer]` Required M signer accounts for the SPL Token Multisig /// account. /// /// Data expected by this instruction: @@ -248,7 +260,9 @@ pub enum ConfidentialTransferInstruction { /// `VerifyTransferWithFee` is included in the same transaction or /// context state account if these proofs are pre-verified into a /// context state account. - /// 5. `[signer]` The single source account owner. + /// 5. `[]` (Optional) Record account if the accompanying proof is to be + /// read from a record account. + /// 6. `[signer]` The single source account owner. /// /// * Multisignature owner/delegate /// 1. `[writable]` The source SPL Token account. @@ -258,8 +272,10 @@ pub enum ConfidentialTransferInstruction { /// `VerifyTransferWithFee` is included in the same transaction or /// context state account if these proofs are pre-verified into a /// context state account. - /// 5. `[]` The multisig source account owner. - /// 6.. `[signer]` Required M signer accounts for the SPL Token Multisig + /// 5. `[]` (Optional) Record account if the accompanying proof is to be + /// read from a record account. + /// 6. `[]` The multisig source account owner. + /// 7.. `[signer]` Required M signer accounts for the SPL Token Multisig /// account. /// /// Data expected by this instruction: @@ -715,8 +731,11 @@ pub fn inner_configure_account( ]; let proof_instruction_offset = match proof_data_location { - ProofLocation::InstructionOffset(proof_instruction_offset, _) => { + ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) => { accounts.push(AccountMeta::new_readonly(sysvar::instructions::id(), false)); + if let ProofData::RecordAccount(record_address, _) = proof_data { + accounts.push(AccountMeta::new_readonly(*record_address, false)); + } proof_instruction_offset.into() } ProofLocation::ContextStateAccount(context_state_account) => { @@ -781,8 +800,14 @@ pub fn configure_account( if proof_instruction_offset != 1 { return Err(TokenError::InvalidProofInstructionOffset.into()); } - instructions.push(verify_pubkey_validity(None, proof_data)); - }; + match proof_data { + ProofData::ProofData(data) => instructions.push(verify_pubkey_validity(None, data)), + ProofData::RecordAccount(address, offset) => instructions.push( + ProofInstruction::VerifyPubkeyValidity + .encode_verify_proof_from_account(None, address, offset), + ), + }; + } Ok(instructions) } @@ -827,8 +852,11 @@ pub fn inner_empty_account( let mut accounts = vec![AccountMeta::new(*token_account, false)]; let proof_instruction_offset = match proof_data_location { - ProofLocation::InstructionOffset(proof_instruction_offset, _) => { + ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) => { accounts.push(AccountMeta::new_readonly(sysvar::instructions::id(), false)); + if let ProofData::RecordAccount(record_address, _) = proof_data { + accounts.push(AccountMeta::new_readonly(*record_address, false)); + } proof_instruction_offset.into() } ProofLocation::ContextStateAccount(context_state_account) => { @@ -884,7 +912,13 @@ pub fn empty_account( if proof_instruction_offset != 1 { return Err(TokenError::InvalidProofInstructionOffset.into()); } - instructions.push(verify_zero_balance(None, proof_data)); + match proof_data { + ProofData::ProofData(data) => instructions.push(verify_zero_balance(None, data)), + ProofData::RecordAccount(address, offset) => instructions.push( + ProofInstruction::VerifyZeroBalance + .encode_verify_proof_from_account(None, address, offset), + ), + }; }; Ok(instructions) @@ -946,8 +980,11 @@ pub fn inner_withdraw( ]; let proof_instruction_offset = match proof_data_location { - ProofLocation::InstructionOffset(proof_instruction_offset, _) => { + ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) => { accounts.push(AccountMeta::new_readonly(sysvar::instructions::id(), false)); + if let ProofData::RecordAccount(record_address, _) = proof_data { + accounts.push(AccountMeta::new_readonly(*record_address, false)); + } proof_instruction_offset.into() } ProofLocation::ContextStateAccount(context_state_account) => { @@ -1015,7 +1052,13 @@ pub fn withdraw( if proof_instruction_offset != 1 { return Err(TokenError::InvalidProofInstructionOffset.into()); } - instructions.push(verify_withdraw(None, proof_data)); + match proof_data { + ProofData::ProofData(data) => instructions.push(verify_withdraw(None, data)), + ProofData::RecordAccount(address, offset) => instructions.push( + ProofInstruction::VerifyWithdraw + .encode_verify_proof_from_account(None, address, offset), + ), + }; }; Ok(instructions) @@ -1043,8 +1086,11 @@ pub fn inner_transfer( ]; let proof_instruction_offset = match proof_data_location { - ProofLocation::InstructionOffset(proof_instruction_offset, _) => { + ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) => { accounts.push(AccountMeta::new_readonly(sysvar::instructions::id(), false)); + if let ProofData::RecordAccount(record_address, _) = proof_data { + accounts.push(AccountMeta::new_readonly(*record_address, false)); + } proof_instruction_offset.into() } ProofLocation::ContextStateAccount(context_state_account) => { @@ -1108,7 +1154,13 @@ pub fn transfer( if proof_instruction_offset != 1 { return Err(TokenError::InvalidProofInstructionOffset.into()); } - instructions.push(verify_transfer(None, proof_data)); + match proof_data { + ProofData::ProofData(data) => instructions.push(verify_transfer(None, data)), + ProofData::RecordAccount(address, offset) => instructions.push( + ProofInstruction::VerifyTransfer + .encode_verify_proof_from_account(None, address, offset), + ), + }; }; Ok(instructions) @@ -1136,8 +1188,11 @@ pub fn inner_transfer_with_fee( ]; let proof_instruction_offset = match proof_data_location { - ProofLocation::InstructionOffset(proof_instruction_offset, _) => { + ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) => { accounts.push(AccountMeta::new_readonly(sysvar::instructions::id(), false)); + if let ProofData::RecordAccount(record_address, _) = proof_data { + accounts.push(AccountMeta::new_readonly(*record_address, false)); + } proof_instruction_offset.into() } ProofLocation::ContextStateAccount(context_state_account) => { @@ -1201,7 +1256,13 @@ pub fn transfer_with_fee( if proof_instruction_offset != 1 { return Err(TokenError::InvalidProofInstructionOffset.into()); } - instructions.push(verify_transfer_with_fee(None, proof_data)); + match proof_data { + ProofData::ProofData(data) => instructions.push(verify_transfer_with_fee(None, data)), + ProofData::RecordAccount(address, offset) => instructions.push( + ProofInstruction::VerifyTransferWithFee + .encode_verify_proof_from_account(None, address, offset), + ), + }; }; Ok(instructions) 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 13c6001ba7b..dc234d65bff 100644 --- a/token/program-2022/src/extension/confidential_transfer/verify_proof.rs +++ b/token/program-2022/src/extension/confidential_transfer/verify_proof.rs @@ -43,11 +43,13 @@ pub fn verify_configure_account_proof( 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::< + Ok(decode_proof_instruction_context::< PubkeyValidityData, PubkeyValidityProofContext, >( - ProofInstruction::VerifyPubkeyValidity, &zkp_instruction + account_info_iter, + ProofInstruction::VerifyPubkeyValidity, + &zkp_instruction, )?) } } @@ -77,11 +79,13 @@ pub fn verify_empty_account_proof( 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::< + Ok(decode_proof_instruction_context::< ZeroBalanceProofData, ZeroBalanceProofContext, >( - ProofInstruction::VerifyZeroBalance, &zkp_instruction + account_info_iter, + ProofInstruction::VerifyZeroBalance, + &zkp_instruction, )?) } } @@ -110,11 +114,13 @@ pub fn verify_withdraw_proof( 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::< + Ok(decode_proof_instruction_context::< WithdrawData, WithdrawProofContext, >( - ProofInstruction::VerifyWithdraw, &zkp_instruction + account_info_iter, + ProofInstruction::VerifyWithdraw, + &zkp_instruction, )?) } } @@ -259,11 +265,13 @@ pub fn verify_transfer_proof( let sysvar_account_info = next_account_info(account_info_iter)?; let zkp_instruction = get_instruction_relative(proof_instruction_offset, sysvar_account_info)?; - let proof_context = (*decode_proof_instruction_context::< - TransferData, - TransferProofContext, - >(ProofInstruction::VerifyTransfer, &zkp_instruction)?) - .into(); + let proof_context = + (decode_proof_instruction_context::( + account_info_iter, + ProofInstruction::VerifyTransfer, + &zkp_instruction, + )?) + .into(); Ok(Some(proof_context)) } @@ -490,10 +498,12 @@ pub fn verify_transfer_with_fee_proof( let sysvar_account_info = next_account_info(account_info_iter)?; let zkp_instruction = get_instruction_relative(proof_instruction_offset, sysvar_account_info)?; - let proof_context = decode_proof_instruction_context::< - TransferWithFeeData, - TransferWithFeeProofContext, - >(ProofInstruction::VerifyTransferWithFee, &zkp_instruction)?; + let proof_context = + decode_proof_instruction_context::( + account_info_iter, + ProofInstruction::VerifyTransferWithFee, + &zkp_instruction, + )?; let proof_tranfer_fee_basis_points: u16 = proof_context.fee_parameters.fee_rate_basis_points.into(); @@ -509,7 +519,7 @@ pub fn verify_transfer_with_fee_proof( return Err(TokenError::FeeParametersMismatch.into()); } - Ok(Some((*proof_context).into())) + Ok(Some(proof_context.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 12a708624cb..e6e33ada34a 100644 --- a/token/program-2022/src/extension/confidential_transfer_fee/instruction.rs +++ b/token/program-2022/src/extension/confidential_transfer_fee/instruction.rs @@ -14,8 +14,10 @@ use { DecryptableBalance, }, instruction::{encode_instruction, TokenInstruction}, - proof::ProofLocation, - solana_zk_token_sdk::zk_token_elgamal::pod::ElGamalPubkey, + proof::{ProofData, ProofLocation}, + solana_zk_token_sdk::{ + zk_token_elgamal::pod::ElGamalPubkey, zk_token_proof_instruction::ProofInstruction, + }, }, bytemuck::{Pod, Zeroable}, num_enum::{IntoPrimitive, TryFromPrimitive}, @@ -75,7 +77,9 @@ pub enum ConfidentialTransferFeeInstruction { /// included in the same transaction or context state account if /// `VerifyCiphertextCiphertextEquality` is pre-verified into a context /// state account. - /// 3. `[signer]` The mint's `withdraw_withheld_authority`. + /// 3. `[]` (Optional) Record account if the accompanying proof is to be + /// read from a record account. + /// 4. `[signer]` The mint's `withdraw_withheld_authority`. /// /// * Multisignature owner/delegate /// 0. `[writable]` The token mint. Must include the `TransferFeeConfig` @@ -86,8 +90,10 @@ pub enum ConfidentialTransferFeeInstruction { /// included in the same transaction or context state account if /// `VerifyCiphertextCiphertextEquality` is pre-verified into a context /// state account. - /// 3. `[]` The mint's multisig `withdraw_withheld_authority`. - /// 4. ..3+M `[signer]` M signer accounts. + /// 3. `[]` (Optional) Record account if the accompanying proof is to be + /// read from a record account. + /// 4. `[]` The mint's multisig `withdraw_withheld_authority`. + /// 5. ..3+M `[signer]` M signer accounts. /// /// Data expected by this instruction: /// WithdrawWithheldTokensFromMintData @@ -136,8 +142,10 @@ pub enum ConfidentialTransferFeeInstruction { /// included in the same transaction or context state account if /// `VerifyCiphertextCiphertextEquality` is pre-verified into a context /// state account. - /// 3. `[signer]` The mint's `withdraw_withheld_authority`. - /// 4. ..3+N `[writable]` The source accounts to withdraw from. + /// 3. `[]` (Optional) Record account if the accompanying proof is to be + /// read from a record account. + /// 4. `[signer]` The mint's `withdraw_withheld_authority`. + /// 5. ..3+N `[writable]` The source accounts to withdraw from. /// /// * Multisignature owner/delegate /// 0. `[]` The token mint. Must include the `TransferFeeConfig` @@ -148,9 +156,11 @@ pub enum ConfidentialTransferFeeInstruction { /// included in the same transaction or context state account if /// `VerifyCiphertextCiphertextEquality` is pre-verified into a context /// state account. - /// 3. `[]` The mint's multisig `withdraw_withheld_authority`. - /// 4. ..4+M `[signer]` M signer accounts. - /// 4+M+1. ..4+M+N `[writable]` The source accounts to withdraw from. + /// 3. `[]` (Optional) Record account if the accompanying proof is to be + /// read from a record account. + /// 4. `[]` The mint's multisig `withdraw_withheld_authority`. + /// 5. ..5+M `[signer]` M signer accounts. + /// 5+M+1. ..5+M+N `[writable]` The source accounts to withdraw from. /// /// Data expected by this instruction: /// WithdrawWithheldTokensFromAccountsData @@ -303,8 +313,11 @@ pub fn inner_withdraw_withheld_tokens_from_mint( ]; let proof_instruction_offset = match proof_data_location { - ProofLocation::InstructionOffset(proof_instruction_offset, _) => { + ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) => { accounts.push(AccountMeta::new_readonly(sysvar::instructions::id(), false)); + if let ProofData::RecordAccount(record_address, _) = proof_data { + accounts.push(AccountMeta::new_readonly(*record_address, false)); + } proof_instruction_offset.into() } ProofLocation::ContextStateAccount(context_state_account) => { @@ -366,7 +379,15 @@ pub fn withdraw_withheld_tokens_from_mint( if proof_instruction_offset != 1 { return Err(TokenError::InvalidProofInstructionOffset.into()); } - instructions.push(verify_ciphertext_ciphertext_equality(None, proof_data)); + match proof_data { + ProofData::ProofData(data) => { + instructions.push(verify_ciphertext_ciphertext_equality(None, data)) + } + ProofData::RecordAccount(address, offset) => instructions.push( + ProofInstruction::VerifyCiphertextCiphertextEquality + .encode_verify_proof_from_account(None, address, offset), + ), + }; }; Ok(instructions) @@ -395,8 +416,11 @@ pub fn inner_withdraw_withheld_tokens_from_accounts( ]; let proof_instruction_offset = match proof_data_location { - ProofLocation::InstructionOffset(proof_instruction_offset, _) => { + ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) => { accounts.push(AccountMeta::new_readonly(sysvar::instructions::id(), false)); + if let ProofData::RecordAccount(record_address, _) = proof_data { + accounts.push(AccountMeta::new_readonly(*record_address, false)); + } proof_instruction_offset.into() } ProofLocation::ContextStateAccount(context_state_account) => { @@ -466,7 +490,15 @@ pub fn withdraw_withheld_tokens_from_accounts( if proof_instruction_offset != 1 { return Err(TokenError::InvalidProofInstructionOffset.into()); } - instructions.push(verify_ciphertext_ciphertext_equality(None, proof_data)); + match proof_data { + ProofData::ProofData(data) => { + instructions.push(verify_ciphertext_ciphertext_equality(None, data)) + } + ProofData::RecordAccount(address, offset) => instructions.push( + ProofInstruction::VerifyCiphertextCiphertextEquality + .encode_verify_proof_from_account(None, address, offset), + ), + }; }; Ok(instructions) diff --git a/token/program-2022/src/extension/confidential_transfer_fee/processor.rs b/token/program-2022/src/extension/confidential_transfer_fee/processor.rs index 4fa079ec0b4..26ee3a9c235 100644 --- a/token/program-2022/src/extension/confidential_transfer_fee/processor.rs +++ b/token/program-2022/src/extension/confidential_transfer_fee/processor.rs @@ -42,6 +42,7 @@ use { sysvar::instructions::get_instruction_relative, }, spl_pod::{bytemuck::pod_from_bytes, optional_keys::OptionalNonZeroPubkey}, + std::slice::Iter, }; /// Processes an [InitializeConfidentialTransferFeeConfig] instruction. @@ -79,10 +80,8 @@ fn process_withdraw_withheld_tokens_from_mint( // zero-knowledge proof certifies that the exact withheld amount is credited to // the destination account. - let proof_context = verify_ciphertext_ciphertext_equality_proof( - next_account_info(account_info_iter)?, - proof_instruction_offset, - )?; + let proof_context = + verify_ciphertext_ciphertext_equality_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(); @@ -175,13 +174,14 @@ fn process_withdraw_withheld_tokens_from_mint( /// instruction or a `[WithdrawWithheldTokensFromAccounts]` and return the /// corresponding proof context. fn verify_ciphertext_ciphertext_equality_proof( - account_info: &AccountInfo<'_>, + account_info_iter: &mut Iter<'_, AccountInfo<'_>>, proof_instruction_offset: i64, ) -> Result { if proof_instruction_offset == 0 { + let context_account_info = next_account_info(account_info_iter)?; // 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(); + check_zk_token_proof_program_account(context_account_info.owner)?; + let context_state_account_data = context_account_info.data.borrow(); let context_state = pod_from_bytes::< ProofContextState, >(&context_state_account_data)?; @@ -192,12 +192,15 @@ fn verify_ciphertext_ciphertext_equality_proof( Ok(context_state.proof_context) } else { + let sysvar_account_info = next_account_info(account_info_iter)?; // interpret `account_info` as a sysvar - let zkp_instruction = get_instruction_relative(proof_instruction_offset, account_info)?; - Ok(*decode_proof_instruction_context::< + let zkp_instruction = + get_instruction_relative(proof_instruction_offset, sysvar_account_info)?; + Ok(decode_proof_instruction_context::< CiphertextCiphertextEqualityProofData, CiphertextCiphertextEqualityProofContext, >( + account_info_iter, ProofInstruction::VerifyCiphertextCiphertextEquality, &zkp_instruction, )?) @@ -219,10 +222,8 @@ fn process_withdraw_withheld_tokens_from_accounts( // zero-knowledge proof certifies that the exact aggregate withheld amount is // credited to the destination account. - let proof_context = verify_ciphertext_ciphertext_equality_proof( - next_account_info(account_info_iter)?, - proof_instruction_offset, - )?; + let proof_context = + verify_ciphertext_ciphertext_equality_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(); diff --git a/token/program-2022/src/lib.rs b/token/program-2022/src/lib.rs index 7561c6b5d8e..3830c18f603 100644 --- a/token/program-2022/src/lib.rs +++ b/token/program-2022/src/lib.rs @@ -121,6 +121,14 @@ pub fn check_zk_token_proof_program_account(zk_token_proof_program_id: &Pubkey) Ok(()) } +/// Checks that the supplied program ID is correct for the spl record program +pub fn check_spl_record_program_account(spl_record_program_id: &Pubkey) -> ProgramResult { + if spl_record_program_id != &spl_record::id() { + return Err(ProgramError::IncorrectProgramId); + } + Ok(()) +} + /// Checks if the spplied program ID is that of the system program pub fn check_system_program_account(system_program_id: &Pubkey) -> ProgramResult { if system_program_id != &system_program::id() { diff --git a/token/program-2022/src/proof.rs b/token/program-2022/src/proof.rs index fbb9b525a10..2ea006eaf3c 100644 --- a/token/program-2022/src/proof.rs +++ b/token/program-2022/src/proof.rs @@ -1,21 +1,33 @@ //! Helper for processing instruction data from ZK Token proof program use { + crate::check_spl_record_program_account, bytemuck::Pod, - solana_program::{instruction::Instruction, msg, program_error::ProgramError, pubkey::Pubkey}, + solana_program::{ + account_info::{next_account_info, AccountInfo}, + instruction::Instruction, + msg, + program_error::ProgramError, + pubkey::Pubkey, + }, solana_zk_token_sdk::{ instruction::ZkProofData, zk_token_proof_instruction::ProofInstruction, zk_token_proof_program, }, - std::num::NonZeroI8, + std::{num::NonZeroI8, slice::Iter}, }; +/// If a proof is to be read from a record account, the proof instruction data +/// must be 5 bytes: 1 byte for the proof type and 4 bytes for the u32 offset +const INSTRUCTION_DATA_LENGTH_WITH_RECORD_ACCOUNT: usize = 5; + /// Decodes the proof context data associated with a zero-knowledge proof /// instruction. pub fn decode_proof_instruction_context, U: Pod>( + account_info_iter: &mut Iter<'_, AccountInfo<'_>>, expected: ProofInstruction, instruction: &Instruction, -) -> Result<&U, ProgramError> { +) -> Result { if instruction.program_id != zk_token_proof_program::id() || ProofInstruction::instruction_type(&instruction.data) != Some(expected) { @@ -23,9 +35,28 @@ pub fn decode_proof_instruction_context, U: Pod>( return Err(ProgramError::InvalidInstructionData); } - ProofInstruction::proof_data::(&instruction.data) - .map(ZkProofData::context_data) - .ok_or(ProgramError::InvalidInstructionData) + // If the instruction data size is exactly 5 bytes, then interpret it as an + // offset byte for a record account. This behavior is identical to that of + // the ZK ElGamal proof program. + if instruction.data.len() == INSTRUCTION_DATA_LENGTH_WITH_RECORD_ACCOUNT { + let record_account = next_account_info(account_info_iter)?; + check_spl_record_program_account(record_account.owner)?; + + // first byte is the proof type + let start_offset = u32::from_le_bytes(instruction.data[1..].try_into().unwrap()) as usize; + let end_offset = start_offset + .checked_add(std::mem::size_of::()) + .ok_or(ProgramError::InvalidAccountData)?; + let raw_proof_data = &record_account.data.borrow()[start_offset..end_offset]; + + bytemuck::try_from_bytes::(raw_proof_data) + .map(|proof_data| *ZkProofData::context_data(proof_data)) + .map_err(|_| ProgramError::InvalidAccountData) + } else { + ProofInstruction::proof_data::(&instruction.data) + .map(|proof_data| *ZkProofData::context_data(proof_data)) + .ok_or(ProgramError::InvalidInstructionData) + } } /// A proof location type meant to be used for arguments to instruction @@ -34,11 +65,22 @@ pub fn decode_proof_instruction_context, U: Pod>( pub enum ProofLocation<'a, T> { /// The proof is included in the same transaction of a corresponding /// token-2022 instruction. - InstructionOffset(NonZeroI8, &'a T), + InstructionOffset(NonZeroI8, ProofData<'a, T>), /// The proof is pre-verified into a context state account. ContextStateAccount(&'a Pubkey), } +/// A proof data type to distinguish between proof data included as part of +/// instruction data and proof data stored in a record account. +#[derive(Clone, Copy)] +pub enum ProofData<'a, T> { + /// The proof data + ProofData(&'a T), + /// The address of a record account containing the proof data and its byte + /// offset + RecordAccount(&'a Pubkey, u32), +} + /// Instruction options for when using split context state accounts #[derive(Clone, Copy)] pub struct SplitContextStateAccountsConfig { From 24b6b7ff581fcd316abacc39166f1a50907b15d8 Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Fri, 26 Jul 2024 19:04:50 +0900 Subject: [PATCH 02/16] add support for record program in the client --- Cargo.lock | 3 + token/client/Cargo.toml | 3 + token/client/src/proof_generation.rs | 6 + token/client/src/temp_client.rs | 130 ++++++++++++ token/client/src/token.rs | 292 +++++++++++++++++++++++---- 5 files changed, 397 insertions(+), 37 deletions(-) create mode 100644 token/client/src/temp_client.rs diff --git a/Cargo.lock b/Cargo.lock index 4351981064d..82ab488a34c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7649,6 +7649,8 @@ name = "spl-token-client" version = "0.11.0" dependencies = [ "async-trait", + "bincode", + "bytemuck", "curve25519-dalek", "futures 0.3.30", "futures-util", @@ -7660,6 +7662,7 @@ dependencies = [ "solana-sdk", "spl-associated-token-account 4.0.0", "spl-memo 5.0.0", + "spl-record", "spl-token 6.0.0", "spl-token-2022 4.0.1", "spl-token-group-interface 0.3.0", diff --git a/token/client/Cargo.toml b/token/client/Cargo.toml index 762b2f3e4cb..0431fcc0d82 100644 --- a/token/client/Cargo.toml +++ b/token/client/Cargo.toml @@ -9,6 +9,8 @@ version = "0.11.0" [dependencies] async-trait = "0.1" +bincode = "1.3.2" +bytemuck = "1.16.1" curve25519-dalek = "3.2.1" futures = "0.3.30" futures-util = "0.3" @@ -26,6 +28,7 @@ spl-associated-token-account = { version = "4.0.0", path = "../../associated-tok spl-memo = { version = "5.0", path = "../../memo/program", features = [ "no-entrypoint", ] } +spl-record = { version = "0.2.0", path = "../../record/program", features = ["no-entrypoint"] } spl-token = { version = "6.0", path = "../program", features = [ "no-entrypoint", ] } diff --git a/token/client/src/proof_generation.rs b/token/client/src/proof_generation.rs index 3c41d5094eb..b7582945e88 100644 --- a/token/client/src/proof_generation.rs +++ b/token/client/src/proof_generation.rs @@ -6,6 +6,7 @@ use { curve25519_dalek::scalar::Scalar, + solana_sdk::pubkey::Pubkey, spl_token_2022::{ error::TokenError, extension::confidential_transfer::{ @@ -32,6 +33,11 @@ use { }, }; +pub enum ProofAccount { + ContextAccount(Pubkey), + RecordAccount(Pubkey), +} + /// The main logic to create the five split proof data for a transfer with fee. #[allow(clippy::too_many_arguments)] pub fn transfer_with_fee_split_proof_data( diff --git a/token/client/src/temp_client.rs b/token/client/src/temp_client.rs new file mode 100644 index 00000000000..c6c69b0b038 --- /dev/null +++ b/token/client/src/temp_client.rs @@ -0,0 +1,130 @@ +/// Basic trait for sending transactions to validators +pub trait SendTransaction { + type Output; +} + +/// Basic trait for simulating transactions in a validator +pub trait SimulateTransaction { + type SimulationOutput: SimulationResult; +} + +/// Trait for the output of a simulation +pub trait SimulationResult { + fn get_compute_units_consumed(&self) -> ProgramClientResult; +} + +/// Extend basic `SendTransaction` trait with function `send` where client is +/// `&mut BankClient`. Required for `ProgramBanksClient`. +pub trait SendTransactionBanksClient: SendTransaction { + fn send<'a>( + &self, + client: &'a mut BanksClient, + transaction: Transaction, + ) -> BoxFuture<'a, ProgramClientResult>; +} + +/// Extends basic `SimulateTransaction` trait with function `simulation` where +/// client is `&mut BanksClient`. Required for `ProgramBanksClient`. +pub trait SimulateTransactionBanksClient: SimulateTransaction { + fn simulate<'a>( + &self, + client: &'a mut BanksClient, + transaction: Transaction, + ) -> BoxFuture<'a, ProgramClientResult>; +} + +/// Send transaction to validator using `BanksClient::process_transaction`. +#[derive(Debug, Clone, Copy, Default)] +pub struct ProgramBanksClientProcessTransaction; + +impl SendTransaction for ProgramBanksClientProcessTransaction { + type Output = (); +} + +impl SendTransactionBanksClient for ProgramBanksClientProcessTransaction { + fn send<'a>( + &self, + client: &'a mut BanksClient, + transaction: Transaction, + ) -> BoxFuture<'a, ProgramClientResult> { + Box::pin(async move { + client + .process_transaction(transaction) + .await + .map_err(Into::into) + }) + } +} + +pub type ProgramClientError = Box; +pub type ProgramClientResult = Result; + +/// Generic client interface for programs. +#[async_trait] +pub trait ProgramClient +where + ST: SendTransaction + SimulateTransaction, +{ + async fn get_minimum_balance_for_rent_exemption( + &self, + data_len: usize, + ) -> ProgramClientResult; + + async fn get_latest_blockhash(&self) -> ProgramClientResult; + + async fn send_transaction(&self, transaction: &Transaction) -> ProgramClientResult; + + async fn get_account(&self, address: Pubkey) -> ProgramClientResult>; + + async fn simulate_transaction( + &self, + transaction: &Transaction, + ) -> ProgramClientResult; +} + +enum ProgramBanksClientContext { + Client(Arc>), + Context(Arc>), +} + +/// Program client for `BanksClient` from crate `solana-program-test`. +pub struct ProgramBanksClient { + context: ProgramBanksClientContext, + send: ST, +} + +impl fmt::Debug for ProgramBanksClient { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ProgramBanksClient").finish() + } +} + +impl ProgramBanksClient { + fn new(context: ProgramBanksClientContext, send: ST) -> Self { + Self { context, send } + } + + pub fn new_from_client(client: Arc>, send: ST) -> Self { + Self::new(ProgramBanksClientContext::Client(client), send) + } + + pub fn new_from_context(context: Arc>, send: ST) -> Self { + Self::new(ProgramBanksClientContext::Context(context), send) + } + + async fn run_in_lock(&self, f: F) -> O + where + for<'a> F: Fn(&'a mut BanksClient) -> BoxFuture<'a, O>, + { + match &self.context { + ProgramBanksClientContext::Client(client) => { + let mut lock = client.lock().await; + f(&mut lock).await + } + ProgramBanksClientContext::Context(context) => { + let mut lock = context.lock().await; + f(&mut lock.banks_client).await + } + } + } +} diff --git a/token/client/src/token.rs b/token/client/src/token.rs index a9fefc21f70..0b9db16b6ac 100644 --- a/token/client/src/token.rs +++ b/token/client/src/token.rs @@ -4,8 +4,9 @@ use { ProgramClient, ProgramClientError, SendTransaction, SimulateTransaction, SimulationResult, }, - proof_generation::transfer_with_fee_split_proof_data, + proof_generation::{transfer_with_fee_split_proof_data, ProofAccount}, }, + bytemuck::bytes_of, futures::{future::join_all, try_join}, futures_util::TryFutureExt, solana_program_test::tokio::time, @@ -15,9 +16,11 @@ use { hash::Hash, instruction::{AccountMeta, Instruction}, message::Message, + packet::PACKET_DATA_SIZE, program_error::ProgramError, program_pack::Pack, pubkey::Pubkey, + signature::Signature, signer::{signers::Signers, Signer, SignerError}, system_instruction, transaction::Transaction, @@ -28,6 +31,7 @@ use { create_associated_token_account, create_associated_token_account_idempotent, }, }, + spl_record::state::RecordData, spl_token_2022::{ extension::{ confidential_transfer::{ @@ -51,7 +55,7 @@ use { BaseStateWithExtensions, Extension, ExtensionType, StateWithExtensionsOwned, }, instruction, offchain, - proof::ProofLocation, + proof::{ProofData, ProofLocation}, solana_zk_token_sdk::{ encryption::{ auth_encryption::AeKey, @@ -1892,7 +1896,7 @@ where &self, account: &Pubkey, authority: &Pubkey, - context_state_account: Option<&Pubkey>, + proof_account: Option<&ProofAccount>, maximum_pending_balance_credit_counter: Option, elgamal_keypair: &ElGamalKeypair, aes_key: &AeKey, @@ -1906,7 +1910,7 @@ where let maximum_pending_balance_credit_counter = maximum_pending_balance_credit_counter .unwrap_or(DEFAULT_MAXIMUM_PENDING_BALANCE_CREDIT_COUNTER); - let proof_data = if context_state_account.is_some() { + let proof_data = if proof_account.is_some() { None } else { Some( @@ -1916,10 +1920,23 @@ where }; let proof_location = if let Some(proof_data_temp) = proof_data.as_ref() { - ProofLocation::InstructionOffset(1.try_into().unwrap(), proof_data_temp) + ProofLocation::InstructionOffset( + 1.try_into().unwrap(), + ProofData::ProofData(proof_data_temp), + ) } else { - let context_state_account = context_state_account.unwrap(); - ProofLocation::ContextStateAccount(context_state_account) + match proof_account.unwrap() { + ProofAccount::ContextAccount(context_state_account) => { + ProofLocation::ContextStateAccount(context_state_account) + } + ProofAccount::RecordAccount(record_account) => ProofLocation::InstructionOffset( + 1.try_into().unwrap(), + ProofData::RecordAccount( + record_account, + RecordData::WRITABLE_START_INDEX as u32, + ), + ), + } }; let decryptable_balance = aes_key.encrypt(0); @@ -1969,7 +1986,7 @@ where &self, account: &Pubkey, authority: &Pubkey, - context_state_account: Option<&Pubkey>, + proof_account: Option<&ProofAccount>, account_info: Option, elgamal_keypair: &ElGamalKeypair, signing_keypairs: &S, @@ -1986,7 +2003,7 @@ where EmptyAccountAccountInfo::new(confidential_transfer_account) }; - let proof_data = if context_state_account.is_some() { + let proof_data = if proof_account.is_some() { None } else { Some( @@ -1997,10 +2014,23 @@ where }; let proof_location = if let Some(proof_data_temp) = proof_data.as_ref() { - ProofLocation::InstructionOffset(1.try_into().unwrap(), proof_data_temp) + ProofLocation::InstructionOffset( + 1.try_into().unwrap(), + ProofData::ProofData(proof_data_temp), + ) } else { - let context_state_account = context_state_account.unwrap(); - ProofLocation::ContextStateAccount(context_state_account) + match proof_account.unwrap() { + ProofAccount::ContextAccount(context_state_account) => { + ProofLocation::ContextStateAccount(context_state_account) + } + ProofAccount::RecordAccount(record_account) => ProofLocation::InstructionOffset( + 1.try_into().unwrap(), + ProofData::RecordAccount( + record_account, + RecordData::WRITABLE_START_INDEX as u32, + ), + ), + } }; self.process_ixs( @@ -2051,7 +2081,7 @@ where &self, account: &Pubkey, authority: &Pubkey, - context_state_account: Option<&Pubkey>, + proof_account: Option<&ProofAccount>, withdraw_amount: u64, decimals: u8, account_info: Option, @@ -2071,7 +2101,7 @@ where WithdrawAccountInfo::new(confidential_transfer_account) }; - let proof_data = if context_state_account.is_some() { + let proof_data = if proof_account.is_some() { None } else { Some( @@ -2082,10 +2112,23 @@ where }; let proof_location = if let Some(proof_data_temp) = proof_data.as_ref() { - ProofLocation::InstructionOffset(1.try_into().unwrap(), proof_data_temp) + ProofLocation::InstructionOffset( + 1.try_into().unwrap(), + ProofData::ProofData(proof_data_temp), + ) } else { - let context_state_account = context_state_account.unwrap(); - ProofLocation::ContextStateAccount(context_state_account) + match proof_account.unwrap() { + ProofAccount::ContextAccount(context_state_account) => { + ProofLocation::ContextStateAccount(context_state_account) + } + ProofAccount::RecordAccount(record_account) => ProofLocation::InstructionOffset( + 1.try_into().unwrap(), + ProofData::RecordAccount( + record_account, + RecordData::WRITABLE_START_INDEX as u32, + ), + ), + } }; let new_decryptable_available_balance = account_info @@ -2173,7 +2216,7 @@ where source_account: &Pubkey, destination_account: &Pubkey, source_authority: &Pubkey, - context_state_account: Option<&Pubkey>, + proof_account: Option<&ProofAccount>, transfer_amount: u64, account_info: Option, source_elgamal_keypair: &ElGamalKeypair, @@ -2194,7 +2237,7 @@ where TransferAccountInfo::new(confidential_transfer_account) }; - let proof_data = if context_state_account.is_some() { + let proof_data = if proof_account.is_some() { None } else { Some( @@ -2211,10 +2254,23 @@ where }; let proof_location = if let Some(proof_data_temp) = proof_data.as_ref() { - ProofLocation::InstructionOffset(1.try_into().unwrap(), proof_data_temp) + ProofLocation::InstructionOffset( + 1.try_into().unwrap(), + ProofData::ProofData(proof_data_temp), + ) } else { - let context_state_account = context_state_account.unwrap(); - ProofLocation::ContextStateAccount(context_state_account) + match proof_account.unwrap() { + ProofAccount::ContextAccount(context_state_account) => { + ProofLocation::ContextStateAccount(context_state_account) + } + ProofAccount::RecordAccount(record_account) => ProofLocation::InstructionOffset( + 1.try_into().unwrap(), + ProofData::RecordAccount( + record_account, + RecordData::WRITABLE_START_INDEX as u32, + ), + ), + } }; let new_decryptable_available_balance = account_info @@ -2405,6 +2461,110 @@ where ) } + /// Create a record account containing zero-knowledge proof needed for a + /// confidential transfer. + pub async fn confidential_transfer_create_record_account< + S: Signer, + ZK: Pod + ZkProofData, + U: Pod, + >( + &self, + record_account: &Pubkey, + record_authority: &Pubkey, + proof_data: &ZK, + record_account_signer: &S, + record_authority_signer: &S, + ) -> TokenResult> { + let proof_data = bytes_of(proof_data); + let space = proof_data + .len() + .saturating_add(RecordData::WRITABLE_START_INDEX); + let rent = self + .client + .get_minimum_balance_for_rent_exemption(space) + .await + .map_err(TokenError::Client)?; + + // A closure that constructs a vector of instructions needed to create and write to record + // accounts. The closure is defined as a convenience function to be fed into the function + // `calculate_record_max_chunk_size`. + let create_record_instructions = |first_instruction: bool, bytes: &[u8], offset: u64| { + let mut ixs = vec![]; + if first_instruction { + ixs.push(system_instruction::create_account( + &self.payer.pubkey(), + record_account, + rent, + space as u64, + &spl_record::id(), + )); + ixs.push(spl_record::instruction::initialize( + record_account, + record_authority, + )); + } + ixs.push(spl_record::instruction::write( + record_account, + record_authority, + offset, + bytes, + )); + ixs + }; + let first_chunk_size = calculate_record_max_chunk_size(create_record_instructions, true); + let (first_chunk, rest) = if space <= first_chunk_size { + (proof_data, &[] as &[u8]) + } else { + proof_data.split_at(first_chunk_size) + }; + + let first_ixs = create_record_instructions(true, first_chunk, 0); + self.process_ixs( + &first_ixs, + &[record_account_signer, record_authority_signer], + ) + .await?; + + let subsequent_chunk_size = + calculate_record_max_chunk_size(create_record_instructions, false); + let mut record_offset = first_chunk_size; + let mut ixs_batch = vec![]; + for chunk in rest.chunks(subsequent_chunk_size) { + ixs_batch.push(create_record_instructions( + false, + chunk, + record_offset as u64, + )); + record_offset = record_offset.saturating_add(chunk.len()); + } + + let futures = ixs_batch + .into_iter() + .map(|ixs| async move { self.process_ixs(&ixs, &[record_authority_signer]).await }) + .collect::>(); + + join_all(futures).await.into_iter().collect() + } + + /// Close a record account. + pub async fn confidential_transfer_close_record_account( + &self, + record_account: &Pubkey, + record_authority: &Pubkey, + receiver: &Pubkey, + record_authority_signer: &S, + ) -> TokenResult { + self.process_ixs( + &[spl_record::instruction::close_account( + record_account, + record_authority, + receiver, + )], + &[record_authority_signer], + ) + .await + } + /// Create equality proof context state account for a confidential transfer. #[allow(clippy::too_many_arguments)] pub async fn create_equality_proof_context_state_for_transfer( @@ -2750,7 +2910,7 @@ where source_account: &Pubkey, destination_account: &Pubkey, source_authority: &Pubkey, - context_state_account: Option<&Pubkey>, + proof_account: Option<&ProofAccount>, transfer_amount: u64, account_info: Option, source_elgamal_keypair: &ElGamalKeypair, @@ -2774,7 +2934,7 @@ where TransferAccountInfo::new(confidential_transfer_account) }; - let proof_data = if context_state_account.is_some() { + let proof_data = if proof_account.is_some() { None } else { Some( @@ -2794,10 +2954,23 @@ where }; let proof_location = if let Some(proof_data_temp) = proof_data.as_ref() { - ProofLocation::InstructionOffset(1.try_into().unwrap(), proof_data_temp) + ProofLocation::InstructionOffset( + 1.try_into().unwrap(), + ProofData::ProofData(proof_data_temp), + ) } else { - let context_state_account = context_state_account.unwrap(); - ProofLocation::ContextStateAccount(context_state_account) + match proof_account.unwrap() { + ProofAccount::ContextAccount(context_state_account) => { + ProofLocation::ContextStateAccount(context_state_account) + } + ProofAccount::RecordAccount(record_account) => ProofLocation::InstructionOffset( + 1.try_into().unwrap(), + ProofData::RecordAccount( + record_account, + RecordData::WRITABLE_START_INDEX as u32, + ), + ), + } }; let new_decryptable_available_balance = account_info @@ -3493,7 +3666,7 @@ where &self, destination_account: &Pubkey, withdraw_withheld_authority: &Pubkey, - context_state_account: Option<&Pubkey>, + proof_account: Option<&ProofAccount>, withheld_tokens_info: Option, withdraw_withheld_authority_elgamal_keypair: &ElGamalKeypair, destination_elgamal_pubkey: &ElGamalPubkey, @@ -3513,7 +3686,7 @@ where WithheldTokensInfo::new(&confidential_transfer_fee_config.withheld_amount) }; - let proof_data = if context_state_account.is_some() { + let proof_data = if proof_account.is_some() { None } else { Some( @@ -3527,10 +3700,23 @@ where }; let proof_location = if let Some(proof_data_temp) = proof_data.as_ref() { - ProofLocation::InstructionOffset(1.try_into().unwrap(), proof_data_temp) + ProofLocation::InstructionOffset( + 1.try_into().unwrap(), + ProofData::ProofData(proof_data_temp), + ) } else { - let context_state_account = context_state_account.unwrap(); - ProofLocation::ContextStateAccount(context_state_account) + match proof_account.unwrap() { + ProofAccount::ContextAccount(context_state_account) => { + ProofLocation::ContextStateAccount(context_state_account) + } + ProofAccount::RecordAccount(record_account) => ProofLocation::InstructionOffset( + 1.try_into().unwrap(), + ProofData::RecordAccount( + record_account, + RecordData::WRITABLE_START_INDEX as u32, + ), + ), + } }; self.process_ixs( @@ -3554,7 +3740,7 @@ where &self, destination_account: &Pubkey, withdraw_withheld_authority: &Pubkey, - context_state_account: Option<&Pubkey>, + proof_account: Option<&ProofAccount>, withheld_tokens_info: Option, withdraw_withheld_authority_elgamal_keypair: &ElGamalKeypair, destination_elgamal_pubkey: &ElGamalPubkey, @@ -3585,7 +3771,7 @@ where WithheldTokensInfo::new(&aggregate_withheld_amount.into()) }; - let proof_data = if context_state_account.is_some() { + let proof_data = if proof_account.is_some() { None } else { Some( @@ -3599,10 +3785,23 @@ where }; let proof_location = if let Some(proof_data_temp) = proof_data.as_ref() { - ProofLocation::InstructionOffset(1.try_into().unwrap(), proof_data_temp) + ProofLocation::InstructionOffset( + 1.try_into().unwrap(), + ProofData::ProofData(proof_data_temp), + ) } else { - let context_state_account = context_state_account.unwrap(); - ProofLocation::ContextStateAccount(context_state_account) + match proof_account.unwrap() { + ProofAccount::ContextAccount(context_state_account) => { + ProofLocation::ContextStateAccount(context_state_account) + } + ProofAccount::RecordAccount(record_account) => ProofLocation::InstructionOffset( + 1.try_into().unwrap(), + ProofData::RecordAccount( + record_account, + RecordData::WRITABLE_START_INDEX as u32, + ), + ), + } }; self.process_ixs( @@ -4080,3 +4279,22 @@ where self.process_ixs(&instructions, signing_keypairs).await } } + +/// Calculates the maximum chunk size for a zero-knowledge proof record +/// instruction to fit inside a single transaction. +fn calculate_record_max_chunk_size( + create_record_instructions: F, + first_instruction: bool, +) -> usize +where + F: Fn(bool, &[u8], u64) -> Vec, +{ + let ixs = create_record_instructions(first_instruction, &[], 0); + let message = Message::new_with_blockhash(&ixs, Some(&Pubkey::default()), &Hash::default()); + let tx_size = bincode::serialized_size(&Transaction { + signatures: vec![Signature::default(); message.header.num_required_signatures as usize], + message, + }) + .unwrap() as usize; + PACKET_DATA_SIZE.saturating_sub(tx_size).saturating_sub(1) +} From 1a717704e8690a55a391958dbd7d070bb0fdb18e Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Fri, 26 Jul 2024 19:05:44 +0900 Subject: [PATCH 03/16] add record program tests --- Cargo.lock | 1 + token/program-2022-test/Cargo.toml | 3 + .../tests/confidential_transfer.rs | 241 +++++++++++- .../tests/confidential_transfer_fee.rs | 359 +++++++++++++++++- token/program-2022-test/tests/program_test.rs | 8 +- 5 files changed, 599 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 82ab488a34c..9c516c395a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7595,6 +7595,7 @@ dependencies = [ "spl-instruction-padding", "spl-memo 5.0.0", "spl-pod 0.3.0", + "spl-record", "spl-tlv-account-resolution 0.7.0", "spl-token-2022 4.0.1", "spl-token-client", diff --git a/token/program-2022-test/Cargo.toml b/token/program-2022-test/Cargo.toml index 6350ebd285a..bb9ec0caed8 100644 --- a/token/program-2022-test/Cargo.toml +++ b/token/program-2022-test/Cargo.toml @@ -27,6 +27,9 @@ spl-memo = { version = "5.0.0", path = "../../memo/program", features = [ "no-entrypoint", ] } spl-pod = { version = "0.3.0", path = "../../libraries/pod" } +spl-record = { version = "0.2.0", path = "../../record/program", features = [ + "no-entrypoint", +]} spl-token-2022 = { version = "4.0.0", path = "../program-2022", features = [ "no-entrypoint", ] } diff --git a/token/program-2022-test/tests/confidential_transfer.rs b/token/program-2022-test/tests/confidential_transfer.rs index bacc44c2848..b7386a8f3c4 100644 --- a/token/program-2022-test/tests/confidential_transfer.rs +++ b/token/program-2022-test/tests/confidential_transfer.rs @@ -20,7 +20,7 @@ use { extension::{ confidential_transfer::{ self, - account_info::TransferAccountInfo, + account_info::{EmptyAccountAccountInfo, TransferAccountInfo, WithdrawAccountInfo}, instruction::{ CloseSplitContextStateAccounts, TransferSplitContextStateAccounts, TransferWithFeeSplitContextStateAccounts, @@ -38,7 +38,7 @@ use { }, }, spl_token_client::{ - proof_generation::transfer_with_fee_split_proof_data, + proof_generation::{transfer_with_fee_split_proof_data, ProofAccount}, token::{ComputeUnitLimit, ExtensionInitializationParams, TokenError as TokenClientError}, }, std::{convert::TryInto, mem::size_of}, @@ -445,6 +445,83 @@ async fn confidential_transfer_empty_account() { .unwrap(); } +#[tokio::test] +async fn confidential_transfer_empty_account_with_record() { + 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; + + // newly created confidential transfer account should hold no balance and + // therefore, immediately closable + 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, .. } = context.token_context.unwrap(); + let alice_meta = ConfidentialTokenAccountMeta::new(&token, &alice, None, false, false).await; + + let state = token + .get_account_info(&alice_meta.token_account) + .await + .unwrap(); + let extension = state + .get_extension::() + .unwrap(); + let account_info = EmptyAccountAccountInfo::new(extension); + + let zero_balance_proof = account_info + .generate_proof_data(&alice_meta.elgamal_keypair) + .unwrap(); + + let record_account = Keypair::new(); + let record_account_authority = Keypair::new(); + + token + .confidential_transfer_create_record_account( + &record_account.pubkey(), + &record_account_authority.pubkey(), + &zero_balance_proof, + &record_account, + &record_account_authority, + ) + .await + .unwrap(); + + let proof_account = ProofAccount::RecordAccount(record_account.pubkey()); + + token + .confidential_transfer_empty_account( + &alice_meta.token_account, + &alice.pubkey(), + Some(&proof_account), + None, + &alice_meta.elgamal_keypair, + &[&alice], + ) + .await + .unwrap(); + + token + .confidential_transfer_close_record_account( + &record_account.pubkey(), + &record_account_authority.pubkey(), + &alice.pubkey(), + &record_account_authority, + ) + .await + .unwrap(); +} + #[cfg(feature = "zk-ops")] #[tokio::test] async fn confidential_transfer_deposit() { @@ -809,6 +886,134 @@ async fn confidential_transfer_withdraw() { .unwrap(); } +#[cfg(feature = "zk-ops")] +#[tokio::test] +async fn confidential_transfer_withdraw_with_record_account() { + 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, + 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 state = token + .get_account_info(&alice_meta.token_account) + .await + .unwrap(); + assert_eq!(state.base.amount, 0); + alice_meta + .check_balances( + &token, + ConfidentialTokenAccountBalances { + pending_balance_lo: 0, + pending_balance_hi: 0, + available_balance: 42, + decryptable_available_balance: 42, + }, + ) + .await; + + let state = token + .get_account_info(&alice_meta.token_account) + .await + .unwrap(); + let extension = state + .get_extension::() + .unwrap(); + let account_info = WithdrawAccountInfo::new(extension); + + let withdraw_proof = account_info + .generate_proof_data(42, &alice_meta.elgamal_keypair, &alice_meta.aes_key) + .unwrap(); + + let record_account = Keypair::new(); + let record_account_authority = Keypair::new(); + + token + .confidential_transfer_create_record_account( + &record_account.pubkey(), + &record_account_authority.pubkey(), + &withdraw_proof, + &record_account, + &record_account_authority, + ) + .await + .unwrap(); + + let proof_account = ProofAccount::RecordAccount(record_account.pubkey()); + + token + .confidential_transfer_withdraw( + &alice_meta.token_account, + &alice.pubkey(), + Some(&proof_account), + 42, + decimals, + None, + &alice_meta.elgamal_keypair, + &alice_meta.aes_key, + &[&alice], + ) + .await + .unwrap(); + + let state = token + .get_account_info(&alice_meta.token_account) + .await + .unwrap(); + assert_eq!(state.base.amount, 42); + alice_meta + .check_balances( + &token, + ConfidentialTokenAccountBalances { + pending_balance_lo: 0, + pending_balance_hi: 0, + available_balance: 0, + decryptable_available_balance: 0, + }, + ) + .await; + + token + .confidential_transfer_close_record_account( + &record_account.pubkey(), + &record_account_authority.pubkey(), + &alice.pubkey(), + &record_account_authority, + ) + .await + .unwrap(); +} + #[cfg(feature = "zk-ops")] #[tokio::test] async fn confidential_transfer_transfer() { @@ -1635,7 +1840,9 @@ async fn confidential_transfer_configure_token_account_with_proof_context() { .confidential_transfer_configure_token_account( &token_account, &alice.pubkey(), - Some(&context_state_account.pubkey()), + Some(&ProofAccount::ContextAccount( + context_state_account.pubkey(), + )), None, &elgamal_keypair, &aes_key, @@ -1722,7 +1929,9 @@ async fn confidential_transfer_configure_token_account_with_proof_context() { .confidential_transfer_configure_token_account( &token_account, &bob.pubkey(), - Some(&context_state_account.pubkey()), + Some(&ProofAccount::ContextAccount( + context_state_account.pubkey(), + )), None, &elgamal_keypair, &aes_key, @@ -1809,7 +2018,9 @@ async fn confidential_transfer_empty_account_with_proof_context() { .confidential_transfer_empty_account( &alice_meta.token_account, &alice.pubkey(), - Some(&context_state_account.pubkey()), + Some(&ProofAccount::ContextAccount( + context_state_account.pubkey(), + )), None, &alice_meta.elgamal_keypair, &[&alice], @@ -1864,7 +2075,9 @@ async fn confidential_transfer_empty_account_with_proof_context() { .confidential_transfer_empty_account( &bob_meta.token_account, &bob.pubkey(), - Some(&context_state_account.pubkey()), + Some(&ProofAccount::ContextAccount( + context_state_account.pubkey(), + )), None, &bob_meta.elgamal_keypair, &[&bob], @@ -1977,7 +2190,9 @@ async fn confidential_transfer_withdraw_with_proof_context() { .confidential_transfer_withdraw( &alice_meta.token_account, &alice.pubkey(), - Some(&context_state_account.pubkey()), + Some(&ProofAccount::ContextAccount( + context_state_account.pubkey(), + )), 0, decimals, None, @@ -2035,7 +2250,9 @@ async fn confidential_transfer_withdraw_with_proof_context() { .confidential_transfer_withdraw( &bob_meta.token_account, &bob.pubkey(), - Some(&context_state_account.pubkey()), + Some(&ProofAccount::ContextAccount( + context_state_account.pubkey(), + )), 0, decimals, None, @@ -2169,7 +2386,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(&ProofAccount::ContextAccount( + context_state_account.pubkey(), + )), 42, None, &alice_meta.elgamal_keypair, @@ -2241,7 +2460,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(&ProofAccount::ContextAccount( + context_state_account.pubkey(), + )), 0, None, &alice_meta.elgamal_keypair, diff --git a/token/program-2022-test/tests/confidential_transfer_fee.rs b/token/program-2022-test/tests/confidential_transfer_fee.rs index 161ee2c2a81..0266bdc124b 100644 --- a/token/program-2022-test/tests/confidential_transfer_fee.rs +++ b/token/program-2022-test/tests/confidential_transfer_fee.rs @@ -35,6 +35,7 @@ use { }, spl_token_client::{ client::{SendTransaction, SimulateTransaction}, + proof_generation::ProofAccount, token::{ExtensionInitializationParams, Token, TokenError as TokenClientError}, }, std::{convert::TryInto, mem::size_of}, @@ -612,6 +613,193 @@ async fn confidential_transfer_withdraw_withheld_tokens_from_mint() { check_withheld_amount_in_mint(&token, &withdraw_withheld_authority_elgamal_keypair, 0).await; } +#[cfg(feature = "zk-ops")] +#[tokio::test] +async fn confidential_transfer_withdraw_withheld_tokens_from_mint_with_record_account() { + let transfer_fee_authority = Keypair::new(); + let withdraw_withheld_authority = Keypair::new(); + + let confidential_transfer_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 confidential_transfer_fee_authority = Keypair::new(); + let withdraw_withheld_authority_elgamal_keypair = ElGamalKeypair::new_rand(); + let withdraw_withheld_authority_elgamal_pubkey = + (*withdraw_withheld_authority_elgamal_keypair.pubkey()).into(); + + let mut context = TestContext::new().await; + context + .init_token_with_mint(vec![ + ExtensionInitializationParams::TransferFeeConfig { + transfer_fee_config_authority: Some(transfer_fee_authority.pubkey()), + withdraw_withheld_authority: Some(withdraw_withheld_authority.pubkey()), + transfer_fee_basis_points: TEST_FEE_BASIS_POINTS, + maximum_fee: TEST_MAXIMUM_FEE, + }, + ExtensionInitializationParams::ConfidentialTransferMint { + authority: Some(confidential_transfer_authority.pubkey()), + auto_approve_new_accounts, + auditor_elgamal_pubkey: Some(auditor_elgamal_pubkey), + }, + ExtensionInitializationParams::ConfidentialTransferFeeConfig { + authority: Some(confidential_transfer_fee_authority.pubkey()), + withdraw_withheld_authority_elgamal_pubkey, + }, + ]) + .await + .unwrap(); + + let TokenContext { + token, + alice, + bob, + mint_authority, + decimals, + .. + } = context.token_context.unwrap(); + + let alice_meta = + ConfidentialTokenAccountMeta::new(&token, &alice, &mint_authority, 100, decimals).await; + let bob_meta = + ConfidentialTokenAccountMeta::new(&token, &bob, &mint_authority, 0, decimals).await; + + let transfer_fee_parameters = TransferFee { + epoch: 0.into(), + maximum_fee: TEST_MAXIMUM_FEE.into(), + transfer_fee_basis_points: TEST_FEE_BASIS_POINTS.into(), + }; + + // Test fee is 2.5% so the withheld fees should be 3 + token + .confidential_transfer_transfer_with_fee( + &alice_meta.token_account, + &bob_meta.token_account, + &alice.pubkey(), + None, + 100, + None, + &alice_meta.elgamal_keypair, + &alice_meta.aes_key, + bob_meta.elgamal_keypair.pubkey(), + Some(auditor_elgamal_keypair.pubkey()), + withdraw_withheld_authority_elgamal_keypair.pubkey(), + transfer_fee_parameters.transfer_fee_basis_points.into(), + transfer_fee_parameters.maximum_fee.into(), + &[&alice], + ) + .await + .unwrap(); + + let new_decryptable_available_balance = alice_meta.aes_key.encrypt(0); + token + .confidential_transfer_withdraw_withheld_tokens_from_mint( + &alice_meta.token_account, + &withdraw_withheld_authority.pubkey(), + None, + None, + &withdraw_withheld_authority_elgamal_keypair, + alice_meta.elgamal_keypair.pubkey(), + &new_decryptable_available_balance.into(), + &[&withdraw_withheld_authority], + ) + .await + .unwrap(); + + // withheld fees are not harvested to mint yet + alice_meta + .check_balances( + &token, + ConfidentialTokenAccountBalances { + pending_balance_lo: 0, + pending_balance_hi: 0, + available_balance: 0, + decryptable_available_balance: 0, + }, + ) + .await; + + token + .confidential_transfer_harvest_withheld_tokens_to_mint(&[&bob_meta.token_account]) + .await + .unwrap(); + + let state = token + .get_account_info(&bob_meta.token_account) + .await + .unwrap(); + let extension = state + .get_extension::() + .unwrap(); + assert_eq!(extension.withheld_amount, pod::ElGamalCiphertext::zeroed()); + + // calculate and encrypt fee to attach to the `WithdrawWithheldTokensFromMint` + // instruction data + let fee = transfer_fee_parameters.calculate_fee(100).unwrap(); + let new_decryptable_available_balance = alice_meta.aes_key.encrypt(fee); + + check_withheld_amount_in_mint(&token, &withdraw_withheld_authority_elgamal_keypair, fee).await; + + let state = token.get_mint_info().await.unwrap(); + let extension = state + .get_extension::() + .unwrap(); + let account_info = WithheldTokensInfo::new(&extension.withheld_amount); + + let equality_proof = account_info + .generate_proof_data( + &withdraw_withheld_authority_elgamal_keypair, + alice_meta.elgamal_keypair.pubkey(), + ) + .unwrap(); + + let record_account = Keypair::new(); + let record_account_authority = Keypair::new(); + + token + .confidential_transfer_create_record_account( + &record_account.pubkey(), + &record_account_authority.pubkey(), + &equality_proof, + &record_account, + &record_account_authority, + ) + .await + .unwrap(); + + let proof_account = ProofAccount::RecordAccount(record_account.pubkey()); + + token + .confidential_transfer_withdraw_withheld_tokens_from_mint( + &alice_meta.token_account, + &withdraw_withheld_authority.pubkey(), + Some(&proof_account), + None, + &withdraw_withheld_authority_elgamal_keypair, + alice_meta.elgamal_keypair.pubkey(), + &new_decryptable_available_balance.into(), + &[&withdraw_withheld_authority], + ) + .await + .unwrap(); + + // withheld fees are withdrawn back to alice's account + alice_meta + .check_balances( + &token, + ConfidentialTokenAccountBalances { + pending_balance_lo: 0, + pending_balance_hi: 0, + available_balance: 3, + decryptable_available_balance: 3, + }, + ) + .await; + + check_withheld_amount_in_mint(&token, &withdraw_withheld_authority_elgamal_keypair, 0).await; +} + #[cfg(feature = "zk-ops")] #[tokio::test] async fn confidential_transfer_withdraw_withheld_tokens_from_accounts() { @@ -742,6 +930,169 @@ async fn confidential_transfer_withdraw_withheld_tokens_from_accounts() { assert_eq!(extension.withheld_amount, pod::ElGamalCiphertext::zeroed()); } +#[cfg(feature = "zk-ops")] +#[tokio::test] +async fn confidential_transfer_withdraw_withheld_tokens_from_accounts_with_record_account() { + let transfer_fee_authority = Keypair::new(); + let withdraw_withheld_authority = Keypair::new(); + + let confidential_transfer_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 confidential_transfer_fee_authority = Keypair::new(); + let withdraw_withheld_authority_elgamal_keypair = ElGamalKeypair::new_rand(); + let withdraw_withheld_authority_elgamal_pubkey = + (*withdraw_withheld_authority_elgamal_keypair.pubkey()).into(); + + let mut context = TestContext::new().await; + context + .init_token_with_mint(vec![ + ExtensionInitializationParams::TransferFeeConfig { + transfer_fee_config_authority: Some(transfer_fee_authority.pubkey()), + withdraw_withheld_authority: Some(withdraw_withheld_authority.pubkey()), + transfer_fee_basis_points: TEST_FEE_BASIS_POINTS, + maximum_fee: TEST_MAXIMUM_FEE, + }, + ExtensionInitializationParams::ConfidentialTransferMint { + authority: Some(confidential_transfer_authority.pubkey()), + auto_approve_new_accounts, + auditor_elgamal_pubkey: Some(auditor_elgamal_pubkey), + }, + ExtensionInitializationParams::ConfidentialTransferFeeConfig { + authority: Some(confidential_transfer_fee_authority.pubkey()), + withdraw_withheld_authority_elgamal_pubkey, + }, + ]) + .await + .unwrap(); + + let TokenContext { + token, + alice, + bob, + mint_authority, + decimals, + .. + } = context.token_context.unwrap(); + + let alice_meta = + ConfidentialTokenAccountMeta::new(&token, &alice, &mint_authority, 100, decimals).await; + let bob_meta = + ConfidentialTokenAccountMeta::new(&token, &bob, &mint_authority, 0, decimals).await; + + let transfer_fee_parameters = TransferFee { + epoch: 0.into(), + maximum_fee: TEST_MAXIMUM_FEE.into(), + transfer_fee_basis_points: TEST_FEE_BASIS_POINTS.into(), + }; + + // Test fee is 2.5% so the withheld fees should be 3 + token + .confidential_transfer_transfer_with_fee( + &alice_meta.token_account, + &bob_meta.token_account, + &alice.pubkey(), + None, + 100, + None, + &alice_meta.elgamal_keypair, + &alice_meta.aes_key, + bob_meta.elgamal_keypair.pubkey(), + Some(auditor_elgamal_keypair.pubkey()), + withdraw_withheld_authority_elgamal_keypair.pubkey(), + transfer_fee_parameters.transfer_fee_basis_points.into(), + transfer_fee_parameters.maximum_fee.into(), + &[&alice], + ) + .await + .unwrap(); + + let state = token + .get_account_info(&bob_meta.token_account) + .await + .unwrap(); + let withheld_amount = state + .get_extension::() + .unwrap() + .withheld_amount; + let withheld_tokens_info = WithheldTokensInfo::new(&withheld_amount); + + let equality_proof = withheld_tokens_info + .generate_proof_data( + &withdraw_withheld_authority_elgamal_keypair, + alice_meta.elgamal_keypair.pubkey(), + ) + .unwrap(); + + let record_account = Keypair::new(); + let record_account_authority = Keypair::new(); + + token + .confidential_transfer_create_record_account( + &record_account.pubkey(), + &record_account_authority.pubkey(), + &equality_proof, + &record_account, + &record_account_authority, + ) + .await + .unwrap(); + + let proof_account = ProofAccount::RecordAccount(record_account.pubkey()); + + let fee = transfer_fee_parameters.calculate_fee(100).unwrap(); + let new_decryptable_available_balance = alice_meta.aes_key.encrypt(fee); + token + .confidential_transfer_withdraw_withheld_tokens_from_accounts( + &alice_meta.token_account, + &withdraw_withheld_authority.pubkey(), + Some(&proof_account), + None, + &withdraw_withheld_authority_elgamal_keypair, + alice_meta.elgamal_keypair.pubkey(), + &new_decryptable_available_balance.into(), + &[&bob_meta.token_account], + &[&withdraw_withheld_authority], + ) + .await + .unwrap(); + + alice_meta + .check_balances( + &token, + ConfidentialTokenAccountBalances { + pending_balance_lo: 0, + pending_balance_hi: 0, + available_balance: fee, + decryptable_available_balance: fee, + }, + ) + .await; + + bob_meta + .check_balances( + &token, + ConfidentialTokenAccountBalances { + pending_balance_lo: 97, + pending_balance_hi: 0, + available_balance: 0, + decryptable_available_balance: 0, + }, + ) + .await; + + let state = token + .get_account_info(&bob_meta.token_account) + .await + .unwrap(); + let extension = state + .get_extension::() + .unwrap(); + assert_eq!(extension.withheld_amount, pod::ElGamalCiphertext::zeroed()); +} + #[cfg(feature = "zk-ops")] #[tokio::test] async fn confidential_transfer_withdraw_withheld_tokens_from_mint_with_proof_context() { @@ -885,7 +1236,9 @@ async fn confidential_transfer_withdraw_withheld_tokens_from_mint_with_proof_con .confidential_transfer_withdraw_withheld_tokens_from_mint( &alice_meta.token_account, &withdraw_withheld_authority.pubkey(), - Some(&context_state_account.pubkey()), + Some(&ProofAccount::ContextAccount( + context_state_account.pubkey(), + )), None, &withdraw_withheld_authority_elgamal_keypair, alice_meta.elgamal_keypair.pubkey(), @@ -1051,7 +1404,9 @@ async fn confidential_transfer_withdraw_withheld_tokens_from_accounts_with_proof .confidential_transfer_withdraw_withheld_tokens_from_accounts( &alice_meta.token_account, &withdraw_withheld_authority.pubkey(), - Some(&context_state_account.pubkey()), + Some(&ProofAccount::ContextAccount( + context_state_account.pubkey(), + )), None, &withdraw_withheld_authority_elgamal_keypair, alice_meta.elgamal_keypair.pubkey(), diff --git a/token/program-2022-test/tests/program_test.rs b/token/program-2022-test/tests/program_test.rs index 0d4979383b4..698ebc346ae 100644 --- a/token/program-2022-test/tests/program_test.rs +++ b/token/program-2022-test/tests/program_test.rs @@ -42,7 +42,13 @@ pub struct TestContext { impl TestContext { pub async fn new() -> Self { - let program_test = ProgramTest::new("spl_token_2022", id(), processor!(Processor::process)); + let mut program_test = + ProgramTest::new("spl_token_2022", id(), processor!(Processor::process)); + program_test.add_program( + "spl_record", + spl_record::id(), + processor!(spl_record::processor::process_instruction), + ); let context = program_test.start_with_context().await; let context = Arc::new(Mutex::new(context)); From 5aea1aebb599f89074c56988c8eb6cf86871e78f Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Fri, 26 Jul 2024 19:11:21 +0900 Subject: [PATCH 04/16] cargo fmt --- token/client/src/token.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/token/client/src/token.rs b/token/client/src/token.rs index 0b9db16b6ac..1113cfa9563 100644 --- a/token/client/src/token.rs +++ b/token/client/src/token.rs @@ -2485,9 +2485,9 @@ where .await .map_err(TokenError::Client)?; - // A closure that constructs a vector of instructions needed to create and write to record - // accounts. The closure is defined as a convenience function to be fed into the function - // `calculate_record_max_chunk_size`. + // A closure that constructs a vector of instructions needed to create and write + // to record accounts. The closure is defined as a convenience function + // to be fed into the function `calculate_record_max_chunk_size`. let create_record_instructions = |first_instruction: bool, bytes: &[u8], offset: u64| { let mut ixs = vec![]; if first_instruction { From c0372276d6fe10933c2b3ac4c68f0c3028c89e7e Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Fri, 26 Jul 2024 19:26:11 +0900 Subject: [PATCH 05/16] update token-cli for the new interface --- token/cli/src/command.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/token/cli/src/command.rs b/token/cli/src/command.rs index 82abe3910f7..1faf086127a 100644 --- a/token/cli/src/command.rs +++ b/token/cli/src/command.rs @@ -69,6 +69,7 @@ use { }, spl_token_client::{ client::{ProgramRpcClientSendTransaction, RpcClientResponse}, + proof_generation::ProofAccount, token::{ComputeUnitLimit, ExtensionInitializationParams, Token}, }, spl_token_group_interface::state::TokenGroup, @@ -3348,7 +3349,7 @@ async fn command_deposit_withdraw_confidential_tokens( .confidential_transfer_withdraw( &token_account_address, &owner, - Some(&context_state_pubkey), + Some(&ProofAccount::ContextAccount(context_state_pubkey)), amount, decimals, Some(withdraw_account_info), From 7d23697374fec8c91f77af46e7fe3ec0ea304b5d Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Mon, 29 Jul 2024 10:46:03 +0900 Subject: [PATCH 06/16] remove unnecessary file --- token/client/src/temp_client.rs | 130 -------------------------------- 1 file changed, 130 deletions(-) delete mode 100644 token/client/src/temp_client.rs diff --git a/token/client/src/temp_client.rs b/token/client/src/temp_client.rs deleted file mode 100644 index c6c69b0b038..00000000000 --- a/token/client/src/temp_client.rs +++ /dev/null @@ -1,130 +0,0 @@ -/// Basic trait for sending transactions to validators -pub trait SendTransaction { - type Output; -} - -/// Basic trait for simulating transactions in a validator -pub trait SimulateTransaction { - type SimulationOutput: SimulationResult; -} - -/// Trait for the output of a simulation -pub trait SimulationResult { - fn get_compute_units_consumed(&self) -> ProgramClientResult; -} - -/// Extend basic `SendTransaction` trait with function `send` where client is -/// `&mut BankClient`. Required for `ProgramBanksClient`. -pub trait SendTransactionBanksClient: SendTransaction { - fn send<'a>( - &self, - client: &'a mut BanksClient, - transaction: Transaction, - ) -> BoxFuture<'a, ProgramClientResult>; -} - -/// Extends basic `SimulateTransaction` trait with function `simulation` where -/// client is `&mut BanksClient`. Required for `ProgramBanksClient`. -pub trait SimulateTransactionBanksClient: SimulateTransaction { - fn simulate<'a>( - &self, - client: &'a mut BanksClient, - transaction: Transaction, - ) -> BoxFuture<'a, ProgramClientResult>; -} - -/// Send transaction to validator using `BanksClient::process_transaction`. -#[derive(Debug, Clone, Copy, Default)] -pub struct ProgramBanksClientProcessTransaction; - -impl SendTransaction for ProgramBanksClientProcessTransaction { - type Output = (); -} - -impl SendTransactionBanksClient for ProgramBanksClientProcessTransaction { - fn send<'a>( - &self, - client: &'a mut BanksClient, - transaction: Transaction, - ) -> BoxFuture<'a, ProgramClientResult> { - Box::pin(async move { - client - .process_transaction(transaction) - .await - .map_err(Into::into) - }) - } -} - -pub type ProgramClientError = Box; -pub type ProgramClientResult = Result; - -/// Generic client interface for programs. -#[async_trait] -pub trait ProgramClient -where - ST: SendTransaction + SimulateTransaction, -{ - async fn get_minimum_balance_for_rent_exemption( - &self, - data_len: usize, - ) -> ProgramClientResult; - - async fn get_latest_blockhash(&self) -> ProgramClientResult; - - async fn send_transaction(&self, transaction: &Transaction) -> ProgramClientResult; - - async fn get_account(&self, address: Pubkey) -> ProgramClientResult>; - - async fn simulate_transaction( - &self, - transaction: &Transaction, - ) -> ProgramClientResult; -} - -enum ProgramBanksClientContext { - Client(Arc>), - Context(Arc>), -} - -/// Program client for `BanksClient` from crate `solana-program-test`. -pub struct ProgramBanksClient { - context: ProgramBanksClientContext, - send: ST, -} - -impl fmt::Debug for ProgramBanksClient { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("ProgramBanksClient").finish() - } -} - -impl ProgramBanksClient { - fn new(context: ProgramBanksClientContext, send: ST) -> Self { - Self { context, send } - } - - pub fn new_from_client(client: Arc>, send: ST) -> Self { - Self::new(ProgramBanksClientContext::Client(client), send) - } - - pub fn new_from_context(context: Arc>, send: ST) -> Self { - Self::new(ProgramBanksClientContext::Context(context), send) - } - - async fn run_in_lock(&self, f: F) -> O - where - for<'a> F: Fn(&'a mut BanksClient) -> BoxFuture<'a, O>, - { - match &self.context { - ProgramBanksClientContext::Client(client) => { - let mut lock = client.lock().await; - f(&mut lock).await - } - ProgramBanksClientContext::Context(context) => { - let mut lock = context.lock().await; - f(&mut lock.banks_client).await - } - } - } -} From 2c10b5d3b9270037f608d5242b23b03b3c3a1546 Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Mon, 29 Jul 2024 10:49:47 +0900 Subject: [PATCH 07/16] remove dependency on record program --- Cargo.lock | 1 - token/program-2022/Cargo.toml | 1 - token/program-2022/src/lib.rs | 8 -------- token/program-2022/src/proof.rs | 2 -- 4 files changed, 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9c516c395a1..084adcdbfff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7571,7 +7571,6 @@ dependencies = [ "solana-zk-token-sdk", "spl-memo 5.0.0", "spl-pod 0.3.0", - "spl-record", "spl-tlv-account-resolution 0.7.0", "spl-token 6.0.0", "spl-token-group-interface 0.3.0", diff --git a/token/program-2022/Cargo.toml b/token/program-2022/Cargo.toml index dd911d216fe..18e3f95531b 100644 --- a/token/program-2022/Cargo.toml +++ b/token/program-2022/Cargo.toml @@ -26,7 +26,6 @@ solana-program = "2.0.3" solana-security-txt = "1.1.1" solana-zk-token-sdk = "2.0.3" spl-memo = { version = "5.0", path = "../../memo/program", features = [ "no-entrypoint" ] } -spl-record = { version = "0.2.0", path = "../../record/program", features = [ "no-entrypoint" ]} spl-token = { version = "6.0", path = "../program", features = ["no-entrypoint"] } spl-token-group-interface = { version = "0.3.0", path = "../../token-group/interface" } spl-token-metadata-interface = { version = "0.4.0", path = "../../token-metadata/interface" } diff --git a/token/program-2022/src/lib.rs b/token/program-2022/src/lib.rs index 3830c18f603..7561c6b5d8e 100644 --- a/token/program-2022/src/lib.rs +++ b/token/program-2022/src/lib.rs @@ -121,14 +121,6 @@ pub fn check_zk_token_proof_program_account(zk_token_proof_program_id: &Pubkey) Ok(()) } -/// Checks that the supplied program ID is correct for the spl record program -pub fn check_spl_record_program_account(spl_record_program_id: &Pubkey) -> ProgramResult { - if spl_record_program_id != &spl_record::id() { - return Err(ProgramError::IncorrectProgramId); - } - Ok(()) -} - /// Checks if the spplied program ID is that of the system program pub fn check_system_program_account(system_program_id: &Pubkey) -> ProgramResult { if system_program_id != &system_program::id() { diff --git a/token/program-2022/src/proof.rs b/token/program-2022/src/proof.rs index 2ea006eaf3c..71e54f2add6 100644 --- a/token/program-2022/src/proof.rs +++ b/token/program-2022/src/proof.rs @@ -1,7 +1,6 @@ //! Helper for processing instruction data from ZK Token proof program use { - crate::check_spl_record_program_account, bytemuck::Pod, solana_program::{ account_info::{next_account_info, AccountInfo}, @@ -40,7 +39,6 @@ pub fn decode_proof_instruction_context, U: Pod>( // the ZK ElGamal proof program. if instruction.data.len() == INSTRUCTION_DATA_LENGTH_WITH_RECORD_ACCOUNT { let record_account = next_account_info(account_info_iter)?; - check_spl_record_program_account(record_account.owner)?; // first byte is the proof type let start_offset = u32::from_le_bytes(instruction.data[1..].try_into().unwrap()) as usize; From 55b4baf29f232845b8dca76421eca45c18fb8b79 Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Mon, 29 Jul 2024 11:13:25 +0900 Subject: [PATCH 08/16] rename `ProofData::ProofData` to `ProofData::InstructionData` --- token/client/src/token.rs | 14 +++++++------- .../extension/confidential_transfer/instruction.rs | 14 +++++++++----- .../confidential_transfer_fee/instruction.rs | 4 ++-- token/program-2022/src/proof.rs | 4 ++-- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/token/client/src/token.rs b/token/client/src/token.rs index 1113cfa9563..e3c811066ca 100644 --- a/token/client/src/token.rs +++ b/token/client/src/token.rs @@ -1922,7 +1922,7 @@ where let proof_location = if let Some(proof_data_temp) = proof_data.as_ref() { ProofLocation::InstructionOffset( 1.try_into().unwrap(), - ProofData::ProofData(proof_data_temp), + ProofData::InstructionData(proof_data_temp), ) } else { match proof_account.unwrap() { @@ -2016,7 +2016,7 @@ where let proof_location = if let Some(proof_data_temp) = proof_data.as_ref() { ProofLocation::InstructionOffset( 1.try_into().unwrap(), - ProofData::ProofData(proof_data_temp), + ProofData::InstructionData(proof_data_temp), ) } else { match proof_account.unwrap() { @@ -2114,7 +2114,7 @@ where let proof_location = if let Some(proof_data_temp) = proof_data.as_ref() { ProofLocation::InstructionOffset( 1.try_into().unwrap(), - ProofData::ProofData(proof_data_temp), + ProofData::InstructionData(proof_data_temp), ) } else { match proof_account.unwrap() { @@ -2256,7 +2256,7 @@ where let proof_location = if let Some(proof_data_temp) = proof_data.as_ref() { ProofLocation::InstructionOffset( 1.try_into().unwrap(), - ProofData::ProofData(proof_data_temp), + ProofData::InstructionData(proof_data_temp), ) } else { match proof_account.unwrap() { @@ -2956,7 +2956,7 @@ where let proof_location = if let Some(proof_data_temp) = proof_data.as_ref() { ProofLocation::InstructionOffset( 1.try_into().unwrap(), - ProofData::ProofData(proof_data_temp), + ProofData::InstructionData(proof_data_temp), ) } else { match proof_account.unwrap() { @@ -3702,7 +3702,7 @@ where let proof_location = if let Some(proof_data_temp) = proof_data.as_ref() { ProofLocation::InstructionOffset( 1.try_into().unwrap(), - ProofData::ProofData(proof_data_temp), + ProofData::InstructionData(proof_data_temp), ) } else { match proof_account.unwrap() { @@ -3787,7 +3787,7 @@ where let proof_location = if let Some(proof_data_temp) = proof_data.as_ref() { ProofLocation::InstructionOffset( 1.try_into().unwrap(), - ProofData::ProofData(proof_data_temp), + ProofData::InstructionData(proof_data_temp), ) } else { match proof_account.unwrap() { diff --git a/token/program-2022/src/extension/confidential_transfer/instruction.rs b/token/program-2022/src/extension/confidential_transfer/instruction.rs index 4b6dfa61f73..d8e857c3817 100644 --- a/token/program-2022/src/extension/confidential_transfer/instruction.rs +++ b/token/program-2022/src/extension/confidential_transfer/instruction.rs @@ -801,7 +801,9 @@ pub fn configure_account( return Err(TokenError::InvalidProofInstructionOffset.into()); } match proof_data { - ProofData::ProofData(data) => instructions.push(verify_pubkey_validity(None, data)), + ProofData::InstructionData(data) => { + instructions.push(verify_pubkey_validity(None, data)) + } ProofData::RecordAccount(address, offset) => instructions.push( ProofInstruction::VerifyPubkeyValidity .encode_verify_proof_from_account(None, address, offset), @@ -913,7 +915,7 @@ pub fn empty_account( return Err(TokenError::InvalidProofInstructionOffset.into()); } match proof_data { - ProofData::ProofData(data) => instructions.push(verify_zero_balance(None, data)), + ProofData::InstructionData(data) => instructions.push(verify_zero_balance(None, data)), ProofData::RecordAccount(address, offset) => instructions.push( ProofInstruction::VerifyZeroBalance .encode_verify_proof_from_account(None, address, offset), @@ -1053,7 +1055,7 @@ pub fn withdraw( return Err(TokenError::InvalidProofInstructionOffset.into()); } match proof_data { - ProofData::ProofData(data) => instructions.push(verify_withdraw(None, data)), + ProofData::InstructionData(data) => instructions.push(verify_withdraw(None, data)), ProofData::RecordAccount(address, offset) => instructions.push( ProofInstruction::VerifyWithdraw .encode_verify_proof_from_account(None, address, offset), @@ -1155,7 +1157,7 @@ pub fn transfer( return Err(TokenError::InvalidProofInstructionOffset.into()); } match proof_data { - ProofData::ProofData(data) => instructions.push(verify_transfer(None, data)), + ProofData::InstructionData(data) => instructions.push(verify_transfer(None, data)), ProofData::RecordAccount(address, offset) => instructions.push( ProofInstruction::VerifyTransfer .encode_verify_proof_from_account(None, address, offset), @@ -1257,7 +1259,9 @@ pub fn transfer_with_fee( return Err(TokenError::InvalidProofInstructionOffset.into()); } match proof_data { - ProofData::ProofData(data) => instructions.push(verify_transfer_with_fee(None, data)), + ProofData::InstructionData(data) => { + instructions.push(verify_transfer_with_fee(None, data)) + } ProofData::RecordAccount(address, offset) => instructions.push( ProofInstruction::VerifyTransferWithFee .encode_verify_proof_from_account(None, address, offset), 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 e6e33ada34a..9bad6aec922 100644 --- a/token/program-2022/src/extension/confidential_transfer_fee/instruction.rs +++ b/token/program-2022/src/extension/confidential_transfer_fee/instruction.rs @@ -380,7 +380,7 @@ pub fn withdraw_withheld_tokens_from_mint( return Err(TokenError::InvalidProofInstructionOffset.into()); } match proof_data { - ProofData::ProofData(data) => { + ProofData::InstructionData(data) => { instructions.push(verify_ciphertext_ciphertext_equality(None, data)) } ProofData::RecordAccount(address, offset) => instructions.push( @@ -491,7 +491,7 @@ pub fn withdraw_withheld_tokens_from_accounts( return Err(TokenError::InvalidProofInstructionOffset.into()); } match proof_data { - ProofData::ProofData(data) => { + ProofData::InstructionData(data) => { instructions.push(verify_ciphertext_ciphertext_equality(None, data)) } ProofData::RecordAccount(address, offset) => instructions.push( diff --git a/token/program-2022/src/proof.rs b/token/program-2022/src/proof.rs index 71e54f2add6..acd42e1c276 100644 --- a/token/program-2022/src/proof.rs +++ b/token/program-2022/src/proof.rs @@ -69,11 +69,11 @@ pub enum ProofLocation<'a, T> { } /// A proof data type to distinguish between proof data included as part of -/// instruction data and proof data stored in a record account. +/// zk-token proof instruction data and proof data stored in a record account. #[derive(Clone, Copy)] pub enum ProofData<'a, T> { /// The proof data - ProofData(&'a T), + InstructionData(&'a T), /// The address of a record account containing the proof data and its byte /// offset RecordAccount(&'a Pubkey, u32), From 9a4557043d6e49071a4c7ac58b363deaeef6f492 Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Mon, 29 Jul 2024 12:46:03 +0900 Subject: [PATCH 09/16] build record program in ci --- .github/workflows/pull-request-token.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull-request-token.yml b/.github/workflows/pull-request-token.yml index 3e2c7261d6e..a079b2bf638 100644 --- a/.github/workflows/pull-request-token.yml +++ b/.github/workflows/pull-request-token.yml @@ -66,7 +66,9 @@ jobs: echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH - name: Build and test token - run: ./ci/cargo-test-sbf.sh token + run: | + cargo build-sbf --manifest-path record/program/Cargo.toml + ./ci/cargo-test-sbf.sh token - name: Upload programs uses: actions/upload-artifact@v3 @@ -246,6 +248,7 @@ jobs: run: | cargo build-sbf --manifest-path token/program-2022/Cargo.toml cargo build-sbf --manifest-path instruction-padding/program/Cargo.toml + cargo build-sbf --manifest-path record/program/Cargo.toml ./token/twoxtx-setup.sh ./token/twoxtx-solana/cargo-test-sbf --manifest-path token/program-2022-test/Cargo.toml -- --nocapture From cf7a0307d1231628cda7e3864c5626c60993c198 Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Tue, 30 Jul 2024 11:15:35 +0900 Subject: [PATCH 10/16] prefer native over bpf for token22 tests --- .github/workflows/pull-request-token.yml | 25 ++++++++----------- token/program-2022-test/tests/program_test.rs | 1 + 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/.github/workflows/pull-request-token.yml b/.github/workflows/pull-request-token.yml index a079b2bf638..d0c0ed38de4 100644 --- a/.github/workflows/pull-request-token.yml +++ b/.github/workflows/pull-request-token.yml @@ -3,19 +3,19 @@ name: Token Pull Request on: pull_request: paths: - - 'associated-token-account/**' - - 'token/**' - - 'ci/*-version.sh' - - '.github/workflows/pull-request-token.yml' - - '!token/js/**' + - "associated-token-account/**" + - "token/**" + - "ci/*-version.sh" + - ".github/workflows/pull-request-token.yml" + - "!token/js/**" push: branches: [master] paths: - - 'associated-token-account/**' - - 'token/**' - - 'ci/*-version.sh' - - '.github/workflows/pull-request-token.yml' - - '!token/js/**' + - "associated-token-account/**" + - "token/**" + - "ci/*-version.sh" + - ".github/workflows/pull-request-token.yml" + - "!token/js/**" concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -66,9 +66,7 @@ jobs: echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH - name: Build and test token - run: | - cargo build-sbf --manifest-path record/program/Cargo.toml - ./ci/cargo-test-sbf.sh token + run: ./ci/cargo-test-sbf.sh token - name: Upload programs uses: actions/upload-artifact@v3 @@ -248,7 +246,6 @@ jobs: run: | cargo build-sbf --manifest-path token/program-2022/Cargo.toml cargo build-sbf --manifest-path instruction-padding/program/Cargo.toml - cargo build-sbf --manifest-path record/program/Cargo.toml ./token/twoxtx-setup.sh ./token/twoxtx-solana/cargo-test-sbf --manifest-path token/program-2022-test/Cargo.toml -- --nocapture diff --git a/token/program-2022-test/tests/program_test.rs b/token/program-2022-test/tests/program_test.rs index 698ebc346ae..25fd28abd79 100644 --- a/token/program-2022-test/tests/program_test.rs +++ b/token/program-2022-test/tests/program_test.rs @@ -44,6 +44,7 @@ impl TestContext { pub async fn new() -> Self { let mut program_test = ProgramTest::new("spl_token_2022", id(), processor!(Processor::process)); + program_test.prefer_bpf(false); program_test.add_program( "spl_record", spl_record::id(), From 123c4ec1f06c7414e4075d12ce2e678d53cf4ed9 Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Tue, 30 Jul 2024 11:21:36 +0900 Subject: [PATCH 11/16] add length check on record account length --- token/program-2022/src/proof.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/token/program-2022/src/proof.rs b/token/program-2022/src/proof.rs index acd42e1c276..d0dcf9efc2d 100644 --- a/token/program-2022/src/proof.rs +++ b/token/program-2022/src/proof.rs @@ -45,7 +45,11 @@ pub fn decode_proof_instruction_context, U: Pod>( let end_offset = start_offset .checked_add(std::mem::size_of::()) .ok_or(ProgramError::InvalidAccountData)?; - let raw_proof_data = &record_account.data.borrow()[start_offset..end_offset]; + + let record_account_data = record_account.data.borrow(); + let raw_proof_data = record_account_data + .get(start_offset..end_offset) + .ok_or(ProgramError::AccountDataTooSmall)?; bytemuck::try_from_bytes::(raw_proof_data) .map(|proof_data| *ZkProofData::context_data(proof_data)) From b2131ec6f7be27b0d88392ebf53351b18d19cccb Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Tue, 30 Jul 2024 11:33:22 +0900 Subject: [PATCH 12/16] remove redundant test on withdrawing empty withheld tokens --- .../tests/confidential_transfer_fee.rs | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/token/program-2022-test/tests/confidential_transfer_fee.rs b/token/program-2022-test/tests/confidential_transfer_fee.rs index 0266bdc124b..15bb3b51cbb 100644 --- a/token/program-2022-test/tests/confidential_transfer_fee.rs +++ b/token/program-2022-test/tests/confidential_transfer_fee.rs @@ -692,34 +692,6 @@ async fn confidential_transfer_withdraw_withheld_tokens_from_mint_with_record_ac .await .unwrap(); - let new_decryptable_available_balance = alice_meta.aes_key.encrypt(0); - token - .confidential_transfer_withdraw_withheld_tokens_from_mint( - &alice_meta.token_account, - &withdraw_withheld_authority.pubkey(), - None, - None, - &withdraw_withheld_authority_elgamal_keypair, - alice_meta.elgamal_keypair.pubkey(), - &new_decryptable_available_balance.into(), - &[&withdraw_withheld_authority], - ) - .await - .unwrap(); - - // withheld fees are not harvested to mint yet - alice_meta - .check_balances( - &token, - ConfidentialTokenAccountBalances { - pending_balance_lo: 0, - pending_balance_hi: 0, - available_balance: 0, - decryptable_available_balance: 0, - }, - ) - .await; - token .confidential_transfer_harvest_withheld_tokens_to_mint(&[&bob_meta.token_account]) .await From 8bc6421686027ec8da83878618054293b637af44 Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Tue, 30 Jul 2024 14:03:15 +0900 Subject: [PATCH 13/16] refactor `ProofLocation` logic --- token/client/src/token.rs | 199 +++++++++++++------------------------- 1 file changed, 66 insertions(+), 133 deletions(-) diff --git a/token/client/src/token.rs b/token/client/src/token.rs index e3c811066ca..1386472f865 100644 --- a/token/client/src/token.rs +++ b/token/client/src/token.rs @@ -1919,25 +1919,11 @@ where ) }; - let proof_location = if let Some(proof_data_temp) = proof_data.as_ref() { - ProofLocation::InstructionOffset( - 1.try_into().unwrap(), - ProofData::InstructionData(proof_data_temp), - ) - } else { - match proof_account.unwrap() { - ProofAccount::ContextAccount(context_state_account) => { - ProofLocation::ContextStateAccount(context_state_account) - } - ProofAccount::RecordAccount(record_account) => ProofLocation::InstructionOffset( - 1.try_into().unwrap(), - ProofData::RecordAccount( - record_account, - RecordData::WRITABLE_START_INDEX as u32, - ), - ), - } - }; + // cannot panic as long as either `proof_data` or `proof_account` is `Some(..)`, which is + // guaranteed by the previous check + let proof_location = + Self::confidential_transfer_create_proof_location(proof_data.as_ref(), proof_account) + .unwrap(); let decryptable_balance = aes_key.encrypt(0); @@ -2013,25 +1999,11 @@ where ) }; - let proof_location = if let Some(proof_data_temp) = proof_data.as_ref() { - ProofLocation::InstructionOffset( - 1.try_into().unwrap(), - ProofData::InstructionData(proof_data_temp), - ) - } else { - match proof_account.unwrap() { - ProofAccount::ContextAccount(context_state_account) => { - ProofLocation::ContextStateAccount(context_state_account) - } - ProofAccount::RecordAccount(record_account) => ProofLocation::InstructionOffset( - 1.try_into().unwrap(), - ProofData::RecordAccount( - record_account, - RecordData::WRITABLE_START_INDEX as u32, - ), - ), - } - }; + // cannot panic as long as either `proof_data` or `proof_account` is `Some(..)`, which is + // guaranteed by the previous check + let proof_location = + Self::confidential_transfer_create_proof_location(proof_data.as_ref(), proof_account) + .unwrap(); self.process_ixs( &confidential_transfer::instruction::empty_account( @@ -2111,25 +2083,11 @@ where ) }; - let proof_location = if let Some(proof_data_temp) = proof_data.as_ref() { - ProofLocation::InstructionOffset( - 1.try_into().unwrap(), - ProofData::InstructionData(proof_data_temp), - ) - } else { - match proof_account.unwrap() { - ProofAccount::ContextAccount(context_state_account) => { - ProofLocation::ContextStateAccount(context_state_account) - } - ProofAccount::RecordAccount(record_account) => ProofLocation::InstructionOffset( - 1.try_into().unwrap(), - ProofData::RecordAccount( - record_account, - RecordData::WRITABLE_START_INDEX as u32, - ), - ), - } - }; + // cannot panic as long as either `proof_data` or `proof_account` is `Some(..)`, which is + // guaranteed by the previous check + let proof_location = + Self::confidential_transfer_create_proof_location(proof_data.as_ref(), proof_account) + .unwrap(); let new_decryptable_available_balance = account_info .new_decryptable_available_balance(withdraw_amount, aes_key) @@ -2253,25 +2211,11 @@ where ) }; - let proof_location = if let Some(proof_data_temp) = proof_data.as_ref() { - ProofLocation::InstructionOffset( - 1.try_into().unwrap(), - ProofData::InstructionData(proof_data_temp), - ) - } else { - match proof_account.unwrap() { - ProofAccount::ContextAccount(context_state_account) => { - ProofLocation::ContextStateAccount(context_state_account) - } - ProofAccount::RecordAccount(record_account) => ProofLocation::InstructionOffset( - 1.try_into().unwrap(), - ProofData::RecordAccount( - record_account, - RecordData::WRITABLE_START_INDEX as u32, - ), - ), - } - }; + // cannot panic as long as either `proof_data` or `proof_account` is `Some(..)`, which is + // guaranteed by the previous check + let proof_location = + Self::confidential_transfer_create_proof_location(proof_data.as_ref(), proof_account) + .unwrap(); let new_decryptable_available_balance = account_info .new_decryptable_available_balance(transfer_amount, source_aes_key) @@ -2953,25 +2897,11 @@ where ) }; - let proof_location = if let Some(proof_data_temp) = proof_data.as_ref() { - ProofLocation::InstructionOffset( - 1.try_into().unwrap(), - ProofData::InstructionData(proof_data_temp), - ) - } else { - match proof_account.unwrap() { - ProofAccount::ContextAccount(context_state_account) => { - ProofLocation::ContextStateAccount(context_state_account) - } - ProofAccount::RecordAccount(record_account) => ProofLocation::InstructionOffset( - 1.try_into().unwrap(), - ProofData::RecordAccount( - record_account, - RecordData::WRITABLE_START_INDEX as u32, - ), - ), - } - }; + // cannot panic as long as either `proof_data` or `proof_account` is `Some(..)`, which is + // guaranteed by the previous check + let proof_location = + Self::confidential_transfer_create_proof_location(proof_data.as_ref(), proof_account) + .unwrap(); let new_decryptable_available_balance = account_info .new_decryptable_available_balance(transfer_amount, source_aes_key) @@ -3699,25 +3629,11 @@ where ) }; - let proof_location = if let Some(proof_data_temp) = proof_data.as_ref() { - ProofLocation::InstructionOffset( - 1.try_into().unwrap(), - ProofData::InstructionData(proof_data_temp), - ) - } else { - match proof_account.unwrap() { - ProofAccount::ContextAccount(context_state_account) => { - ProofLocation::ContextStateAccount(context_state_account) - } - ProofAccount::RecordAccount(record_account) => ProofLocation::InstructionOffset( - 1.try_into().unwrap(), - ProofData::RecordAccount( - record_account, - RecordData::WRITABLE_START_INDEX as u32, - ), - ), - } - }; + // cannot panic as long as either `proof_data` or `proof_account` is `Some(..)`, which is + // guaranteed by the previous check + let proof_location = + Self::confidential_transfer_create_proof_location(proof_data.as_ref(), proof_account) + .unwrap(); self.process_ixs( &confidential_transfer_fee::instruction::withdraw_withheld_tokens_from_mint( @@ -3784,25 +3700,11 @@ where ) }; - let proof_location = if let Some(proof_data_temp) = proof_data.as_ref() { - ProofLocation::InstructionOffset( - 1.try_into().unwrap(), - ProofData::InstructionData(proof_data_temp), - ) - } else { - match proof_account.unwrap() { - ProofAccount::ContextAccount(context_state_account) => { - ProofLocation::ContextStateAccount(context_state_account) - } - ProofAccount::RecordAccount(record_account) => ProofLocation::InstructionOffset( - 1.try_into().unwrap(), - ProofData::RecordAccount( - record_account, - RecordData::WRITABLE_START_INDEX as u32, - ), - ), - } - }; + // cannot panic as long as either `proof_data` or `proof_account` is `Some(..)`, which is + // guaranteed by the previous check + let proof_location = + Self::confidential_transfer_create_proof_location(proof_data.as_ref(), proof_account) + .unwrap(); self.process_ixs( &confidential_transfer_fee::instruction::withdraw_withheld_tokens_from_accounts( @@ -3886,6 +3788,37 @@ where .await } + // Creates `ProofLocation` from proof data and `ProofAccount`. If both `proof_data` and + // `proof_account` are `None`, then the result is `None`. + fn confidential_transfer_create_proof_location<'a, ZK: ZkProofData, U: Pod>( + proof_data: Option<&'a ZK>, + proof_account: Option<&'a ProofAccount>, + ) -> Option> { + if let Some(proof_data) = proof_data { + Some(ProofLocation::InstructionOffset( + 1.try_into().unwrap(), + ProofData::InstructionData(proof_data), + )) + } else if let Some(proof_account) = proof_account { + match proof_account { + ProofAccount::ContextAccount(context_state_account) => { + Some(ProofLocation::ContextStateAccount(context_state_account)) + } + ProofAccount::RecordAccount(record_account) => { + Some(ProofLocation::InstructionOffset( + 1.try_into().unwrap(), + ProofData::RecordAccount( + record_account, + RecordData::WRITABLE_START_INDEX as u32, + ), + )) + } + } + } else { + None + } + } + pub async fn withdraw_excess_lamports( &self, source: &Pubkey, From 801e51457e821e59e28f44f5f2f5e42c63901bf2 Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Tue, 30 Jul 2024 14:10:44 +0900 Subject: [PATCH 14/16] add instruction offset in `ProofAccount::RecordAccount` --- token/client/src/proof_generation.rs | 2 +- token/client/src/token.rs | 7 ++----- .../program-2022-test/tests/confidential_transfer.rs | 11 +++++++++-- .../tests/confidential_transfer_fee.rs | 11 +++++++++-- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/token/client/src/proof_generation.rs b/token/client/src/proof_generation.rs index b7582945e88..cd5f540c7ad 100644 --- a/token/client/src/proof_generation.rs +++ b/token/client/src/proof_generation.rs @@ -35,7 +35,7 @@ use { pub enum ProofAccount { ContextAccount(Pubkey), - RecordAccount(Pubkey), + RecordAccount(Pubkey, u32), } /// The main logic to create the five split proof data for a transfer with fee. diff --git a/token/client/src/token.rs b/token/client/src/token.rs index 1386472f865..e613f206cd7 100644 --- a/token/client/src/token.rs +++ b/token/client/src/token.rs @@ -3804,13 +3804,10 @@ where ProofAccount::ContextAccount(context_state_account) => { Some(ProofLocation::ContextStateAccount(context_state_account)) } - ProofAccount::RecordAccount(record_account) => { + ProofAccount::RecordAccount(record_account, offset) => { Some(ProofLocation::InstructionOffset( 1.try_into().unwrap(), - ProofData::RecordAccount( - record_account, - RecordData::WRITABLE_START_INDEX as u32, - ), + ProofData::RecordAccount(record_account, *offset), )) } } diff --git a/token/program-2022-test/tests/confidential_transfer.rs b/token/program-2022-test/tests/confidential_transfer.rs index b7386a8f3c4..ce125d5e029 100644 --- a/token/program-2022-test/tests/confidential_transfer.rs +++ b/token/program-2022-test/tests/confidential_transfer.rs @@ -15,6 +15,7 @@ use { transaction::{Transaction, TransactionError}, transport::TransportError, }, + spl_record::state::RecordData, spl_token_2022::{ error::TokenError, extension::{ @@ -497,7 +498,10 @@ async fn confidential_transfer_empty_account_with_record() { .await .unwrap(); - let proof_account = ProofAccount::RecordAccount(record_account.pubkey()); + let proof_account = ProofAccount::RecordAccount( + record_account.pubkey(), + RecordData::WRITABLE_START_INDEX as u32, + ); token .confidential_transfer_empty_account( @@ -969,7 +973,10 @@ async fn confidential_transfer_withdraw_with_record_account() { .await .unwrap(); - let proof_account = ProofAccount::RecordAccount(record_account.pubkey()); + let proof_account = ProofAccount::RecordAccount( + record_account.pubkey(), + RecordData::WRITABLE_START_INDEX as u32, + ); token .confidential_transfer_withdraw( diff --git a/token/program-2022-test/tests/confidential_transfer_fee.rs b/token/program-2022-test/tests/confidential_transfer_fee.rs index 15bb3b51cbb..1221c773828 100644 --- a/token/program-2022-test/tests/confidential_transfer_fee.rs +++ b/token/program-2022-test/tests/confidential_transfer_fee.rs @@ -13,6 +13,7 @@ use { transaction::{Transaction, TransactionError}, transport::TransportError, }, + spl_record::state::RecordData, spl_token_2022::{ error::TokenError, extension::{ @@ -740,7 +741,10 @@ async fn confidential_transfer_withdraw_withheld_tokens_from_mint_with_record_ac .await .unwrap(); - let proof_account = ProofAccount::RecordAccount(record_account.pubkey()); + let proof_account = ProofAccount::RecordAccount( + record_account.pubkey(), + RecordData::WRITABLE_START_INDEX as u32, + ); token .confidential_transfer_withdraw_withheld_tokens_from_mint( @@ -1012,7 +1016,10 @@ async fn confidential_transfer_withdraw_withheld_tokens_from_accounts_with_recor .await .unwrap(); - let proof_account = ProofAccount::RecordAccount(record_account.pubkey()); + let proof_account = ProofAccount::RecordAccount( + record_account.pubkey(), + RecordData::WRITABLE_START_INDEX as u32, + ); let fee = transfer_fee_parameters.calculate_fee(100).unwrap(); let new_decryptable_available_balance = alice_meta.aes_key.encrypt(fee); From df0d78448bd7149c03958a9458f9e308a52c4061 Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Tue, 30 Jul 2024 18:05:35 +0900 Subject: [PATCH 15/16] cargo fmt --- token/client/src/token.rs | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/token/client/src/token.rs b/token/client/src/token.rs index e613f206cd7..c65b2b40812 100644 --- a/token/client/src/token.rs +++ b/token/client/src/token.rs @@ -1919,8 +1919,8 @@ where ) }; - // cannot panic as long as either `proof_data` or `proof_account` is `Some(..)`, which is - // guaranteed by the previous check + // cannot panic as long as either `proof_data` or `proof_account` is `Some(..)`, + // which is guaranteed by the previous check let proof_location = Self::confidential_transfer_create_proof_location(proof_data.as_ref(), proof_account) .unwrap(); @@ -1999,8 +1999,8 @@ where ) }; - // cannot panic as long as either `proof_data` or `proof_account` is `Some(..)`, which is - // guaranteed by the previous check + // cannot panic as long as either `proof_data` or `proof_account` is `Some(..)`, + // which is guaranteed by the previous check let proof_location = Self::confidential_transfer_create_proof_location(proof_data.as_ref(), proof_account) .unwrap(); @@ -2083,8 +2083,8 @@ where ) }; - // cannot panic as long as either `proof_data` or `proof_account` is `Some(..)`, which is - // guaranteed by the previous check + // cannot panic as long as either `proof_data` or `proof_account` is `Some(..)`, + // which is guaranteed by the previous check let proof_location = Self::confidential_transfer_create_proof_location(proof_data.as_ref(), proof_account) .unwrap(); @@ -2211,8 +2211,8 @@ where ) }; - // cannot panic as long as either `proof_data` or `proof_account` is `Some(..)`, which is - // guaranteed by the previous check + // cannot panic as long as either `proof_data` or `proof_account` is `Some(..)`, + // which is guaranteed by the previous check let proof_location = Self::confidential_transfer_create_proof_location(proof_data.as_ref(), proof_account) .unwrap(); @@ -2897,8 +2897,8 @@ where ) }; - // cannot panic as long as either `proof_data` or `proof_account` is `Some(..)`, which is - // guaranteed by the previous check + // cannot panic as long as either `proof_data` or `proof_account` is `Some(..)`, + // which is guaranteed by the previous check let proof_location = Self::confidential_transfer_create_proof_location(proof_data.as_ref(), proof_account) .unwrap(); @@ -3629,8 +3629,8 @@ where ) }; - // cannot panic as long as either `proof_data` or `proof_account` is `Some(..)`, which is - // guaranteed by the previous check + // cannot panic as long as either `proof_data` or `proof_account` is `Some(..)`, + // which is guaranteed by the previous check let proof_location = Self::confidential_transfer_create_proof_location(proof_data.as_ref(), proof_account) .unwrap(); @@ -3700,8 +3700,8 @@ where ) }; - // cannot panic as long as either `proof_data` or `proof_account` is `Some(..)`, which is - // guaranteed by the previous check + // cannot panic as long as either `proof_data` or `proof_account` is `Some(..)`, + // which is guaranteed by the previous check let proof_location = Self::confidential_transfer_create_proof_location(proof_data.as_ref(), proof_account) .unwrap(); @@ -3788,8 +3788,8 @@ where .await } - // Creates `ProofLocation` from proof data and `ProofAccount`. If both `proof_data` and - // `proof_account` are `None`, then the result is `None`. + // Creates `ProofLocation` from proof data and `ProofAccount`. If both + // `proof_data` and `proof_account` are `None`, then the result is `None`. fn confidential_transfer_create_proof_location<'a, ZK: ZkProofData, U: Pod>( proof_data: Option<&'a ZK>, proof_account: Option<&'a ProofAccount>, From 09f594c8d37eaff9f10708788655895a31c92925 Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Tue, 30 Jul 2024 19:18:18 +0900 Subject: [PATCH 16/16] revert auto-format --- .github/workflows/pull-request-token.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/pull-request-token.yml b/.github/workflows/pull-request-token.yml index d0c0ed38de4..3e2c7261d6e 100644 --- a/.github/workflows/pull-request-token.yml +++ b/.github/workflows/pull-request-token.yml @@ -3,19 +3,19 @@ name: Token Pull Request on: pull_request: paths: - - "associated-token-account/**" - - "token/**" - - "ci/*-version.sh" - - ".github/workflows/pull-request-token.yml" - - "!token/js/**" + - 'associated-token-account/**' + - 'token/**' + - 'ci/*-version.sh' + - '.github/workflows/pull-request-token.yml' + - '!token/js/**' push: branches: [master] paths: - - "associated-token-account/**" - - "token/**" - - "ci/*-version.sh" - - ".github/workflows/pull-request-token.yml" - - "!token/js/**" + - 'associated-token-account/**' + - 'token/**' + - 'ci/*-version.sh' + - '.github/workflows/pull-request-token.yml' + - '!token/js/**' concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}