From de05760e786b3d0b38dff797b8731b38cd7936e7 Mon Sep 17 00:00:00 2001 From: Jon Cinque Date: Tue, 2 May 2023 16:52:38 +0200 Subject: [PATCH] tlv-account-resolution: Add state interface library for additional accounts (#4146) --- Cargo.lock | 15 + Cargo.toml | 1 + libraries/tlv-account-resolution/Cargo.toml | 30 ++ libraries/tlv-account-resolution/README.md | 139 ++++++ libraries/tlv-account-resolution/src/error.rs | 72 +++ libraries/tlv-account-resolution/src/lib.rs | 14 + libraries/tlv-account-resolution/src/pod.rs | 199 ++++++++ libraries/tlv-account-resolution/src/state.rs | 445 ++++++++++++++++++ 8 files changed, 915 insertions(+) create mode 100644 libraries/tlv-account-resolution/Cargo.toml create mode 100644 libraries/tlv-account-resolution/README.md create mode 100644 libraries/tlv-account-resolution/src/error.rs create mode 100644 libraries/tlv-account-resolution/src/lib.rs create mode 100644 libraries/tlv-account-resolution/src/pod.rs create mode 100644 libraries/tlv-account-resolution/src/state.rs diff --git a/Cargo.lock b/Cargo.lock index ec29e031..0b7ea22f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6354,6 +6354,21 @@ dependencies = [ "spl-token 3.5.0", ] +[[package]] +name = "spl-tlv-account-resolution" +version = "0.1.0" +dependencies = [ + "bytemuck", + "num-derive", + "num-traits", + "num_enum", + "solana-program", + "solana-program-test", + "solana-sdk", + "spl-type-length-value", + "thiserror", +] + [[package]] name = "spl-token" version = "3.5.0" diff --git a/Cargo.toml b/Cargo.toml index e82b8c93..37a584d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ members = [ "libraries/math", "libraries/concurrent-merkle-tree", "libraries/merkle-tree-reference", + "libraries/tlv-account-resolution", "libraries/type-length-value", "memo/program", "name-service/program", diff --git a/libraries/tlv-account-resolution/Cargo.toml b/libraries/tlv-account-resolution/Cargo.toml new file mode 100644 index 00000000..4181dda1 --- /dev/null +++ b/libraries/tlv-account-resolution/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "spl-tlv-account-resolution" +version = "0.1.0" +description = "Solana Program Library TLV Account Resolution Interface" +authors = ["Solana Labs Maintainers "] +repository = "https://github.com/solana-labs/solana-program-library" +license = "Apache-2.0" +edition = "2021" + +[features] +test-sbf = [] + +[dependencies] +bytemuck = { version = "1.13.0", features = ["derive"] } +num-derive = "0.3" +num-traits = "0.2" +num_enum = "0.5.9" +solana-program = "1.14.12" +spl-type-length-value = { version = "0.1", path = "../type-length-value" } +thiserror = "1.0" + +[dev-dependencies] +solana-program-test = "1.14.12" +solana-sdk = "1.14.12" + +[lib] +crate-type = ["cdylib", "lib"] + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] diff --git a/libraries/tlv-account-resolution/README.md b/libraries/tlv-account-resolution/README.md new file mode 100644 index 00000000..63cf368d --- /dev/null +++ b/libraries/tlv-account-resolution/README.md @@ -0,0 +1,139 @@ +# TLV Account Resolution + +Library defining a generic state interface to encode additional required accounts +for an instruction, using Type-Length-Value structures. + +## Example usage + +If you want to encode the additional required accounts for your instruction +into a TLV entry in an account, you can do the following: + +```rust +use { + solana_program::{account_info::AccountInfo, instruction::{AccountMeta, Instruction}, pubkey::Pubkey}, + spl_type_length_value::discriminator::{Discriminator, TlvDiscriminator}, + spl_tlv_account_resolution::state::ExtraAccountMetas, +}; + +struct MyInstruction; +impl TlvDiscriminator for MyInstruction { + // For ease of use, give it the same discriminator as its instruction definition + const TLV_DISCRIMINATOR: Discriminator = Discriminator::new([1; Discriminator::LENGTH]); +} + +// Actually put it in the additional required account keys and signer / writable +let extra_metas = [ + AccountMeta::new(Pubkey::new_unique(), false), + AccountMeta::new(Pubkey::new_unique(), true), + AccountMeta::new_readonly(Pubkey::new_unique(), true), + AccountMeta::new_readonly(Pubkey::new_unique(), false), +]; + +// Assume that this buffer is actually account data, already allocated to `account_size` +let account_size = ExtraAccountMetas::size_of(extra_metas.len()).unwrap(); +let mut buffer = vec![0; account_size]; + +// Initialize the structure for your instruction +ExtraAccountMetas::init_with_account_metas::(&mut buffer, &extra_metas).unwrap(); + +// Off-chain, you can add the additional accounts directly from the account data +let program_id = Pubkey::new_unique(); +let mut instruction = Instruction::new_with_bytes(program_id, &[], vec![]); +ExtraAccountMetas::add_to_instruction::(&mut instruction, &buffer).unwrap(); + +// On-chain, you can add the additional accounts *and* account infos +let mut cpi_instruction = Instruction::new_with_bytes(program_id, &[], vec![]); + +// Include all of the well-known required account infos here first +let mut cpi_account_infos = vec![]; + +// Provide all "remaining_account_infos" that are *not* part of any other known interface +let remaining_account_infos = &[]; +ExtraAccountMetas::add_to_cpi_instruction::( + &mut cpi_instruction, + &mut cpi_account_infos, + &buffer, + &remaining_account_infos, +).unwrap(); +``` + +For ease of use on-chain, `ExtraAccountMetas::init_with_account_infos` is also +provided to initialize directly from a set of given accounts. + +## Motivation + +The Solana account model presents unique challeneges for program interfaces. +Since it's impossible to load additional accounts on-chain, if a program requires +additional accounts to properly implement an instruction, there's no clear way +for clients to fetch these accounts. + +There are two main ways to fetch additional accounts, dynamically through program +simulation, or statically by fetching account data. This library implements +additional account resolution statically. You can find more information about +dynamic account resolution in the Appendix. + +### Static Account Resolution + +It's possible for programs to write the additional required account infos +into account data, so that on-chain and off-chain clients simply need to read +the data to figure out the additional required accounts. + +Rather than exposing this data dynamically through program execution, this method +uses static account data. + +For example, let's imagine there's a `Transferable` interface, along with a +`transfer` instruction. Some programs that implement `transfer` may need more +accounts than just the ones defined in the interface. How does a an on-chain or +off-chain client figure out the additional required accounts? + +The "static" approach requires programs to write the extra required accounts to +an account defined at a given address. This could be directly in the `mint`, or +some address derivable from the mint address. + +Off-chain, a client must fetch this additional account and read its data to find +out the additional required accounts, and then include them in the instruction. + +On-chain, a program must have access to "remaining account infos" containing the +special account and all other required accounts to properly create the CPI +instruction and give the correct account infos. + +This approach could also be called a "state interface". + +## How it works + +This library uses `spl-type-length-value` to read and write required instruction +accounts from account data. + +Interface instructions must have an 8-byte discriminator, so that the exposed +`ExtraAccountMetas` type can use the instruction discriminator as a `TlvDiscriminator`. + +This can be confusing. Typically, a type implements `TlvDiscriminator`, so that +the type can be written into TLV data. In this case, `ExtraAccountMetas` is +generic over `TlvDiscriminator`, meaning that a program can write many different instances of +`ExtraAccountMetas` into one account, using different `TlvDiscriminator`s. + +Also, it's reusing an instruction discriminator as a TLV discriminator. For example, +if the `transfer` instruction has a discriminator of `[1, 2, 3, 4, 5, 6, 7, 8]`, +then the account uses a TLV discriminator of `[1, 2, 3, 4, 5, 6, 7, 8]` to denote +where the additional account metas are stored. + +This isn't required, but makes it easier for clients to find the additional +required accounts for an instruction. + +## Appendix + +### Dynamic Account Resolution + +To expose the additional accounts required, instruction interfaces can include +supplemental instructions to return the required accounts. + +For example, in the `Transferable` interface example, along with a `transfer` +instruction, also requires implementations to expose a +`get_additional_accounts_for_transfer` instruction. + +In the program implementation, this instruction writes the additional accounts +into return data, making it easy for on-chain and off-chain clients to consume. + +See the +[relevant sRFC](https://forum.solana.com/t/srfc-00010-additional-accounts-request-transfer-spec/122) +for more information about the dynamic approach. diff --git a/libraries/tlv-account-resolution/src/error.rs b/libraries/tlv-account-resolution/src/error.rs new file mode 100644 index 00000000..0529ec4b --- /dev/null +++ b/libraries/tlv-account-resolution/src/error.rs @@ -0,0 +1,72 @@ +//! Error types + +use { + num_derive::FromPrimitive, + solana_program::{ + decode_error::DecodeError, + msg, + program_error::{PrintProgramError, ProgramError}, + }, + thiserror::Error, +}; + +/// Errors that may be returned by the Account Resolution library. +#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)] +pub enum AccountResolutionError { + /// Incorrect account provided + #[error("Incorrect account provided")] + IncorrectAccount, + /// Not enough accounts provided + #[error("Not enough accounts provided")] + NotEnoughAccounts, + /// No value initialized in TLV data + #[error("No value initialized in TLV data")] + TlvUninitialized, + /// Some value initialized in TLV data + #[error("Some value initialized in TLV data")] + TlvInitialized, + /// Provided byte buffer too small for validation pubkeys + #[error("Provided byte buffer too small for validation pubkeys")] + BufferTooSmall, + /// Error in checked math operation + #[error("Error in checked math operation")] + CalculationFailure, + /// Too many pubkeys provided + #[error("Too many pubkeys provided")] + TooManyPubkeys, + /// Provided byte buffer too large for expected type + #[error("Provided byte buffer too large for expected type")] + BufferTooLarge, +} +impl From for ProgramError { + fn from(e: AccountResolutionError) -> Self { + ProgramError::Custom(e as u32) + } +} +impl DecodeError for AccountResolutionError { + fn type_of() -> &'static str { + "AccountResolutionError" + } +} + +impl PrintProgramError for AccountResolutionError { + fn print(&self) + where + E: 'static + + std::error::Error + + DecodeError + + PrintProgramError + + num_traits::FromPrimitive, + { + match self { + Self::IncorrectAccount => msg!("Incorrect account provided"), + Self::NotEnoughAccounts => msg!("Not enough accounts provided"), + Self::TlvUninitialized => msg!("No value initialized in TLV data"), + Self::TlvInitialized => msg!("Some value initialized in TLV data"), + Self::BufferTooSmall => msg!("Provided byte buffer too small for validation pubkeys"), + Self::CalculationFailure => msg!("Error in checked math operation"), + Self::TooManyPubkeys => msg!("Too many pubkeys provided"), + Self::BufferTooLarge => msg!("Provided byte buffer too large for expected type"), + } + } +} diff --git a/libraries/tlv-account-resolution/src/lib.rs b/libraries/tlv-account-resolution/src/lib.rs new file mode 100644 index 00000000..87647996 --- /dev/null +++ b/libraries/tlv-account-resolution/src/lib.rs @@ -0,0 +1,14 @@ +//! Crate defining a state interface for offchain account resolution. If a program +//! writes the proper state information into one of their accounts, any offchain +//! and onchain client can fetch any additional required accounts for an instruction. + +#![allow(clippy::integer_arithmetic)] +#![deny(missing_docs)] +#![cfg_attr(not(test), forbid(unsafe_code))] + +pub mod error; +pub mod pod; +pub mod state; + +// Export current sdk types for downstream users building with a different sdk version +pub use solana_program; diff --git a/libraries/tlv-account-resolution/src/pod.rs b/libraries/tlv-account-resolution/src/pod.rs new file mode 100644 index 00000000..2b0f8728 --- /dev/null +++ b/libraries/tlv-account-resolution/src/pod.rs @@ -0,0 +1,199 @@ +//! Pod types to be used with bytemuck for zero-copy serde + +use { + crate::error::AccountResolutionError, + bytemuck::{Pod, Zeroable}, + solana_program::{ + account_info::AccountInfo, instruction::AccountMeta, program_error::ProgramError, + pubkey::Pubkey, + }, + spl_type_length_value::pod::{pod_from_bytes, pod_from_bytes_mut, PodU32}, +}; + +/// Convert a slice into a mutable `Pod` slice (zero copy) +pub fn pod_slice_from_bytes(bytes: &[u8]) -> Result<&[T], ProgramError> { + bytemuck::try_cast_slice(bytes).map_err(|_| ProgramError::InvalidArgument) +} +/// Convert a slice into a mutable `Pod` slice (zero copy) +pub fn pod_slice_from_bytes_mut(bytes: &mut [u8]) -> Result<&mut [T], ProgramError> { + bytemuck::try_cast_slice_mut(bytes).map_err(|_| ProgramError::InvalidArgument) +} + +/// The standard `bool` is not a `Pod`, define a replacement that is +#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] +#[repr(transparent)] +pub struct PodBool(u8); +impl From for PodBool { + fn from(b: bool) -> Self { + Self(if b { 1 } else { 0 }) + } +} +impl From<&PodBool> for bool { + fn from(b: &PodBool) -> Self { + b.0 != 0 + } +} +impl From for bool { + fn from(b: PodBool) -> Self { + b.0 != 0 + } +} + +/// The standard `AccountMeta` is not a `Pod`, define a replacement that is +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] +pub struct PodAccountMeta { + /// The pubkey of the account + pub pubkey: Pubkey, + /// Whether the account should sign + pub is_signer: PodBool, + /// Whether the account should be writable + pub is_writable: PodBool, +} +impl PartialEq> for PodAccountMeta { + fn eq(&self, other: &AccountInfo) -> bool { + self.pubkey == *other.key + && self.is_signer == other.is_signer.into() + && self.is_writable == other.is_writable.into() + } +} + +impl From<&AccountInfo<'_>> for PodAccountMeta { + fn from(account_info: &AccountInfo) -> Self { + Self { + pubkey: *account_info.key, + is_signer: account_info.is_signer.into(), + is_writable: account_info.is_writable.into(), + } + } +} + +impl From<&AccountMeta> for PodAccountMeta { + fn from(meta: &AccountMeta) -> Self { + Self { + pubkey: meta.pubkey, + is_signer: meta.is_signer.into(), + is_writable: meta.is_writable.into(), + } + } +} + +impl From<&PodAccountMeta> for AccountMeta { + fn from(meta: &PodAccountMeta) -> Self { + Self { + pubkey: meta.pubkey, + is_signer: meta.is_signer.into(), + is_writable: meta.is_writable.into(), + } + } +} + +const LENGTH_SIZE: usize = std::mem::size_of::(); +/// Special type for using a slice of `Pod`s in a zero-copy way +pub struct PodSlice<'data, T: Pod> { + length: &'data PodU32, + data: &'data [T], +} +impl<'data, T: Pod> PodSlice<'data, T> { + /// Unpack the buffer into a slice + pub fn unpack<'a>(data: &'a [u8]) -> Result + where + 'a: 'data, + { + if data.len() < LENGTH_SIZE { + return Err(AccountResolutionError::BufferTooSmall.into()); + } + let (length, data) = data.split_at(LENGTH_SIZE); + let length = pod_from_bytes::(length)?; + let _max_length = max_len_for_type::(data.len())?; + let data = pod_slice_from_bytes(data)?; + Ok(Self { length, data }) + } + + /// Get the slice data + pub fn data(&self) -> &[T] { + let length = u32::from(*self.length) as usize; + &self.data[..length] + } + + /// Get the amount of bytes used by `num_items` + pub fn size_of(num_items: usize) -> Result { + std::mem::size_of::() + .checked_mul(num_items) + .and_then(|len| len.checked_add(LENGTH_SIZE)) + .ok_or_else(|| AccountResolutionError::CalculationFailure.into()) + } +} + +/// Special type for using a slice of mutable `Pod`s in a zero-copy way +pub struct PodSliceMut<'data, T: Pod> { + length: &'data mut PodU32, + data: &'data mut [T], + max_length: usize, +} +impl<'data, T: Pod> PodSliceMut<'data, T> { + /// Unpack the mutable buffer into a mutable slice, with the option to + /// initialize the data + fn unpack_internal<'a>(data: &'a mut [u8], init: bool) -> Result + where + 'a: 'data, + { + if data.len() < LENGTH_SIZE { + return Err(AccountResolutionError::BufferTooSmall.into()); + } + let (length, data) = data.split_at_mut(LENGTH_SIZE); + let length = pod_from_bytes_mut::(length)?; + if init { + *length = 0.into(); + } + let max_length = max_len_for_type::(data.len())?; + let data = pod_slice_from_bytes_mut(data)?; + Ok(Self { + length, + data, + max_length, + }) + } + + /// Unpack the mutable buffer into a mutable slice + pub fn unpack<'a>(data: &'a mut [u8]) -> Result + where + 'a: 'data, + { + Self::unpack_internal(data, /* init */ false) + } + + /// Unpack the mutable buffer into a mutable slice, and initialize the + /// slice to 0-length + pub fn init<'a>(data: &'a mut [u8]) -> Result + where + 'a: 'data, + { + Self::unpack_internal(data, /* init */ true) + } + + /// Add another item to the slice + pub fn push(&mut self, t: T) -> Result<(), ProgramError> { + let length = u32::from(*self.length); + if length as usize == self.max_length { + Err(AccountResolutionError::BufferTooSmall.into()) + } else { + self.data[length as usize] = t; + *self.length = length.saturating_add(1).into(); + Ok(()) + } + } +} + +fn max_len_for_type(data_len: usize) -> Result { + let size: usize = std::mem::size_of::(); + let max_len = data_len + .checked_div(size) + .ok_or(AccountResolutionError::CalculationFailure)?; + // check that it isn't overallocated + if max_len.saturating_mul(size) != data_len { + Err(AccountResolutionError::BufferTooLarge.into()) + } else { + Ok(max_len) + } +} diff --git a/libraries/tlv-account-resolution/src/state.rs b/libraries/tlv-account-resolution/src/state.rs new file mode 100644 index 00000000..ddbfc058 --- /dev/null +++ b/libraries/tlv-account-resolution/src/state.rs @@ -0,0 +1,445 @@ +//! State transition types + +use { + crate::{ + error::AccountResolutionError, + pod::{PodAccountMeta, PodSlice, PodSliceMut}, + }, + solana_program::{ + account_info::AccountInfo, + instruction::{AccountMeta, Instruction}, + program_error::ProgramError, + }, + spl_type_length_value::{ + discriminator::TlvDiscriminator, + state::{TlvState, TlvStateBorrowed, TlvStateMut}, + }, +}; + +/// Stateless helper for storing additional accounts required for an instruction. +/// +/// This struct works with any `TlvDiscriminator`, and stores the extra accounts +/// needed for that specific instruction, using the given `Discriminator` as the +/// type-length-value `Discriminator`, and then storing all of the given +/// `AccountMeta`s as a zero-copy slice. +/// +/// Sample usage: +/// +/// ``` +/// use { +/// solana_program::{ +/// account_info::AccountInfo, instruction::{AccountMeta, Instruction}, +/// pubkey::Pubkey +/// }, +/// spl_type_length_value::discriminator::{Discriminator, TlvDiscriminator}, +/// spl_tlv_account_resolution::state::ExtraAccountMetas, +/// }; +/// +/// struct MyInstruction; +/// impl TlvDiscriminator for MyInstruction { +/// // Give it a unique discriminator, can also be generated using a hash function +/// const TLV_DISCRIMINATOR: Discriminator = Discriminator::new([1; Discriminator::LENGTH]); +/// } +/// +/// // actually put it in the additional required account keys and signer / writable +/// let extra_metas = [ +/// AccountMeta::new(Pubkey::new_unique(), false), +/// AccountMeta::new(Pubkey::new_unique(), true), +/// AccountMeta::new_readonly(Pubkey::new_unique(), true), +/// AccountMeta::new_readonly(Pubkey::new_unique(), false), +/// ]; +/// +/// // assume that this buffer is actually account data, already allocated to `account_size` +/// let account_size = ExtraAccountMetas::size_of(extra_metas.len()).unwrap(); +/// let mut buffer = vec![0; account_size]; +/// +/// // Initialize the structure for your instruction +/// ExtraAccountMetas::init_with_account_metas::(&mut buffer, &extra_metas).unwrap(); +/// +/// // Off-chain, you can add the additional accounts directly from the account data +/// let program_id = Pubkey::new_unique(); +/// let mut instruction = Instruction::new_with_bytes(program_id, &[], vec![]); +/// ExtraAccountMetas::add_to_instruction::(&mut instruction, &buffer).unwrap(); +/// +/// // On-chain, you can add the additional accounts *and* account infos +/// let mut cpi_instruction = Instruction::new_with_bytes(program_id, &[], vec![]); +/// let mut cpi_account_infos = vec![]; // assume the other required account infos are already included +/// let remaining_account_infos: &[AccountInfo<'_>] = &[]; // these are the account infos provided to the instruction that are *not* part of any other known interface +/// ExtraAccountMetas::add_to_cpi_instruction::( +/// &mut cpi_instruction, +/// &mut cpi_account_infos, +/// &buffer, +/// &remaining_account_infos, +/// ); +/// ``` +pub struct ExtraAccountMetas; +impl ExtraAccountMetas { + /// Initialize pod slice data for the given instruction and any type + /// convertible to account metas + pub fn init<'a, T: TlvDiscriminator, M>( + data: &mut [u8], + convertible_account_metas: &'a [M], + ) -> Result<(), ProgramError> + where + PodAccountMeta: From<&'a M>, + { + let mut state = TlvStateMut::unpack(data).unwrap(); + let tlv_size = PodSlice::::size_of(convertible_account_metas.len())?; + let bytes = state.alloc::(tlv_size)?; + let mut extra_account_metas = PodSliceMut::init(bytes)?; + for account_metas in convertible_account_metas { + extra_account_metas.push(PodAccountMeta::from(account_metas))?; + } + Ok(()) + } + + /// Initialize a TLV entry for the given discriminator, populating the data + /// with the given account infos + pub fn init_with_account_infos( + data: &mut [u8], + account_infos: &[AccountInfo<'_>], + ) -> Result<(), ProgramError> { + Self::init::(data, account_infos) + } + + /// Initialize a TLV entry for the given discriminator, populating the data + /// with the given account metas + pub fn init_with_account_metas( + data: &mut [u8], + account_metas: &[AccountMeta], + ) -> Result<(), ProgramError> { + Self::init::(data, account_metas) + } + + /// Get the byte size required to hold `num_items` items + pub fn size_of(num_items: usize) -> Result { + Ok(TlvStateBorrowed::get_base_len() + .saturating_add(PodSlice::::size_of(num_items)?)) + } + + /// Add the additional account metas to an existing instruction + pub fn add_to_instruction( + instruction: &mut Instruction, + data: &[u8], + ) -> Result<(), ProgramError> { + let state = TlvStateBorrowed::unpack(data)?; + let bytes = state.get_bytes::()?; + let extra_account_metas = PodSlice::::unpack(bytes)?; + instruction + .accounts + .extend(extra_account_metas.data().iter().map(|m| m.into())); + Ok(()) + } + + /// Add the additional account metas and account infos for a CPI + pub fn add_to_cpi_instruction<'a, T: TlvDiscriminator>( + cpi_instruction: &mut Instruction, + cpi_account_infos: &mut Vec>, + data: &[u8], + account_infos: &[AccountInfo<'a>], + ) -> Result<(), ProgramError> { + let state = TlvStateBorrowed::unpack(data)?; + let bytes = state.get_bytes::()?; + let extra_account_metas = PodSlice::::unpack(bytes)?; + + for account_meta in extra_account_metas.data().iter().map(AccountMeta::from) { + let account_info = account_infos + .iter() + .find(|&x| *x.key == account_meta.pubkey) + .ok_or(AccountResolutionError::IncorrectAccount)? + .clone(); + cpi_account_infos.push(account_info); + cpi_instruction.accounts.push(account_meta); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + solana_program::{clock::Epoch, instruction::AccountMeta, pubkey::Pubkey}, + spl_type_length_value::discriminator::Discriminator, + }; + + pub struct TestInstruction; + impl TlvDiscriminator for TestInstruction { + const TLV_DISCRIMINATOR: Discriminator = Discriminator::new([1; Discriminator::LENGTH]); + } + + pub struct TestOtherInstruction; + impl TlvDiscriminator for TestOtherInstruction { + const TLV_DISCRIMINATOR: Discriminator = Discriminator::new([2; Discriminator::LENGTH]); + } + + #[test] + fn init_with_metas() { + let metas = [ + AccountMeta::new(Pubkey::new_unique(), false), + AccountMeta::new(Pubkey::new_unique(), true), + AccountMeta::new_readonly(Pubkey::new_unique(), true), + AccountMeta::new_readonly(Pubkey::new_unique(), false), + ]; + let account_size = ExtraAccountMetas::size_of(metas.len()).unwrap(); + let mut buffer = vec![0; account_size]; + + ExtraAccountMetas::init_with_account_metas::(&mut buffer, &metas).unwrap(); + + let mut instruction = Instruction::new_with_bytes(Pubkey::new_unique(), &[], vec![]); + ExtraAccountMetas::add_to_instruction::(&mut instruction, &buffer) + .unwrap(); + assert_eq!( + instruction + .accounts + .iter() + .map(PodAccountMeta::from) + .collect::>(), + metas.iter().map(PodAccountMeta::from).collect::>() + ); + } + + #[test] + fn init_multiple() { + let metas = [ + AccountMeta::new(Pubkey::new_unique(), false), + AccountMeta::new(Pubkey::new_unique(), true), + AccountMeta::new_readonly(Pubkey::new_unique(), true), + AccountMeta::new_readonly(Pubkey::new_unique(), false), + ]; + let other_metas = [AccountMeta::new(Pubkey::new_unique(), false)]; + let account_size = ExtraAccountMetas::size_of(metas.len()).unwrap() + + ExtraAccountMetas::size_of(other_metas.len()).unwrap(); + let mut buffer = vec![0; account_size]; + + ExtraAccountMetas::init_with_account_metas::(&mut buffer, &metas).unwrap(); + ExtraAccountMetas::init_with_account_metas::( + &mut buffer, + &other_metas, + ) + .unwrap(); + + let mut instruction = Instruction::new_with_bytes(Pubkey::new_unique(), &[], vec![]); + ExtraAccountMetas::add_to_instruction::(&mut instruction, &buffer) + .unwrap(); + assert_eq!( + instruction + .accounts + .iter() + .map(PodAccountMeta::from) + .collect::>(), + metas.iter().map(PodAccountMeta::from).collect::>() + ); + let mut instruction = Instruction::new_with_bytes(Pubkey::new_unique(), &[], vec![]); + ExtraAccountMetas::add_to_instruction::(&mut instruction, &buffer) + .unwrap(); + assert_eq!( + instruction + .accounts + .iter() + .map(PodAccountMeta::from) + .collect::>(), + other_metas + .iter() + .map(PodAccountMeta::from) + .collect::>() + ); + } + + #[test] + fn init_mixed() { + // annoying to setup, but need to test this! + let pubkey1 = Pubkey::new_unique(); + let mut lamports1 = 0; + let mut data1 = []; + let pubkey2 = Pubkey::new_unique(); + let mut lamports2 = 0; + let mut data2 = []; + let pubkey3 = Pubkey::new_unique(); + let mut lamports3 = 0; + let mut data3 = []; + let owner = Pubkey::new_unique(); + let account_infos = [ + AccountInfo::new( + &pubkey1, + false, + true, + &mut lamports1, + &mut data1, + &owner, + false, + Epoch::default(), + ), + AccountInfo::new( + &pubkey2, + true, + false, + &mut lamports2, + &mut data2, + &owner, + false, + Epoch::default(), + ), + AccountInfo::new( + &pubkey3, + false, + false, + &mut lamports3, + &mut data3, + &owner, + false, + Epoch::default(), + ), + ]; + let metas = [ + AccountMeta::new(Pubkey::new_unique(), false), + AccountMeta::new(Pubkey::new_unique(), true), + AccountMeta::new_readonly(Pubkey::new_unique(), true), + AccountMeta::new_readonly(Pubkey::new_unique(), false), + AccountMeta::new_readonly(Pubkey::new_unique(), false), + AccountMeta::new_readonly(Pubkey::new_unique(), false), + ]; + let account_size = ExtraAccountMetas::size_of(account_infos.len()).unwrap() + + ExtraAccountMetas::size_of(metas.len()).unwrap(); + let mut buffer = vec![0; account_size]; + + ExtraAccountMetas::init_with_account_infos::(&mut buffer, &account_infos) + .unwrap(); + ExtraAccountMetas::init_with_account_metas::(&mut buffer, &metas) + .unwrap(); + + let mut instruction = Instruction::new_with_bytes(Pubkey::new_unique(), &[], vec![]); + ExtraAccountMetas::add_to_instruction::(&mut instruction, &buffer) + .unwrap(); + assert_eq!( + instruction + .accounts + .iter() + .map(PodAccountMeta::from) + .collect::>(), + account_infos + .iter() + .map(PodAccountMeta::from) + .collect::>() + ); + + let mut instruction = Instruction::new_with_bytes(Pubkey::new_unique(), &[], vec![]); + ExtraAccountMetas::add_to_instruction::(&mut instruction, &buffer) + .unwrap(); + assert_eq!( + instruction + .accounts + .iter() + .map(PodAccountMeta::from) + .collect::>(), + metas.iter().map(PodAccountMeta::from).collect::>() + ); + } + + #[test] + fn cpi_instruction() { + // annoying to setup, but need to test this! + let pubkey1 = Pubkey::new_unique(); + let mut lamports1 = 0; + let mut data1 = []; + let pubkey2 = Pubkey::new_unique(); + let mut lamports2 = 0; + let mut data2 = []; + let pubkey3 = Pubkey::new_unique(); + let mut lamports3 = 0; + let mut data3 = []; + let owner = Pubkey::new_unique(); + let account_infos = [ + AccountInfo::new( + &pubkey1, + false, + true, + &mut lamports1, + &mut data1, + &owner, + false, + Epoch::default(), + ), + AccountInfo::new( + &pubkey2, + true, + false, + &mut lamports2, + &mut data2, + &owner, + false, + Epoch::default(), + ), + AccountInfo::new( + &pubkey3, + false, + false, + &mut lamports3, + &mut data3, + &owner, + false, + Epoch::default(), + ), + ]; + let account_size = ExtraAccountMetas::size_of(account_infos.len()).unwrap(); + let mut buffer = vec![0; account_size]; + + ExtraAccountMetas::init_with_account_infos::(&mut buffer, &account_infos) + .unwrap(); + + // make an instruction to check later + let program_id = Pubkey::new_unique(); + let mut instruction = Instruction::new_with_bytes(program_id, &[], vec![]); + ExtraAccountMetas::add_to_instruction::(&mut instruction, &buffer) + .unwrap(); + + // mess around with the account infos to make it harder + let mut messed_account_infos = account_infos.to_vec(); + let pubkey4 = Pubkey::new_unique(); + let mut lamports4 = 0; + let mut data4 = []; + messed_account_infos.push(AccountInfo::new( + &pubkey4, + false, + true, + &mut lamports4, + &mut data4, + &owner, + false, + Epoch::default(), + )); + let pubkey5 = Pubkey::new_unique(); + let mut lamports5 = 0; + let mut data5 = []; + messed_account_infos.push(AccountInfo::new( + &pubkey5, + false, + true, + &mut lamports5, + &mut data5, + &owner, + false, + Epoch::default(), + )); + messed_account_infos.swap(0, 4); + messed_account_infos.swap(1, 2); + + let mut cpi_instruction = Instruction::new_with_bytes(program_id, &[], vec![]); + let mut cpi_account_infos = vec![]; + ExtraAccountMetas::add_to_cpi_instruction::( + &mut cpi_instruction, + &mut cpi_account_infos, + &buffer, + &messed_account_infos, + ) + .unwrap(); + + assert_eq!(cpi_instruction, instruction); + assert_eq!(cpi_account_infos.len(), account_infos.len()); + for (a, b) in std::iter::zip(cpi_account_infos, account_infos) { + assert_eq!(a.key, b.key); + assert_eq!(a.is_signer, b.is_signer); + assert_eq!(a.is_writable, b.is_writable); + } + } +}