diff --git a/Cargo.lock b/Cargo.lock index 44c72a07bcd..04fc022e5a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7595,6 +7595,7 @@ dependencies = [ "spl-instruction-padding", "spl-memo 5.0.0", "spl-pod 0.3.1", + "spl-record", "spl-tlv-account-resolution 0.7.0", "spl-token-2022 4.0.1", "spl-token-client", @@ -7649,6 +7650,8 @@ name = "spl-token-client" version = "0.11.0" dependencies = [ "async-trait", + "bincode", + "bytemuck", "curve25519-dalek", "futures 0.3.30", "futures-util", @@ -7660,6 +7663,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/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), diff --git a/token/client/Cargo.toml b/token/client/Cargo.toml index 762b2f3e4cb..bca57598627 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.3" 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..cd5f540c7ad 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, u32), +} + /// 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/token.rs b/token/client/src/token.rs index a9fefc21f70..c65b2b40812 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( @@ -1915,12 +1919,11 @@ where ) }; - let proof_location = if let Some(proof_data_temp) = proof_data.as_ref() { - ProofLocation::InstructionOffset(1.try_into().unwrap(), proof_data_temp) - } else { - let context_state_account = context_state_account.unwrap(); - ProofLocation::ContextStateAccount(context_state_account) - }; + // 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); @@ -1969,7 +1972,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 +1989,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( @@ -1996,12 +1999,11 @@ where ) }; - let proof_location = if let Some(proof_data_temp) = proof_data.as_ref() { - ProofLocation::InstructionOffset(1.try_into().unwrap(), proof_data_temp) - } else { - let context_state_account = context_state_account.unwrap(); - ProofLocation::ContextStateAccount(context_state_account) - }; + // 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( @@ -2051,7 +2053,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 +2073,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( @@ -2081,12 +2083,11 @@ where ) }; - let proof_location = if let Some(proof_data_temp) = proof_data.as_ref() { - ProofLocation::InstructionOffset(1.try_into().unwrap(), proof_data_temp) - } else { - let context_state_account = context_state_account.unwrap(); - ProofLocation::ContextStateAccount(context_state_account) - }; + // 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) @@ -2173,7 +2174,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 +2195,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( @@ -2210,12 +2211,11 @@ where ) }; - let proof_location = if let Some(proof_data_temp) = proof_data.as_ref() { - ProofLocation::InstructionOffset(1.try_into().unwrap(), proof_data_temp) - } else { - let context_state_account = context_state_account.unwrap(); - ProofLocation::ContextStateAccount(context_state_account) - }; + // 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) @@ -2405,6 +2405,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 +2854,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 +2878,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( @@ -2793,12 +2897,11 @@ where ) }; - let proof_location = if let Some(proof_data_temp) = proof_data.as_ref() { - ProofLocation::InstructionOffset(1.try_into().unwrap(), proof_data_temp) - } else { - let context_state_account = context_state_account.unwrap(); - ProofLocation::ContextStateAccount(context_state_account) - }; + // 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) @@ -3493,7 +3596,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 +3616,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( @@ -3526,12 +3629,11 @@ where ) }; - let proof_location = if let Some(proof_data_temp) = proof_data.as_ref() { - ProofLocation::InstructionOffset(1.try_into().unwrap(), proof_data_temp) - } else { - let context_state_account = context_state_account.unwrap(); - ProofLocation::ContextStateAccount(context_state_account) - }; + // 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( @@ -3554,7 +3656,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 +3687,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( @@ -3598,12 +3700,11 @@ where ) }; - let proof_location = if let Some(proof_data_temp) = proof_data.as_ref() { - ProofLocation::InstructionOffset(1.try_into().unwrap(), proof_data_temp) - } else { - let context_state_account = context_state_account.unwrap(); - ProofLocation::ContextStateAccount(context_state_account) - }; + // 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( @@ -3687,6 +3788,34 @@ 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, offset) => { + Some(ProofLocation::InstructionOffset( + 1.try_into().unwrap(), + ProofData::RecordAccount(record_account, *offset), + )) + } + } + } else { + None + } + } + pub async fn withdraw_excess_lamports( &self, source: &Pubkey, @@ -4080,3 +4209,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) +} 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..ce125d5e029 100644 --- a/token/program-2022-test/tests/confidential_transfer.rs +++ b/token/program-2022-test/tests/confidential_transfer.rs @@ -15,12 +15,13 @@ use { transaction::{Transaction, TransactionError}, transport::TransportError, }, + spl_record::state::RecordData, spl_token_2022::{ error::TokenError, extension::{ confidential_transfer::{ self, - account_info::TransferAccountInfo, + account_info::{EmptyAccountAccountInfo, TransferAccountInfo, WithdrawAccountInfo}, instruction::{ CloseSplitContextStateAccounts, TransferSplitContextStateAccounts, TransferWithFeeSplitContextStateAccounts, @@ -38,7 +39,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 +446,86 @@ 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(), + RecordData::WRITABLE_START_INDEX as u32, + ); + + 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 +890,137 @@ 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(), + RecordData::WRITABLE_START_INDEX as u32, + ); + + 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 +1847,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 +1936,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 +2025,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 +2082,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 +2197,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 +2257,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 +2393,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 +2467,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..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::{ @@ -35,6 +36,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 +614,168 @@ 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(); + + 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(), + RecordData::WRITABLE_START_INDEX as u32, + ); + + 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 +906,172 @@ 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(), + 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); + 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 +1215,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 +1383,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..25fd28abd79 100644 --- a/token/program-2022-test/tests/program_test.rs +++ b/token/program-2022-test/tests/program_test.rs @@ -42,7 +42,14 @@ 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.prefer_bpf(false); + 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)); diff --git a/token/program-2022/src/extension/confidential_transfer/instruction.rs b/token/program-2022/src/extension/confidential_transfer/instruction.rs index ac73574f5a9..d8e857c3817 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,16 @@ 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::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), + ), + }; + } Ok(instructions) } @@ -827,8 +854,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 +914,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::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), + ), + }; }; Ok(instructions) @@ -946,8 +982,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 +1054,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::InstructionData(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 +1088,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 +1156,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::InstructionData(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 +1190,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 +1258,15 @@ 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::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), + ), + }; }; 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..9bad6aec922 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::InstructionData(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::InstructionData(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/proof.rs b/token/program-2022/src/proof.rs index fbb9b525a10..d0dcf9efc2d 100644 --- a/token/program-2022/src/proof.rs +++ b/token/program-2022/src/proof.rs @@ -2,20 +2,31 @@ use { 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 +34,31 @@ 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)?; + + // 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 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)) + .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 +67,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 +/// zk-token proof instruction data and proof data stored in a record account. +#[derive(Clone, Copy)] +pub enum ProofData<'a, T> { + /// The proof data + InstructionData(&'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 {