From b4ade3b464feabdcf2e35b2b7a01245272612139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dean=20=E5=88=A9=E8=BF=AA=E6=81=A9?= Date: Sun, 19 Jan 2025 01:07:28 +0800 Subject: [PATCH 01/13] Added batch instruction --- program/src/entrypoint.rs | 8 +++ program/src/processor/batch.rs | 125 +++++++++++++++++++++++++++++++++ program/src/processor/mod.rs | 1 + 3 files changed, 134 insertions(+) create mode 100644 program/src/processor/batch.rs diff --git a/program/src/entrypoint.rs b/program/src/entrypoint.rs index 0780829..d95a00e 100644 --- a/program/src/entrypoint.rs +++ b/program/src/entrypoint.rs @@ -1,3 +1,4 @@ +use batch::process_batch; use pinocchio::{ account_info::AccountInfo, default_panic_handler, no_allocator, program_entrypoint, program_error::ProgramError, pubkey::Pubkey, ProgramResult, @@ -81,6 +82,13 @@ pub fn process_instruction( process_initialize_mint2(accounts, instruction_data) } + // 255 - Batch + 255 => { + #[cfg(feature = "logging")] + pinocchio::msg!("Instruction: Batch"); + + process_batch(accounts, instruction_data) + } _ => process_remaining_instruction(accounts, instruction_data, *discriminator), } } diff --git a/program/src/processor/batch.rs b/program/src/processor/batch.rs new file mode 100644 index 0000000..63d1fb9 --- /dev/null +++ b/program/src/processor/batch.rs @@ -0,0 +1,125 @@ +use core::mem::size_of; +use pinocchio::{ + account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey, ProgramResult, +}; + +use crate::processor::{ + process_close_account, process_initialize_account3, process_initialize_mint, + process_initialize_mint2, process_mint_to, process_transfer, +}; + +macro_rules! map_accounts { + // For 1 account + ($accounts:expr, $instruction_data:expr, 1) => {{ + let (account_idx, rest) = $instruction_data + .split_first() + .ok_or(ProgramError::InvalidInstructionData)?; + *$instruction_data = rest; + let batch_accounts = [$accounts[*account_idx as usize].clone()]; + batch_accounts + }}; + + // For 2 accounts + ($accounts:expr, $instruction_data:expr, 2) => {{ + let (account_indices, rest) = $instruction_data.split_at(2); + *$instruction_data = rest; + let batch_accounts = [ + $accounts[account_indices[0] as usize].clone(), + $accounts[account_indices[1] as usize].clone(), + ]; + batch_accounts + }}; + + // For 3 accounts + ($accounts:expr, $instruction_data:expr, 3) => {{ + let (account_indices, rest) = $instruction_data.split_at(3); + *$instruction_data = rest; + let batch_accounts = [ + $accounts[account_indices[0] as usize].clone(), + $accounts[account_indices[1] as usize].clone(), + $accounts[account_indices[2] as usize].clone(), + ]; + batch_accounts + }}; +} + +#[inline(always)] +pub fn process_batch(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult { + // Validates the instruction data. + let (counter, mut instruction_data) = instruction_data + .split_first() + .ok_or(ProgramError::InvalidInstructionData)?; + + let mut discriminator; + for _ in 0..*counter { + (discriminator, instruction_data) = instruction_data + .split_first() + .ok_or(ProgramError::InvalidInstructionData)?; + match discriminator { + // 0 - InitializeMint + 0 => { + #[cfg(feature = "logging")] + pinocchio::msg!("Batch Instruction: InitializeMint"); + + let batch_accounts = map_accounts!(accounts, &mut instruction_data, 2); + process_initialize_mint(&batch_accounts, instruction_data, true)?; + if instruction_data[size_of::<(u8, Pubkey)>()] == 0 { + instruction_data = &instruction_data[size_of::<(u8, Pubkey, u8)>()..]; + } else { + instruction_data = &instruction_data[size_of::<(u8, Pubkey, u8, Pubkey)>()..]; + } + } + // 3 - Transfer + 3 => { + #[cfg(feature = "logging")] + pinocchio::msg!("Batch Instruction: Transfer"); + + let batch_accounts = map_accounts!(accounts, &mut instruction_data, 3); + process_transfer(&batch_accounts, instruction_data)?; + instruction_data = &instruction_data[size_of::()..]; + } + // 7 - MintTo + 7 => { + #[cfg(feature = "logging")] + pinocchio::msg!("Batch Instruction: MintTo"); + + let batch_accounts = map_accounts!(accounts, &mut instruction_data, 3); + process_mint_to(&batch_accounts, instruction_data)?; + instruction_data = &instruction_data[size_of::()..]; + } + // 9 - CloseAccount + 9 => { + #[cfg(feature = "logging")] + pinocchio::msg!("Batch Instruction: CloseAccount"); + + let batch_accounts = map_accounts!(accounts, &mut instruction_data, 2); + process_close_account(&batch_accounts)?; + } + 18 => { + #[cfg(feature = "logging")] + pinocchio::msg!("Batch Instruction: InitializeAccount3"); + + let batch_accounts = map_accounts!(accounts, &mut instruction_data, 3); + process_initialize_account3(&batch_accounts, instruction_data)?; + instruction_data = &instruction_data[size_of::()..]; + } + // 20 - InitializeMint2 + 20 => { + #[cfg(feature = "logging")] + pinocchio::msg!("Instruction: InitializeMint2"); + + let batch_accounts = map_accounts!(accounts, &mut instruction_data, 1); + process_initialize_mint2(&batch_accounts, instruction_data)?; + if instruction_data[size_of::<(u8, Pubkey)>()] == 0 { + instruction_data = &instruction_data[size_of::<(u8, Pubkey, u8)>()..]; + } else { + instruction_data = &instruction_data[size_of::<(u8, Pubkey, u8, Pubkey)>()..]; + } + } + _ => { + return Err(ProgramError::InvalidInstructionData); + } + } + } + Ok(()) +} diff --git a/program/src/processor/mod.rs b/program/src/processor/mod.rs index 0f0592f..09a59a6 100644 --- a/program/src/processor/mod.rs +++ b/program/src/processor/mod.rs @@ -21,6 +21,7 @@ use token_interface::{ pub mod amount_to_ui_amount; pub mod approve; pub mod approve_checked; +pub mod batch; pub mod burn; pub mod burn_checked; pub mod close_account; From 20590ec9677018ff6148d390a6e823c369f1dd48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dean=20=E5=88=A9=E8=BF=AA=E6=81=A9?= Date: Sun, 19 Jan 2025 02:12:28 +0800 Subject: [PATCH 02/13] Missing comment --- program/src/processor/batch.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/program/src/processor/batch.rs b/program/src/processor/batch.rs index 63d1fb9..506c44c 100644 --- a/program/src/processor/batch.rs +++ b/program/src/processor/batch.rs @@ -95,6 +95,7 @@ pub fn process_batch(accounts: &[AccountInfo], instruction_data: &[u8]) -> Progr let batch_accounts = map_accounts!(accounts, &mut instruction_data, 2); process_close_account(&batch_accounts)?; } + // 18 - InitializeAccount3 18 => { #[cfg(feature = "logging")] pinocchio::msg!("Batch Instruction: InitializeAccount3"); From 767e37abd099036f1e116310e610d20915e4881f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dean=20=E5=88=A9=E8=BF=AA=E6=81=A9?= Date: Wed, 29 Jan 2025 03:18:58 +0800 Subject: [PATCH 03/13] Test recursion for all priority IXs --- program/src/entrypoint.rs | 2 + program/src/lib.rs | 2 + program/src/processor/batch.rs | 124 ++--------------- .../src/processor/get_account_data_size.rs | 2 +- program/src/processor/revoke.rs | 4 +- program/src/processor/set_authority.rs | 10 +- program/src/processor/shared/approve.rs | 8 +- .../processor/shared/initialize_account.rs | 10 +- program/src/processor/shared/transfer.rs | 14 +- program/tests/batch.rs | 129 ++++++++++++++++++ 10 files changed, 170 insertions(+), 135 deletions(-) create mode 100644 program/tests/batch.rs diff --git a/program/src/entrypoint.rs b/program/src/entrypoint.rs index d95a00e..72359fa 100644 --- a/program/src/entrypoint.rs +++ b/program/src/entrypoint.rs @@ -3,6 +3,7 @@ use pinocchio::{ account_info::AccountInfo, default_panic_handler, no_allocator, program_entrypoint, program_error::ProgramError, pubkey::Pubkey, ProgramResult, }; +use pinocchio_pubkey::pubkey; use crate::processor::*; @@ -34,6 +35,7 @@ pub fn process_instruction( accounts: &[AccountInfo], instruction_data: &[u8], ) -> ProgramResult { + let (discriminator, instruction_data) = instruction_data .split_first() .ok_or(ProgramError::InvalidInstructionData)?; diff --git a/program/src/lib.rs b/program/src/lib.rs index 0bd4439..44e42b7 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -2,5 +2,7 @@ #![no_std] +#![feature(split_at_checked)] + mod entrypoint; mod processor; diff --git a/program/src/processor/batch.rs b/program/src/processor/batch.rs index 506c44c..d4f4c2a 100644 --- a/program/src/processor/batch.rs +++ b/program/src/processor/batch.rs @@ -1,47 +1,8 @@ -use core::mem::size_of; use pinocchio::{ - account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey, ProgramResult, + account_info::AccountInfo, program_error::ProgramError, ProgramResult }; -use crate::processor::{ - process_close_account, process_initialize_account3, process_initialize_mint, - process_initialize_mint2, process_mint_to, process_transfer, -}; - -macro_rules! map_accounts { - // For 1 account - ($accounts:expr, $instruction_data:expr, 1) => {{ - let (account_idx, rest) = $instruction_data - .split_first() - .ok_or(ProgramError::InvalidInstructionData)?; - *$instruction_data = rest; - let batch_accounts = [$accounts[*account_idx as usize].clone()]; - batch_accounts - }}; - - // For 2 accounts - ($accounts:expr, $instruction_data:expr, 2) => {{ - let (account_indices, rest) = $instruction_data.split_at(2); - *$instruction_data = rest; - let batch_accounts = [ - $accounts[account_indices[0] as usize].clone(), - $accounts[account_indices[1] as usize].clone(), - ]; - batch_accounts - }}; - - // For 3 accounts - ($accounts:expr, $instruction_data:expr, 3) => {{ - let (account_indices, rest) = $instruction_data.split_at(3); - *$instruction_data = rest; - let batch_accounts = [ - $accounts[account_indices[0] as usize].clone(), - $accounts[account_indices[1] as usize].clone(), - $accounts[account_indices[2] as usize].clone(), - ]; - batch_accounts - }}; -} +use crate::entrypoint::process_instruction; #[inline(always)] pub fn process_batch(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult { @@ -50,77 +11,18 @@ pub fn process_batch(accounts: &[AccountInfo], instruction_data: &[u8]) -> Progr .split_first() .ok_or(ProgramError::InvalidInstructionData)?; - let mut discriminator; + let mut lengths: &[u8]; + let mut accounts = accounts; + let mut current_accounts: &[AccountInfo]; + let mut current_instruction_data: &[u8]; + for _ in 0..*counter { - (discriminator, instruction_data) = instruction_data - .split_first() + (lengths, instruction_data) = instruction_data + .split_at_checked(2) .ok_or(ProgramError::InvalidInstructionData)?; - match discriminator { - // 0 - InitializeMint - 0 => { - #[cfg(feature = "logging")] - pinocchio::msg!("Batch Instruction: InitializeMint"); - - let batch_accounts = map_accounts!(accounts, &mut instruction_data, 2); - process_initialize_mint(&batch_accounts, instruction_data, true)?; - if instruction_data[size_of::<(u8, Pubkey)>()] == 0 { - instruction_data = &instruction_data[size_of::<(u8, Pubkey, u8)>()..]; - } else { - instruction_data = &instruction_data[size_of::<(u8, Pubkey, u8, Pubkey)>()..]; - } - } - // 3 - Transfer - 3 => { - #[cfg(feature = "logging")] - pinocchio::msg!("Batch Instruction: Transfer"); - - let batch_accounts = map_accounts!(accounts, &mut instruction_data, 3); - process_transfer(&batch_accounts, instruction_data)?; - instruction_data = &instruction_data[size_of::()..]; - } - // 7 - MintTo - 7 => { - #[cfg(feature = "logging")] - pinocchio::msg!("Batch Instruction: MintTo"); - - let batch_accounts = map_accounts!(accounts, &mut instruction_data, 3); - process_mint_to(&batch_accounts, instruction_data)?; - instruction_data = &instruction_data[size_of::()..]; - } - // 9 - CloseAccount - 9 => { - #[cfg(feature = "logging")] - pinocchio::msg!("Batch Instruction: CloseAccount"); - - let batch_accounts = map_accounts!(accounts, &mut instruction_data, 2); - process_close_account(&batch_accounts)?; - } - // 18 - InitializeAccount3 - 18 => { - #[cfg(feature = "logging")] - pinocchio::msg!("Batch Instruction: InitializeAccount3"); - - let batch_accounts = map_accounts!(accounts, &mut instruction_data, 3); - process_initialize_account3(&batch_accounts, instruction_data)?; - instruction_data = &instruction_data[size_of::()..]; - } - // 20 - InitializeMint2 - 20 => { - #[cfg(feature = "logging")] - pinocchio::msg!("Instruction: InitializeMint2"); - - let batch_accounts = map_accounts!(accounts, &mut instruction_data, 1); - process_initialize_mint2(&batch_accounts, instruction_data)?; - if instruction_data[size_of::<(u8, Pubkey)>()] == 0 { - instruction_data = &instruction_data[size_of::<(u8, Pubkey, u8)>()..]; - } else { - instruction_data = &instruction_data[size_of::<(u8, Pubkey, u8, Pubkey)>()..]; - } - } - _ => { - return Err(ProgramError::InvalidInstructionData); - } - } + (current_accounts, accounts) = accounts.split_at_checked(lengths[0].into()).ok_or(ProgramError::InvalidInstructionData)?; + (current_instruction_data, instruction_data) = instruction_data.split_at_checked(lengths[1].into()).ok_or(ProgramError::InvalidInstructionData)?; + process_instruction(&token_interface::program::ID, current_accounts, current_instruction_data)?; } Ok(()) -} +} \ No newline at end of file diff --git a/program/src/processor/get_account_data_size.rs b/program/src/processor/get_account_data_size.rs index 7693b64..0131b48 100644 --- a/program/src/processor/get_account_data_size.rs +++ b/program/src/processor/get_account_data_size.rs @@ -10,7 +10,7 @@ use super::check_account_owner; #[inline(always)] pub fn process_get_account_data_size(accounts: &[AccountInfo]) -> ProgramResult { - let [mint_info, _remaning @ ..] = accounts else { + let [mint_info, _remaining @ ..] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; diff --git a/program/src/processor/revoke.rs b/program/src/processor/revoke.rs index 6361d99..50b01dd 100644 --- a/program/src/processor/revoke.rs +++ b/program/src/processor/revoke.rs @@ -8,7 +8,7 @@ use super::validate_owner; #[inline(always)] pub fn process_revoke(accounts: &[AccountInfo], _instruction_data: &[u8]) -> ProgramResult { - let [source_account_info, owner_info, remaning @ ..] = accounts else { + let [source_account_info, owner_info, remaining @ ..] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; @@ -21,7 +21,7 @@ pub fn process_revoke(accounts: &[AccountInfo], _instruction_data: &[u8]) -> Pro return Err(TokenError::AccountFrozen.into()); } - validate_owner(&source_account.owner, owner_info, remaning)?; + validate_owner(&source_account.owner, owner_info, remaining)?; source_account.clear_delegate(); source_account.set_delegated_amount(0); diff --git a/program/src/processor/set_authority.rs b/program/src/processor/set_authority.rs index 3ad4d12..fcfa9f1 100644 --- a/program/src/processor/set_authority.rs +++ b/program/src/processor/set_authority.rs @@ -22,7 +22,7 @@ pub fn process_set_authority(accounts: &[AccountInfo], instruction_data: &[u8]) // Validates the accounts. - let [account_info, authority_info, remaning @ ..] = accounts else { + let [account_info, authority_info, remaining @ ..] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; @@ -37,7 +37,7 @@ pub fn process_set_authority(accounts: &[AccountInfo], instruction_data: &[u8]) match authority_type { AuthorityType::AccountOwner => { - validate_owner(&account.owner, authority_info, remaning)?; + validate_owner(&account.owner, authority_info, remaining)?; if let Some(authority) = new_authority { account.owner = *authority; @@ -54,7 +54,7 @@ pub fn process_set_authority(accounts: &[AccountInfo], instruction_data: &[u8]) } AuthorityType::CloseAccount => { let authority = account.close_authority().unwrap_or(&account.owner); - validate_owner(authority, authority_info, remaning)?; + validate_owner(authority, authority_info, remaining)?; if let Some(authority) = new_authority { account.set_close_authority(authority); @@ -77,7 +77,7 @@ pub fn process_set_authority(accounts: &[AccountInfo], instruction_data: &[u8]) // mint_authority. let mint_authority = mint.mint_authority().ok_or(TokenError::FixedSupply)?; - validate_owner(mint_authority, authority_info, remaning)?; + validate_owner(mint_authority, authority_info, remaining)?; if let Some(authority) = new_authority { mint.set_mint_authority(authority); @@ -92,7 +92,7 @@ pub fn process_set_authority(accounts: &[AccountInfo], instruction_data: &[u8]) .freeze_authority() .ok_or(TokenError::MintCannotFreeze)?; - validate_owner(freeze_authority, authority_info, remaning)?; + validate_owner(freeze_authority, authority_info, remaining)?; if let Some(authority) = new_authority { mint.set_freeze_authority(authority); diff --git a/program/src/processor/shared/approve.rs b/program/src/processor/shared/approve.rs index a9d2812..d5e90d5 100644 --- a/program/src/processor/shared/approve.rs +++ b/program/src/processor/shared/approve.rs @@ -17,7 +17,7 @@ pub fn process_approve( let (source_account_info, expected_mint_info, delegate_info, owner_info, remaining) = if let Some(expected_decimals) = expected_decimals { - let [source_account_info, expected_mint_info, delegate_info, owner_info, remaning @ ..] = + let [source_account_info, expected_mint_info, delegate_info, owner_info, remaining @ ..] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); @@ -28,10 +28,10 @@ pub fn process_approve( Some((expected_mint_info, expected_decimals)), delegate_info, owner_info, - remaning, + remaining, ) } else { - let [source_account_info, delegate_info, owner_info, remaning @ ..] = accounts else { + let [source_account_info, delegate_info, owner_info, remaining @ ..] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; ( @@ -39,7 +39,7 @@ pub fn process_approve( None, delegate_info, owner_info, - remaning, + remaining, ) }; diff --git a/program/src/processor/shared/initialize_account.rs b/program/src/processor/shared/initialize_account.rs index 952dd1f..8bf5f92 100644 --- a/program/src/processor/shared/initialize_account.rs +++ b/program/src/processor/shared/initialize_account.rs @@ -24,16 +24,16 @@ pub fn process_initialize_account( ) -> ProgramResult { // Accounts expected depend on whether we have the `rent_sysvar` account or not. - let (new_account_info, mint_info, owner, remaning) = if let Some(owner) = owner { - let [new_account_info, mint_info, remaning @ ..] = accounts else { + let (new_account_info, mint_info, owner, remaining) = if let Some(owner) = owner { + let [new_account_info, mint_info, remaining @ ..] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; - (new_account_info, mint_info, owner, remaning) + (new_account_info, mint_info, owner, remaining) } else { - let [new_account_info, mint_info, owner_info, remaning @ ..] = accounts else { + let [new_account_info, mint_info, owner_info, remaining @ ..] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; - (new_account_info, mint_info, owner_info.key(), remaning) + (new_account_info, mint_info, owner_info.key(), remaining) }; // Check rent-exempt status of the token account. diff --git a/program/src/processor/shared/transfer.rs b/program/src/processor/shared/transfer.rs index aafd0a1..c71a5af 100644 --- a/program/src/processor/shared/transfer.rs +++ b/program/src/processor/shared/transfer.rs @@ -20,9 +20,9 @@ pub fn process_transfer( expected_mint_info, destination_account_info, authority_info, - remaning, + remaining, ) = if let Some(decimals) = expected_decimals { - let [source_account_info, mint_info, destination_account_info, authority_info, remaning @ ..] = + let [source_account_info, mint_info, destination_account_info, authority_info, remaining @ ..] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); @@ -32,10 +32,10 @@ pub fn process_transfer( Some((mint_info, decimals)), destination_account_info, authority_info, - remaning, + remaining, ) } else { - let [source_account_info, destination_account_info, authority_info, remaning @ ..] = + let [source_account_info, destination_account_info, authority_info, remaining @ ..] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); @@ -45,7 +45,7 @@ pub fn process_transfer( None, destination_account_info, authority_info, - remaning, + remaining, ) }; @@ -114,7 +114,7 @@ pub fn process_transfer( // Validates the authority (delegate or owner). if source_account.delegate() == Some(authority_info.key()) { - validate_owner(authority_info.key(), authority_info, remaning)?; + validate_owner(authority_info.key(), authority_info, remaining)?; let delegated_amount = source_account .delegated_amount() @@ -129,7 +129,7 @@ pub fn process_transfer( } } } else { - validate_owner(&source_account.owner, authority_info, remaning)?; + validate_owner(&source_account.owner, authority_info, remaining)?; } if self_transfer || amount == 0 { diff --git a/program/tests/batch.rs b/program/tests/batch.rs new file mode 100644 index 0000000..125ca66 --- /dev/null +++ b/program/tests/batch.rs @@ -0,0 +1,129 @@ +#![cfg(feature = "test-sbf")] + +mod setup; + +use std::{collections::{BTreeMap, HashMap}, println}; + +use pinocchio::instruction; +use setup::{account, mint, TOKEN_PROGRAM_ID}; +use solana_program_test::{tokio, ProgramTest}; +use solana_sdk::{ + instruction::{AccountMeta, Instruction}, program_error::ProgramError, program_pack::Pack, pubkey::Pubkey, signature::{Keypair, Signer}, system_instruction, system_program, transaction::Transaction +}; + +fn batch_instruction(instructions: Vec) -> Result { + // Create a Vector of ordered, AccountMetas + let mut accounts: Vec = vec![]; + // Start with the batch discriminator and a length byte + + let mut data: Vec = vec![0xff, instructions.len() as u8]; + for instruction in instructions { + // Error out on non-token IX + if instruction.program_id.ne(&spl_token::ID) { + return Err(ProgramError::IncorrectProgramId) + } + + data.extend_from_slice(&[instruction.accounts.len() as u8]); + data.extend_from_slice(&[instruction.data.len() as u8]); + data.extend_from_slice(&instruction.data); + accounts.extend_from_slice(&instruction.accounts); + } + Ok(Instruction { + program_id: spl_token::ID, + data, + accounts + }) +} + +#[test_case::test_case(spl_token::ID ; "spl-token")] +// #[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")] +#[tokio::test] +async fn batch(token_program: Pubkey) { + let mut context = ProgramTest::new("token_program", token_program, None) + .start_with_context() + .await; + + let rent = context.banks_client.get_rent().await.unwrap(); + + let mint_len = spl_token::state::Mint::LEN; + let mint_rent = rent.minimum_balance(mint_len); + + let account_len = spl_token::state::Account::LEN; + let account_rent = rent.minimum_balance(account_len); + + // Create a mint + let mint_a = Keypair::new(); + let mint_authority = Keypair::new(); + let create_mint_a = system_instruction::create_account(&context.payer.pubkey(), &mint_a.pubkey(), mint_rent, mint_len as u64, &token_program); + let initialize_mint_ix = spl_token::instruction::initialize_mint(&token_program, &mint_a.pubkey(), &mint_authority.pubkey(), None, 6).unwrap(); + + // Create a mint 2 with a freeze authority + let mint_b = Keypair::new(); + let freeze_authority = Pubkey::new_unique(); + let create_mint_b = system_instruction::create_account(&context.payer.pubkey(), &mint_b.pubkey(), mint_rent, mint_len as u64, &token_program); + let initialize_mint_with_freeze_authority_ix = spl_token::instruction::initialize_mint2(&token_program, &mint_b.pubkey(), &mint_authority.pubkey(), Some(&freeze_authority), 6).unwrap(); + + // Create 2 token accounts for mint A and 1 for mint B + let owner_a = Keypair::new(); + let owner_b = Keypair::new(); + let owner_a_ta_a = Keypair::new(); + let owner_a_ta_b = Keypair::new(); + let owner_b_ta_a = Keypair::new(); + + let create_owner_a_ta_a = system_instruction::create_account(&context.payer.pubkey(), &owner_a_ta_a.pubkey(), account_rent, account_len as u64, &token_program); + let create_owner_b_ta_a = system_instruction::create_account(&context.payer.pubkey(), &owner_b_ta_a.pubkey(), account_rent, account_len as u64, &token_program); + let intialize_owner_a_ta_a = spl_token::instruction::initialize_account3(&token_program, &owner_a_ta_a.pubkey(), &mint_a.pubkey(), &owner_a.pubkey()).unwrap(); + let intialize_owner_a_ta_b = spl_token::instruction::initialize_account3(&token_program, &owner_a_ta_b.pubkey(), &mint_b.pubkey(), &owner_a.pubkey()).unwrap(); + let intialize_owner_b_ta_a = spl_token::instruction::initialize_account3(&token_program, &owner_b_ta_a.pubkey(), &mint_a.pubkey(), &owner_b.pubkey()).unwrap(); + + // Mint Token A to Owner A + let mint_token_a_to_owner_a = spl_token::instruction::mint_to(&token_program, &mint_a.pubkey(), &owner_a_ta_a.pubkey(), &mint_authority.pubkey(), &[], 1_000_000).unwrap(); + + // Transfer Token A from Owner A to Owner B + let transfer_token_a_to_owner_b = spl_token::instruction::transfer(&token_program, &owner_a_ta_a.pubkey(), &owner_b_ta_a.pubkey(), &owner_a.pubkey(), &[], 1_000_000).unwrap(); + + // Close Token A + let close_owner_a_ta_a = spl_token::instruction::close_account(&token_program, &owner_a_ta_a.pubkey(), &owner_a.pubkey(), &owner_a.pubkey(), &[]).unwrap(); + + let batch_ix = batch_instruction(vec![ + initialize_mint_ix, + initialize_mint_with_freeze_authority_ix, + intialize_owner_a_ta_a, + intialize_owner_b_ta_a, + mint_token_a_to_owner_a, + transfer_token_a_to_owner_b, + close_owner_a_ta_a + ]).unwrap(); + + println!("{:?}", batch_ix); + + let tx = Transaction::new_signed_with_payer( + &[ + create_mint_a, + create_mint_b, + create_owner_a_ta_a, + create_owner_b_ta_a, + batch_ix + ], + Some(&context.payer.pubkey()), + &vec![&context.payer, &mint_a, &mint_b, &owner_a_ta_a, &owner_b_ta_a, &mint_authority, &owner_a], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + let mint_a_account = context.banks_client.get_account(mint_a.pubkey()).await.unwrap(); + assert!(mint_a_account.is_some()); + let mint_a_account = spl_token::state::Mint::unpack(&mint_a_account.unwrap().data).unwrap(); + assert_eq!(mint_a_account.supply, 1000000); + + let mint_b_account = context.banks_client.get_account(mint_b.pubkey()).await.unwrap(); + assert!(mint_b_account.is_some()); + let mint_b_account = spl_token::state::Mint::unpack(&mint_b_account.unwrap().data).unwrap(); + assert_eq!(mint_b_account.supply, 0); + + let owner_b_ta_a_account = context.banks_client.get_account(owner_b_ta_a.pubkey()).await.unwrap(); + assert!(owner_b_ta_a_account.is_some()); + let owner_b_ta_a_account = spl_token::state::Account::unpack(&owner_b_ta_a_account.unwrap().data).unwrap(); + assert_eq!(owner_b_ta_a_account.amount, 1000000); + +} From 2dfc70ec0a296724668b09dc83b811a8f27d381e Mon Sep 17 00:00:00 2001 From: febo Date: Wed, 29 Jan 2025 03:10:08 +0000 Subject: [PATCH 04/13] Unused import --- program/src/entrypoint.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/program/src/entrypoint.rs b/program/src/entrypoint.rs index 72359fa..d95a00e 100644 --- a/program/src/entrypoint.rs +++ b/program/src/entrypoint.rs @@ -3,7 +3,6 @@ use pinocchio::{ account_info::AccountInfo, default_panic_handler, no_allocator, program_entrypoint, program_error::ProgramError, pubkey::Pubkey, ProgramResult, }; -use pinocchio_pubkey::pubkey; use crate::processor::*; @@ -35,7 +34,6 @@ pub fn process_instruction( accounts: &[AccountInfo], instruction_data: &[u8], ) -> ProgramResult { - let (discriminator, instruction_data) = instruction_data .split_first() .ok_or(ProgramError::InvalidInstructionData)?; From e297454fdaa0e8ad8b3f6223a51704b63dc3f933 Mon Sep 17 00:00:00 2001 From: febo Date: Wed, 29 Jan 2025 03:10:50 +0000 Subject: [PATCH 05/13] Refactor --- program/src/lib.rs | 2 -- program/src/processor/batch.rs | 66 ++++++++++++++++++++++------------ 2 files changed, 43 insertions(+), 25 deletions(-) diff --git a/program/src/lib.rs b/program/src/lib.rs index 44e42b7..0bd4439 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -2,7 +2,5 @@ #![no_std] -#![feature(split_at_checked)] - mod entrypoint; mod processor; diff --git a/program/src/processor/batch.rs b/program/src/processor/batch.rs index d4f4c2a..ecd9d62 100644 --- a/program/src/processor/batch.rs +++ b/program/src/processor/batch.rs @@ -1,28 +1,48 @@ -use pinocchio::{ - account_info::AccountInfo, program_error::ProgramError, ProgramResult -}; +use pinocchio::{account_info::AccountInfo, program_error::ProgramError, ProgramResult}; use crate::entrypoint::process_instruction; -#[inline(always)] -pub fn process_batch(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult { - // Validates the instruction data. - let (counter, mut instruction_data) = instruction_data - .split_first() - .ok_or(ProgramError::InvalidInstructionData)?; - - let mut lengths: &[u8]; - let mut accounts = accounts; - let mut current_accounts: &[AccountInfo]; - let mut current_instruction_data: &[u8]; - - for _ in 0..*counter { - (lengths, instruction_data) = instruction_data - .split_at_checked(2) - .ok_or(ProgramError::InvalidInstructionData)?; - (current_accounts, accounts) = accounts.split_at_checked(lengths[0].into()).ok_or(ProgramError::InvalidInstructionData)?; - (current_instruction_data, instruction_data) = instruction_data.split_at_checked(lengths[1].into()).ok_or(ProgramError::InvalidInstructionData)?; - process_instruction(&token_interface::program::ID, current_accounts, current_instruction_data)?; +/// The size of the batch instruction header. +/// +/// The header of each instruction consists of two `u8` values: +/// * number of the accounts +/// * length of the instruction data +const IX_HEADER_SIZE: usize = 2; + +pub fn process_batch(mut accounts: &[AccountInfo], mut instruction_data: &[u8]) -> ProgramResult { + loop { + // Validates the instruction data and accounts offset. + + match instruction_data.len() { + 0 => break, + n if n < IX_HEADER_SIZE => { + // The instruction data must have at least two bytes. + return Err(ProgramError::InvalidInstructionData); + } + _ => (), + } + // SAFETY: The instruction data is guaranteed to have at least two bytes. + let expected_accounts = unsafe { *instruction_data.get_unchecked(0) as usize }; + let data_offset = IX_HEADER_SIZE + unsafe { *instruction_data.get_unchecked(1) as usize }; + if instruction_data.len() < data_offset { + return Err(ProgramError::InvalidInstructionData); + } + + if accounts.len() < expected_accounts { + return Err(ProgramError::NotEnoughAccountKeys); + } + + // Process the instruction. + + process_instruction( + &token_interface::program::ID, + &accounts[..expected_accounts], + &instruction_data[IX_HEADER_SIZE..data_offset], + )?; + + accounts = &accounts[expected_accounts..]; + instruction_data = &instruction_data[data_offset..]; } + Ok(()) -} \ No newline at end of file +} From c59702d2cbc87c9a2f717d6f7298feb8d16e14c9 Mon Sep 17 00:00:00 2001 From: febo Date: Wed, 29 Jan 2025 03:11:54 +0000 Subject: [PATCH 06/13] Remove instruction counter --- program/tests/batch.rs | 171 +++++++++++++++++++++++++++++++++-------- 1 file changed, 140 insertions(+), 31 deletions(-) diff --git a/program/tests/batch.rs b/program/tests/batch.rs index 125ca66..361858c 100644 --- a/program/tests/batch.rs +++ b/program/tests/batch.rs @@ -2,36 +2,45 @@ mod setup; -use std::{collections::{BTreeMap, HashMap}, println}; +use std::{ + collections::{BTreeMap, HashMap}, + println, +}; use pinocchio::instruction; use setup::{account, mint, TOKEN_PROGRAM_ID}; use solana_program_test::{tokio, ProgramTest}; use solana_sdk::{ - instruction::{AccountMeta, Instruction}, program_error::ProgramError, program_pack::Pack, pubkey::Pubkey, signature::{Keypair, Signer}, system_instruction, system_program, transaction::Transaction + instruction::{AccountMeta, Instruction}, + program_error::ProgramError, + program_pack::Pack, + pubkey::Pubkey, + signature::{Keypair, Signer}, + system_instruction, system_program, + transaction::Transaction, }; fn batch_instruction(instructions: Vec) -> Result { // Create a Vector of ordered, AccountMetas - let mut accounts: Vec = vec![]; + let mut accounts: Vec = vec![]; // Start with the batch discriminator and a length byte - let mut data: Vec = vec![0xff, instructions.len() as u8]; + let mut data: Vec = vec![0xff]; for instruction in instructions { // Error out on non-token IX if instruction.program_id.ne(&spl_token::ID) { - return Err(ProgramError::IncorrectProgramId) + return Err(ProgramError::IncorrectProgramId); } - + data.extend_from_slice(&[instruction.accounts.len() as u8]); data.extend_from_slice(&[instruction.data.len() as u8]); - data.extend_from_slice(&instruction.data); - accounts.extend_from_slice(&instruction.accounts); + data.extend_from_slice(&instruction.data); + accounts.extend_from_slice(&instruction.accounts); } Ok(Instruction { program_id: spl_token::ID, data, - accounts + accounts, }) } @@ -54,14 +63,40 @@ async fn batch(token_program: Pubkey) { // Create a mint let mint_a = Keypair::new(); let mint_authority = Keypair::new(); - let create_mint_a = system_instruction::create_account(&context.payer.pubkey(), &mint_a.pubkey(), mint_rent, mint_len as u64, &token_program); - let initialize_mint_ix = spl_token::instruction::initialize_mint(&token_program, &mint_a.pubkey(), &mint_authority.pubkey(), None, 6).unwrap(); - + let create_mint_a = system_instruction::create_account( + &context.payer.pubkey(), + &mint_a.pubkey(), + mint_rent, + mint_len as u64, + &token_program, + ); + let initialize_mint_ix = spl_token::instruction::initialize_mint( + &token_program, + &mint_a.pubkey(), + &mint_authority.pubkey(), + None, + 6, + ) + .unwrap(); + // Create a mint 2 with a freeze authority let mint_b = Keypair::new(); let freeze_authority = Pubkey::new_unique(); - let create_mint_b = system_instruction::create_account(&context.payer.pubkey(), &mint_b.pubkey(), mint_rent, mint_len as u64, &token_program); - let initialize_mint_with_freeze_authority_ix = spl_token::instruction::initialize_mint2(&token_program, &mint_b.pubkey(), &mint_authority.pubkey(), Some(&freeze_authority), 6).unwrap(); + let create_mint_b = system_instruction::create_account( + &context.payer.pubkey(), + &mint_b.pubkey(), + mint_rent, + mint_len as u64, + &token_program, + ); + let initialize_mint_with_freeze_authority_ix = spl_token::instruction::initialize_mint2( + &token_program, + &mint_b.pubkey(), + &mint_authority.pubkey(), + Some(&freeze_authority), + 6, + ) + .unwrap(); // Create 2 token accounts for mint A and 1 for mint B let owner_a = Keypair::new(); @@ -70,20 +105,73 @@ async fn batch(token_program: Pubkey) { let owner_a_ta_b = Keypair::new(); let owner_b_ta_a = Keypair::new(); - let create_owner_a_ta_a = system_instruction::create_account(&context.payer.pubkey(), &owner_a_ta_a.pubkey(), account_rent, account_len as u64, &token_program); - let create_owner_b_ta_a = system_instruction::create_account(&context.payer.pubkey(), &owner_b_ta_a.pubkey(), account_rent, account_len as u64, &token_program); - let intialize_owner_a_ta_a = spl_token::instruction::initialize_account3(&token_program, &owner_a_ta_a.pubkey(), &mint_a.pubkey(), &owner_a.pubkey()).unwrap(); - let intialize_owner_a_ta_b = spl_token::instruction::initialize_account3(&token_program, &owner_a_ta_b.pubkey(), &mint_b.pubkey(), &owner_a.pubkey()).unwrap(); - let intialize_owner_b_ta_a = spl_token::instruction::initialize_account3(&token_program, &owner_b_ta_a.pubkey(), &mint_a.pubkey(), &owner_b.pubkey()).unwrap(); + let create_owner_a_ta_a = system_instruction::create_account( + &context.payer.pubkey(), + &owner_a_ta_a.pubkey(), + account_rent, + account_len as u64, + &token_program, + ); + let create_owner_b_ta_a = system_instruction::create_account( + &context.payer.pubkey(), + &owner_b_ta_a.pubkey(), + account_rent, + account_len as u64, + &token_program, + ); + let intialize_owner_a_ta_a = spl_token::instruction::initialize_account3( + &token_program, + &owner_a_ta_a.pubkey(), + &mint_a.pubkey(), + &owner_a.pubkey(), + ) + .unwrap(); + let intialize_owner_a_ta_b = spl_token::instruction::initialize_account3( + &token_program, + &owner_a_ta_b.pubkey(), + &mint_b.pubkey(), + &owner_a.pubkey(), + ) + .unwrap(); + let intialize_owner_b_ta_a = spl_token::instruction::initialize_account3( + &token_program, + &owner_b_ta_a.pubkey(), + &mint_a.pubkey(), + &owner_b.pubkey(), + ) + .unwrap(); // Mint Token A to Owner A - let mint_token_a_to_owner_a = spl_token::instruction::mint_to(&token_program, &mint_a.pubkey(), &owner_a_ta_a.pubkey(), &mint_authority.pubkey(), &[], 1_000_000).unwrap(); + let mint_token_a_to_owner_a = spl_token::instruction::mint_to( + &token_program, + &mint_a.pubkey(), + &owner_a_ta_a.pubkey(), + &mint_authority.pubkey(), + &[], + 1_000_000, + ) + .unwrap(); // Transfer Token A from Owner A to Owner B - let transfer_token_a_to_owner_b = spl_token::instruction::transfer(&token_program, &owner_a_ta_a.pubkey(), &owner_b_ta_a.pubkey(), &owner_a.pubkey(), &[], 1_000_000).unwrap(); + let transfer_token_a_to_owner_b = spl_token::instruction::transfer( + &token_program, + &owner_a_ta_a.pubkey(), + &owner_b_ta_a.pubkey(), + &owner_a.pubkey(), + &[], + 1_000_000, + ) + .unwrap(); // Close Token A - let close_owner_a_ta_a = spl_token::instruction::close_account(&token_program, &owner_a_ta_a.pubkey(), &owner_a.pubkey(), &owner_a.pubkey(), &[]).unwrap(); + let close_owner_a_ta_a = spl_token::instruction::close_account( + &token_program, + &owner_a_ta_a.pubkey(), + &owner_a.pubkey(), + &owner_a.pubkey(), + &[], + ) + .unwrap(); let batch_ix = batch_instruction(vec![ initialize_mint_ix, @@ -92,8 +180,9 @@ async fn batch(token_program: Pubkey) { intialize_owner_b_ta_a, mint_token_a_to_owner_a, transfer_token_a_to_owner_b, - close_owner_a_ta_a - ]).unwrap(); + close_owner_a_ta_a, + ]) + .unwrap(); println!("{:?}", batch_ix); @@ -103,27 +192,47 @@ async fn batch(token_program: Pubkey) { create_mint_b, create_owner_a_ta_a, create_owner_b_ta_a, - batch_ix + batch_ix, ], Some(&context.payer.pubkey()), - &vec![&context.payer, &mint_a, &mint_b, &owner_a_ta_a, &owner_b_ta_a, &mint_authority, &owner_a], + &vec![ + &context.payer, + &mint_a, + &mint_b, + &owner_a_ta_a, + &owner_b_ta_a, + &mint_authority, + &owner_a, + ], context.last_blockhash, ); context.banks_client.process_transaction(tx).await.unwrap(); - let mint_a_account = context.banks_client.get_account(mint_a.pubkey()).await.unwrap(); + let mint_a_account = context + .banks_client + .get_account(mint_a.pubkey()) + .await + .unwrap(); assert!(mint_a_account.is_some()); let mint_a_account = spl_token::state::Mint::unpack(&mint_a_account.unwrap().data).unwrap(); assert_eq!(mint_a_account.supply, 1000000); - let mint_b_account = context.banks_client.get_account(mint_b.pubkey()).await.unwrap(); + let mint_b_account = context + .banks_client + .get_account(mint_b.pubkey()) + .await + .unwrap(); assert!(mint_b_account.is_some()); let mint_b_account = spl_token::state::Mint::unpack(&mint_b_account.unwrap().data).unwrap(); assert_eq!(mint_b_account.supply, 0); - let owner_b_ta_a_account = context.banks_client.get_account(owner_b_ta_a.pubkey()).await.unwrap(); + let owner_b_ta_a_account = context + .banks_client + .get_account(owner_b_ta_a.pubkey()) + .await + .unwrap(); assert!(owner_b_ta_a_account.is_some()); - let owner_b_ta_a_account = spl_token::state::Account::unpack(&owner_b_ta_a_account.unwrap().data).unwrap(); + let owner_b_ta_a_account = + spl_token::state::Account::unpack(&owner_b_ta_a_account.unwrap().data).unwrap(); assert_eq!(owner_b_ta_a_account.amount, 1000000); - } From e7a59b6ccf005b34ea8267d3de92f14a1aa95020 Mon Sep 17 00:00:00 2001 From: febo Date: Thu, 30 Jan 2025 16:53:38 +0000 Subject: [PATCH 07/13] Cosmetics --- program/src/processor/batch.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/program/src/processor/batch.rs b/program/src/processor/batch.rs index ecd9d62..2aa06a9 100644 --- a/program/src/processor/batch.rs +++ b/program/src/processor/batch.rs @@ -21,9 +21,11 @@ pub fn process_batch(mut accounts: &[AccountInfo], mut instruction_data: &[u8]) } _ => (), } + // SAFETY: The instruction data is guaranteed to have at least two bytes. let expected_accounts = unsafe { *instruction_data.get_unchecked(0) as usize }; let data_offset = IX_HEADER_SIZE + unsafe { *instruction_data.get_unchecked(1) as usize }; + if instruction_data.len() < data_offset { return Err(ProgramError::InvalidInstructionData); } From 14774fc1b4d3f7ed6d93e790ee584f940691a88b Mon Sep 17 00:00:00 2001 From: febo Date: Fri, 31 Jan 2025 16:19:54 +0000 Subject: [PATCH 08/13] Tweaks --- program/src/entrypoint.rs | 7 ++++--- program/src/processor/batch.rs | 15 ++++++++------- program/tests/batch.rs | 31 +++++++++---------------------- 3 files changed, 21 insertions(+), 32 deletions(-) diff --git a/program/src/entrypoint.rs b/program/src/entrypoint.rs index d95a00e..e1790dc 100644 --- a/program/src/entrypoint.rs +++ b/program/src/entrypoint.rs @@ -28,15 +28,16 @@ default_panic_handler!(); /// - `9`: `CloseAccount` /// - `18`: `InitializeAccount3` /// - `20`: `InitializeMint2` +/// - `255`: `Batch` #[inline(always)] pub fn process_instruction( _program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8], ) -> ProgramResult { - let (discriminator, instruction_data) = instruction_data - .split_first() - .ok_or(ProgramError::InvalidInstructionData)?; + let [discriminator, instruction_data @ ..] = instruction_data else { + return Err(ProgramError::InvalidInstructionData); + }; match *discriminator { // 0 - InitializeMint diff --git a/program/src/processor/batch.rs b/program/src/processor/batch.rs index 2aa06a9..ac64c43 100644 --- a/program/src/processor/batch.rs +++ b/program/src/processor/batch.rs @@ -13,13 +13,9 @@ pub fn process_batch(mut accounts: &[AccountInfo], mut instruction_data: &[u8]) loop { // Validates the instruction data and accounts offset. - match instruction_data.len() { - 0 => break, - n if n < IX_HEADER_SIZE => { - // The instruction data must have at least two bytes. - return Err(ProgramError::InvalidInstructionData); - } - _ => (), + if instruction_data.len() < IX_HEADER_SIZE { + // The instruction data must have at least two bytes. + return Err(ProgramError::InvalidInstructionData); } // SAFETY: The instruction data is guaranteed to have at least two bytes. @@ -42,6 +38,11 @@ pub fn process_batch(mut accounts: &[AccountInfo], mut instruction_data: &[u8]) &instruction_data[IX_HEADER_SIZE..data_offset], )?; + if data_offset == instruction_data.len() { + // The batch is complete. + break; + } + accounts = &accounts[expected_accounts..]; instruction_data = &instruction_data[data_offset..]; } diff --git a/program/tests/batch.rs b/program/tests/batch.rs index 361858c..e94416c 100644 --- a/program/tests/batch.rs +++ b/program/tests/batch.rs @@ -2,13 +2,6 @@ mod setup; -use std::{ - collections::{BTreeMap, HashMap}, - println, -}; - -use pinocchio::instruction; -use setup::{account, mint, TOKEN_PROGRAM_ID}; use solana_program_test::{tokio, ProgramTest}; use solana_sdk::{ instruction::{AccountMeta, Instruction}, @@ -16,27 +9,29 @@ use solana_sdk::{ program_pack::Pack, pubkey::Pubkey, signature::{Keypair, Signer}, - system_instruction, system_program, + system_instruction, transaction::Transaction, }; fn batch_instruction(instructions: Vec) -> Result { - // Create a Vector of ordered, AccountMetas + // Create a `Vec` of ordered `AccountMeta`s let mut accounts: Vec = vec![]; - // Start with the batch discriminator and a length byte - + // Start with the batch discriminator let mut data: Vec = vec![0xff]; + for instruction in instructions { - // Error out on non-token IX + // Error out on non-token IX. if instruction.program_id.ne(&spl_token::ID) { return Err(ProgramError::IncorrectProgramId); } - data.extend_from_slice(&[instruction.accounts.len() as u8]); - data.extend_from_slice(&[instruction.data.len() as u8]); + data.push(instruction.accounts.len() as u8); + data.push(instruction.data.len() as u8); + data.extend_from_slice(&instruction.data); accounts.extend_from_slice(&instruction.accounts); } + Ok(Instruction { program_id: spl_token::ID, data, @@ -102,7 +97,6 @@ async fn batch(token_program: Pubkey) { let owner_a = Keypair::new(); let owner_b = Keypair::new(); let owner_a_ta_a = Keypair::new(); - let owner_a_ta_b = Keypair::new(); let owner_b_ta_a = Keypair::new(); let create_owner_a_ta_a = system_instruction::create_account( @@ -126,13 +120,6 @@ async fn batch(token_program: Pubkey) { &owner_a.pubkey(), ) .unwrap(); - let intialize_owner_a_ta_b = spl_token::instruction::initialize_account3( - &token_program, - &owner_a_ta_b.pubkey(), - &mint_b.pubkey(), - &owner_a.pubkey(), - ) - .unwrap(); let intialize_owner_b_ta_a = spl_token::instruction::initialize_account3( &token_program, &owner_b_ta_a.pubkey(), From 463d630a9ed1f888becf593c0518b4039222ae5c Mon Sep 17 00:00:00 2001 From: febo Date: Fri, 31 Jan 2025 16:23:19 +0000 Subject: [PATCH 09/13] Fix typo --- program/src/processor/shared/initialize_account.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/program/src/processor/shared/initialize_account.rs b/program/src/processor/shared/initialize_account.rs index 8bf5f92..796e82e 100644 --- a/program/src/processor/shared/initialize_account.rs +++ b/program/src/processor/shared/initialize_account.rs @@ -41,7 +41,9 @@ pub fn process_initialize_account( let new_account_info_data_len = new_account_info.data_len(); let minimum_balance = if rent_sysvar_account { - let rent_sysvar_info = remaning.first().ok_or(ProgramError::NotEnoughAccountKeys)?; + let rent_sysvar_info = remaining + .first() + .ok_or(ProgramError::NotEnoughAccountKeys)?; // SAFETY: single immutable borrow to `rent_sysvar_info`; account ID and length are // checked by `from_account_info_unchecked`. let rent = unsafe { Rent::from_account_info_unchecked(rent_sysvar_info)? }; From 3c2cf6400a599dca2bfd16a09e7b0a6ea78cb117 Mon Sep 17 00:00:00 2001 From: febo Date: Fri, 31 Jan 2025 16:23:37 +0000 Subject: [PATCH 10/13] Clippy --- program/tests/setup/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/program/tests/setup/mod.rs b/program/tests/setup/mod.rs index e79b8ce..801eb56 100644 --- a/program/tests/setup/mod.rs +++ b/program/tests/setup/mod.rs @@ -5,4 +5,5 @@ pub mod account; #[allow(dead_code)] pub mod mint; +#[allow(dead_code)] pub const TOKEN_PROGRAM_ID: Pubkey = Pubkey::new_from_array(token_interface::program::ID); From 8df8ddb82f248df513bc7176e6cefea82660017a Mon Sep 17 00:00:00 2001 From: febo Date: Sun, 2 Feb 2025 10:14:27 +0000 Subject: [PATCH 11/13] Tweak account state value --- interface/src/state/account.rs | 6 +++--- program/src/processor/shared/initialize_account.rs | 2 +- program/src/processor/shared/toggle_account_state.rs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/interface/src/state/account.rs b/interface/src/state/account.rs index 0f0ba64..761896b 100644 --- a/interface/src/state/account.rs +++ b/interface/src/state/account.rs @@ -26,7 +26,7 @@ pub struct Account { delegate: COption, /// The account's state. - pub state: AccountState, + pub state: u8, /// Indicates whether this account represents a native token or not. is_native: [u8; 4], @@ -131,7 +131,7 @@ impl Account { #[inline(always)] pub fn is_frozen(&self) -> bool { - self.state == AccountState::Frozen + self.state == AccountState::Frozen as u8 } #[inline(always)] @@ -147,6 +147,6 @@ impl RawType for Account { impl Initializable for Account { #[inline(always)] fn is_initialized(&self) -> bool { - self.state != AccountState::Uninitialized + self.state != AccountState::Uninitialized as u8 } } diff --git a/program/src/processor/shared/initialize_account.rs b/program/src/processor/shared/initialize_account.rs index 796e82e..ba418e5 100644 --- a/program/src/processor/shared/initialize_account.rs +++ b/program/src/processor/shared/initialize_account.rs @@ -78,7 +78,7 @@ pub fn process_initialize_account( }; } - account.state = AccountState::Initialized; + account.state = AccountState::Initialized as u8; account.mint = *mint_info.key(); account.owner = *owner; diff --git a/program/src/processor/shared/toggle_account_state.rs b/program/src/processor/shared/toggle_account_state.rs index c9ff11a..8231586 100644 --- a/program/src/processor/shared/toggle_account_state.rs +++ b/program/src/processor/shared/toggle_account_state.rs @@ -37,9 +37,9 @@ pub fn process_toggle_account_state(accounts: &[AccountInfo], freeze: bool) -> P }?; source_account.state = if freeze { - AccountState::Frozen + AccountState::Frozen as u8 } else { - AccountState::Initialized + AccountState::Initialized as u8 }; Ok(()) From 55ae87b27541793feb8bc237e6a8ca3c1272d0d5 Mon Sep 17 00:00:00 2001 From: febo Date: Sun, 2 Feb 2025 10:14:46 +0000 Subject: [PATCH 12/13] Add batch instruction --- interface/src/instruction.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/interface/src/instruction.rs b/interface/src/instruction.rs index a829e9a..45e5bd7 100644 --- a/interface/src/instruction.rs +++ b/interface/src/instruction.rs @@ -5,7 +5,7 @@ use pinocchio::{program_error::ProgramError, pubkey::Pubkey}; use crate::error::TokenError; /// Instructions supported by the token program. -#[repr(C)] +#[repr(C, u8)] #[derive(Clone, Debug, PartialEq)] pub enum TokenInstruction<'a> { /// Initializes a new mint and optionally deposits all the newly minted @@ -477,6 +477,21 @@ pub enum TokenInstruction<'a> { /// The ui_amount of tokens to reformat. ui_amount: &'a str, }, + + /// Executes a batch of instructions. The instructions to be executed are specified + /// in sequence on the instruction data. Each instruction provides: + /// - `u8`: number of accounts + /// - `u8`: instruction data length (includes the discriminator) + /// - `u8`: instruction discriminator + /// - `[u8]`: instruction data + /// + /// Accounts follow a similar pattern, where accounts for each instruction are + /// specified in sequence. Therefore, the number of accounts expected by this + /// instruction is variable – i.e., it depends on the instructions provided. + /// + /// Both the number of accountsa and instruction data length are used to identify + /// the slice of accounts and instruction data for each instruction. + Batch = 255, // Any new variants also need to be added to program-2022 `TokenInstruction`, so that the // latter remains a superset of this instruction set. New variants also need to be added to // token/js/src/instructions/types.ts to maintain @solana/spl-token compatibility From 297c49ed71797611d603cc68ff156518f8ff0042 Mon Sep 17 00:00:00 2001 From: febo Date: Sun, 2 Feb 2025 10:16:17 +0000 Subject: [PATCH 13/13] Reduce batch compute units --- program/src/entrypoint.rs | 81 +++++++++++++++++++--------------- program/src/processor/batch.rs | 17 ++++--- program/src/processor/mod.rs | 1 + 3 files changed, 56 insertions(+), 43 deletions(-) diff --git a/program/src/entrypoint.rs b/program/src/entrypoint.rs index e1790dc..2429fd6 100644 --- a/program/src/entrypoint.rs +++ b/program/src/entrypoint.rs @@ -1,4 +1,3 @@ -use batch::process_batch; use pinocchio::{ account_info::AccountInfo, default_panic_handler, no_allocator, program_entrypoint, program_error::ProgramError, pubkey::Pubkey, ProgramResult, @@ -12,6 +11,27 @@ no_allocator!(); // Use the default panic handler. default_panic_handler!(); +#[inline(always)] +pub fn process_instruction( + _program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + let [discriminator, instruction_data @ ..] = instruction_data else { + return Err(ProgramError::InvalidInstructionData); + }; + + if *discriminator == 255 { + // 255 - Batch + #[cfg(feature = "logging")] + pinocchio::msg!("Instruction: Batch"); + + return process_batch(accounts, instruction_data); + } + + inner_process_instruction(accounts, instruction_data, *discriminator) +} + /// Process an instruction. /// /// The processor of the token program is divided into two parts to reduce the overhead @@ -22,24 +42,21 @@ default_panic_handler!(); /// /// Instructions on the first part of the processor: /// -/// - `0`: `InitializeMint` -/// - `3`: `Transfer` -/// - `7`: `MintTo` -/// - `9`: `CloseAccount` +/// - `0`: `InitializeMint` +/// - `1`: `InitializeAccount` +/// - `3`: `Transfer` +/// - `7`: `MintTo` +/// - `9`: `CloseAccount` +/// - `18`: `InitializeAccount2` /// - `18`: `InitializeAccount3` /// - `20`: `InitializeMint2` -/// - `255`: `Batch` #[inline(always)] -pub fn process_instruction( - _program_id: &Pubkey, +pub fn inner_process_instruction( accounts: &[AccountInfo], instruction_data: &[u8], + discriminator: u8, ) -> ProgramResult { - let [discriminator, instruction_data @ ..] = instruction_data else { - return Err(ProgramError::InvalidInstructionData); - }; - - match *discriminator { + match discriminator { // 0 - InitializeMint 0 => { #[cfg(feature = "logging")] @@ -47,7 +64,13 @@ pub fn process_instruction( process_initialize_mint(accounts, instruction_data, true) } + // 1 - InitializeAccount + 1 => { + #[cfg(feature = "logging")] + pinocchio::msg!("Instruction: InitializeAccount"); + process_initialize_account(accounts) + } // 3 - Transfer 3 => { #[cfg(feature = "logging")] @@ -69,6 +92,13 @@ pub fn process_instruction( process_close_account(accounts) } + // 16 - InitializeAccount2 + 16 => { + #[cfg(feature = "logging")] + pinocchio::msg!("Instruction: InitializeAccount2"); + + process_initialize_account2(accounts, instruction_data) + } // 18 - InitializeAccount3 18 => { #[cfg(feature = "logging")] @@ -83,14 +113,7 @@ pub fn process_instruction( process_initialize_mint2(accounts, instruction_data) } - // 255 - Batch - 255 => { - #[cfg(feature = "logging")] - pinocchio::msg!("Instruction: Batch"); - - process_batch(accounts, instruction_data) - } - _ => process_remaining_instruction(accounts, instruction_data, *discriminator), + _ => inner_process_remaining_instruction(accounts, instruction_data, discriminator), } } @@ -99,19 +122,12 @@ pub fn process_instruction( /// This function is called by the `process_instruction` function if the discriminator /// does not match any of the common instructions. This function is used to reduce the /// overhead of having a large `match` statement in the `process_instruction` function. -fn process_remaining_instruction( +fn inner_process_remaining_instruction( accounts: &[AccountInfo], instruction_data: &[u8], discriminator: u8, ) -> ProgramResult { match discriminator { - // 1 - InitializeAccount - 1 => { - #[cfg(feature = "logging")] - pinocchio::msg!("Instruction: InitializeAccount"); - - process_initialize_account(accounts) - } // 2 - InitializeMultisig 2 => { #[cfg(feature = "logging")] @@ -189,13 +205,6 @@ fn process_remaining_instruction( process_burn_checked(accounts, instruction_data) } - // 16 - InitializeAccount2 - 16 => { - #[cfg(feature = "logging")] - pinocchio::msg!("Instruction: InitializeAccount2"); - - process_initialize_account2(accounts, instruction_data) - } // 17 - SyncNative 17 => { #[cfg(feature = "logging")] diff --git a/program/src/processor/batch.rs b/program/src/processor/batch.rs index ac64c43..95669d0 100644 --- a/program/src/processor/batch.rs +++ b/program/src/processor/batch.rs @@ -1,6 +1,6 @@ use pinocchio::{account_info::AccountInfo, program_error::ProgramError, ProgramResult}; -use crate::entrypoint::process_instruction; +use crate::entrypoint::inner_process_instruction; /// The size of the batch instruction header. /// @@ -18,11 +18,12 @@ pub fn process_batch(mut accounts: &[AccountInfo], mut instruction_data: &[u8]) return Err(ProgramError::InvalidInstructionData); } - // SAFETY: The instruction data is guaranteed to have at least two bytes. + // SAFETY: The instruction data is guaranteed to have at least two bytes (header) + // + one byte (discriminator). let expected_accounts = unsafe { *instruction_data.get_unchecked(0) as usize }; let data_offset = IX_HEADER_SIZE + unsafe { *instruction_data.get_unchecked(1) as usize }; - if instruction_data.len() < data_offset { + if instruction_data.len() < data_offset || data_offset == 0 { return Err(ProgramError::InvalidInstructionData); } @@ -32,10 +33,12 @@ pub fn process_batch(mut accounts: &[AccountInfo], mut instruction_data: &[u8]) // Process the instruction. - process_instruction( - &token_interface::program::ID, - &accounts[..expected_accounts], - &instruction_data[IX_HEADER_SIZE..data_offset], + // SAFETY: The instruction data and accounts lengths are already validated so all + // the slices are guaranteed to be valid. + inner_process_instruction( + unsafe { accounts.get_unchecked(..expected_accounts) }, + unsafe { instruction_data.get_unchecked(IX_HEADER_SIZE + 1..data_offset) }, + unsafe { *instruction_data.get_unchecked(IX_HEADER_SIZE) }, )?; if data_offset == instruction_data.len() { diff --git a/program/src/processor/mod.rs b/program/src/processor/mod.rs index 09a59a6..f2efa64 100644 --- a/program/src/processor/mod.rs +++ b/program/src/processor/mod.rs @@ -50,6 +50,7 @@ pub mod shared; pub use amount_to_ui_amount::process_amount_to_ui_amount; pub use approve::process_approve; pub use approve_checked::process_approve_checked; +pub use batch::process_batch; pub use burn::process_burn; pub use burn_checked::process_burn_checked; pub use close_account::process_close_account;