From 88326533ed1f86a940445e7cfea3338f194d2433 Mon Sep 17 00:00:00 2001 From: Justin Starry Date: Thu, 31 Mar 2022 17:44:20 +0800 Subject: [PATCH] Add SDK support for creating transactions with address table lookups (#23728) * Add SDK support for creating transactions with address table lookups * fix bpf compilation * rename compile error variants to indicate overflow * Add doc tests * fix bpf compatibility * use constant for overflow tests * Use cfg_attr for dead code attribute * resolve merge conflict --- client/src/nonce_utils.rs | 4 +- .../src/address_lookup_table_account.rs | 7 + sdk/program/src/example_mocks.rs | 119 ++++++- sdk/program/src/lib.rs | 2 + sdk/program/src/message/account_keys.rs | 129 ++++++- sdk/program/src/message/compiled_keys.rs | 330 +++++++++++++++++- sdk/program/src/message/versions/v0/mod.rs | 189 +++++++++- sdk/program/src/system_instruction.rs | 10 +- 8 files changed, 740 insertions(+), 50 deletions(-) create mode 100644 sdk/program/src/address_lookup_table_account.rs diff --git a/client/src/nonce_utils.rs b/client/src/nonce_utils.rs index 5e80f1b410..fd4204d78e 100644 --- a/client/src/nonce_utils.rs +++ b/client/src/nonce_utils.rs @@ -113,7 +113,7 @@ pub fn account_identity_ok(account: &T) -> Result<(), Error> /// /// // Sign the tx with nonce_account's `blockhash` instead of the /// // network's latest blockhash. -/// let nonce_account = client.get_account(&nonce_account_pubkey)?; +/// let nonce_account = client.get_account(nonce_account_pubkey)?; /// let nonce_state = nonce_utils::state_from_account(&nonce_account)?; /// /// Ok(!matches!(nonce_state, State::Uninitialized)) @@ -197,7 +197,7 @@ pub fn state_from_account>( /// /// // Sign the tx with nonce_account's `blockhash` instead of the /// // network's latest blockhash. -/// let nonce_account = client.get_account(&nonce_account_pubkey)?; +/// let nonce_account = client.get_account(nonce_account_pubkey)?; /// let nonce_data = nonce_utils::data_from_account(&nonce_account)?; /// let blockhash = nonce_data.blockhash; /// diff --git a/sdk/program/src/address_lookup_table_account.rs b/sdk/program/src/address_lookup_table_account.rs new file mode 100644 index 0000000000..1430c1d3e1 --- /dev/null +++ b/sdk/program/src/address_lookup_table_account.rs @@ -0,0 +1,7 @@ +use solana_program::pubkey::Pubkey; + +#[derive(Debug, PartialEq, Clone)] +pub struct AddressLookupTableAccount { + pub key: Pubkey, + pub addresses: Vec, +} diff --git a/sdk/program/src/example_mocks.rs b/sdk/program/src/example_mocks.rs index 0b9f1e819d..c884df1360 100644 --- a/sdk/program/src/example_mocks.rs +++ b/sdk/program/src/example_mocks.rs @@ -41,19 +41,25 @@ pub mod solana_client { } pub mod rpc_client { - use super::{ - super::solana_sdk::{ - account::Account, hash::Hash, pubkey::Pubkey, signature::Signature, - transaction::Transaction, + use { + super::{ + super::solana_sdk::{ + account::Account, hash::Hash, pubkey::Pubkey, signature::Signature, + transaction::Transaction, + }, + client_error::Result as ClientResult, }, - client_error::{ClientError, Result as ClientResult}, + std::{cell::RefCell, collections::HashMap, rc::Rc}, }; - pub struct RpcClient; + #[derive(Default)] + pub struct RpcClient { + get_account_responses: Rc>>, + } impl RpcClient { pub fn new(_url: String) -> Self { - RpcClient + RpcClient::default() } pub fn get_latest_blockhash(&self) -> ClientResult { @@ -74,8 +80,19 @@ pub mod solana_client { Ok(0) } - pub fn get_account(&self, _pubkey: &Pubkey) -> Result { - Ok(Account {}) + pub fn get_account(&self, pubkey: &Pubkey) -> ClientResult { + Ok(self + .get_account_responses + .borrow() + .get(pubkey) + .cloned() + .unwrap()) + } + + pub fn set_get_account_response(&self, pubkey: Pubkey, account: Account) { + self.get_account_responses + .borrow_mut() + .insert(pubkey, account); } pub fn get_balance(&self, _pubkey: &Pubkey) -> ClientResult { @@ -92,13 +109,21 @@ pub mod solana_client { /// programs. pub mod solana_sdk { pub use crate::{ - hash, instruction, message, nonce, + address_lookup_table_account, hash, instruction, message, nonce, pubkey::{self, Pubkey}, - system_instruction, + system_instruction, system_program, }; pub mod account { - pub struct Account; + use crate::{clock::Epoch, pubkey::Pubkey}; + #[derive(Clone)] + pub struct Account { + pub lamports: u64, + pub data: Vec, + pub owner: Pubkey, + pub executable: bool, + pub rent_epoch: Epoch, + } pub trait ReadableAccount: Sized { fn data(&self) -> &[u8]; @@ -106,7 +131,7 @@ pub mod solana_sdk { impl ReadableAccount for Account { fn data(&self) -> &[u8] { - &[0] + &self.data } } } @@ -147,22 +172,49 @@ pub mod solana_sdk { pub mod signers { use super::signature::Signer; - #[derive(Debug, thiserror::Error, PartialEq)] - pub enum SignerError {} - pub trait Signers {} impl Signers for [&T; 1] {} impl Signers for [&T; 2] {} } + pub mod signer { + use thiserror::Error; + + #[derive(Error, Debug)] + #[error("mock-error")] + pub struct SignerError; + } + pub mod transaction { use { - super::signers::{SignerError, Signers}, - crate::{hash::Hash, instruction::Instruction, message::Message, pubkey::Pubkey}, + super::{signature::Signature, signer::SignerError, signers::Signers}, + crate::{ + hash::Hash, + instruction::Instruction, + message::{Message, VersionedMessage}, + pubkey::Pubkey, + }, serde::Serialize, }; + pub struct VersionedTransaction { + pub signatures: Vec, + pub message: VersionedMessage, + } + + impl VersionedTransaction { + pub fn try_new( + message: VersionedMessage, + _keypairs: &T, + ) -> std::result::Result { + Ok(VersionedTransaction { + signatures: vec![], + message, + }) + } + } + #[derive(Serialize)] pub struct Transaction { pub message: Message, @@ -213,3 +265,34 @@ pub mod solana_sdk { } } } + +pub mod solana_address_lookup_table_program { + crate::declare_id!("AddressLookupTab1e1111111111111111111111111"); + + pub mod state { + use { + crate::{instruction::InstructionError, pubkey::Pubkey}, + std::borrow::Cow, + }; + + pub struct AddressLookupTable<'a> { + pub addresses: Cow<'a, [Pubkey]>, + } + + impl<'a> AddressLookupTable<'a> { + pub fn serialize_for_tests(self) -> Result, InstructionError> { + let mut data = vec![]; + self.addresses.iter().for_each(|address| { + data.extend_from_slice(address.as_ref()); + }); + Ok(data) + } + + pub fn deserialize(data: &'a [u8]) -> Result, InstructionError> { + Ok(Self { + addresses: Cow::Borrowed(bytemuck::try_cast_slice(data).unwrap()), + }) + } + } + } +} diff --git a/sdk/program/src/lib.rs b/sdk/program/src/lib.rs index a50ecf70da..d6cbec8515 100644 --- a/sdk/program/src/lib.rs +++ b/sdk/program/src/lib.rs @@ -558,6 +558,7 @@ extern crate self as solana_program; pub mod account_info; +pub mod address_lookup_table_account; pub(crate) mod atomic_u64; pub mod blake3; pub mod borsh; @@ -571,6 +572,7 @@ pub mod ed25519_program; pub mod entrypoint; pub mod entrypoint_deprecated; pub mod epoch_schedule; +#[cfg(not(target_arch = "bpf"))] pub mod example_mocks; pub mod feature; pub mod fee_calculator; diff --git a/sdk/program/src/message/account_keys.rs b/sdk/program/src/message/account_keys.rs index 5b6812eb9c..c121150fe1 100644 --- a/sdk/program/src/message/account_keys.rs +++ b/sdk/program/src/message/account_keys.rs @@ -1,7 +1,7 @@ use { crate::{ instruction::{CompiledInstruction, Instruction}, - message::v0::LoadedAddresses, + message::{v0::LoadedAddresses, CompileError}, pubkey::Pubkey, }, std::{collections::BTreeMap, ops::Index}, @@ -82,12 +82,42 @@ impl<'a> AccountKeys<'a> { /// Compile instructions using the order of account keys to determine /// compiled instruction account indexes. + /// + /// # Panics + /// + /// Panics when compiling fails. See [`AccountKeys::try_compile_instructions`] + /// for a full description of failure scenarios. pub fn compile_instructions(&self, instructions: &[Instruction]) -> Vec { - let account_index_map: BTreeMap<&Pubkey, u8> = BTreeMap::from_iter( - self.iter() - .enumerate() - .map(|(index, key)| (key, index as u8)), - ); + self.try_compile_instructions(instructions) + .expect("compilation failure") + } + + /// Compile instructions using the order of account keys to determine + /// compiled instruction account indexes. + /// + /// # Errors + /// + /// Compilation will fail if any `instructions` use account keys which are not + /// present in this account key collection. + /// + /// Compilation will fail if any `instructions` use account keys which are located + /// at an index which cannot be cast to a `u8` without overflow. + pub fn try_compile_instructions( + &self, + instructions: &[Instruction], + ) -> Result, CompileError> { + let mut account_index_map = BTreeMap::<&Pubkey, u8>::new(); + for (index, key) in self.iter().enumerate() { + let index = u8::try_from(index).map_err(|_| CompileError::AccountIndexOverflow)?; + account_index_map.insert(key, index); + } + + let get_account_index = |key: &Pubkey| -> Result { + account_index_map + .get(key) + .cloned() + .ok_or(CompileError::UnknownInstructionKey(*key)) + }; instructions .iter() @@ -95,14 +125,14 @@ impl<'a> AccountKeys<'a> { let accounts: Vec = ix .accounts .iter() - .map(|account_meta| *account_index_map.get(&account_meta.pubkey).unwrap()) - .collect(); + .map(|account_meta| get_account_index(&account_meta.pubkey)) + .collect::, CompileError>>()?; - CompiledInstruction { - program_id_index: *account_index_map.get(&ix.program_id).unwrap(), + Ok(CompiledInstruction { + program_id_index: get_account_index(&ix.program_id)?, data: ix.data.clone(), accounts, - } + }) }) .collect() } @@ -110,7 +140,7 @@ impl<'a> AccountKeys<'a> { #[cfg(test)] mod tests { - use super::*; + use {super::*, crate::instruction::AccountMeta}; fn test_account_keys() -> [Pubkey; 6] { let key0 = Pubkey::new_unique(); @@ -227,4 +257,79 @@ mod tests { assert_eq!(account_keys.get(4), Some(&keys[4])); assert_eq!(account_keys.get(5), Some(&keys[5])); } + + #[test] + fn test_try_compile_instructions() { + let keys = test_account_keys(); + + let static_keys = vec![keys[0]]; + let dynamic_keys = LoadedAddresses { + writable: vec![keys[1]], + readonly: vec![keys[2]], + }; + let account_keys = AccountKeys::new(&static_keys, Some(&dynamic_keys)); + + let instruction = Instruction { + program_id: keys[0], + accounts: vec![ + AccountMeta::new(keys[1], true), + AccountMeta::new(keys[2], true), + ], + data: vec![0], + }; + + assert_eq!( + account_keys.try_compile_instructions(&[instruction]), + Ok(vec![CompiledInstruction { + program_id_index: 0, + accounts: vec![1, 2], + data: vec![0], + }]), + ); + } + + #[test] + fn test_try_compile_instructions_with_unknown_key() { + let static_keys = test_account_keys(); + let account_keys = AccountKeys::new(&static_keys, None); + + let unknown_key = Pubkey::new_unique(); + let test_instructions = [ + Instruction { + program_id: unknown_key, + accounts: vec![], + data: vec![], + }, + Instruction { + program_id: static_keys[0], + accounts: vec![ + AccountMeta::new(static_keys[1], false), + AccountMeta::new(unknown_key, false), + ], + data: vec![], + }, + ]; + + for ix in test_instructions { + assert_eq!( + account_keys.try_compile_instructions(&[ix]), + Err(CompileError::UnknownInstructionKey(unknown_key)) + ); + } + } + + #[test] + fn test_try_compile_instructions_with_too_many_account_keys() { + const MAX_LENGTH_WITHOUT_OVERFLOW: usize = u8::MAX as usize + 1; + let static_keys = vec![Pubkey::default(); MAX_LENGTH_WITHOUT_OVERFLOW]; + let dynamic_keys = LoadedAddresses { + writable: vec![Pubkey::default()], + readonly: vec![], + }; + let account_keys = AccountKeys::new(&static_keys, Some(&dynamic_keys)); + assert_eq!( + account_keys.try_compile_instructions(&[]), + Err(CompileError::AccountIndexOverflow) + ); + } } diff --git a/sdk/program/src/message/compiled_keys.rs b/sdk/program/src/message/compiled_keys.rs index 9a716767ff..bd03358338 100644 --- a/sdk/program/src/message/compiled_keys.rs +++ b/sdk/program/src/message/compiled_keys.rs @@ -1,10 +1,16 @@ +#[cfg(not(target_arch = "bpf"))] +use crate::{ + address_lookup_table_account::AddressLookupTableAccount, + message::v0::{LoadedAddresses, MessageAddressTableLookup}, +}; use { crate::{instruction::Instruction, message::MessageHeader, pubkey::Pubkey}, std::collections::BTreeMap, + thiserror::Error, }; /// A helper struct to collect pubkeys compiled for a set of instructions -#[derive(Default, Debug, PartialEq, Eq)] +#[derive(Default, Debug, Clone, PartialEq, Eq)] pub(crate) struct CompiledKeys { writable_signer_keys: Vec, readonly_signer_keys: Vec, @@ -12,6 +18,17 @@ pub(crate) struct CompiledKeys { readonly_non_signer_keys: Vec, } +#[cfg_attr(target_arch = "bpf", allow(dead_code))] +#[derive(PartialEq, Debug, Error, Eq, Clone)] +pub enum CompileError { + #[error("account index overflowed during compilation")] + AccountIndexOverflow, + #[error("address lookup table index overflowed during compilation")] + AddressTableLookupIndexOverflow, + #[error("encountered unknown account key `{0}` during instruction compilation")] + UnknownInstructionKey(Pubkey), +} + #[derive(Default, Debug)] struct CompiledKeyMeta { is_signer: bool, @@ -65,17 +82,22 @@ impl CompiledKeys { } } - pub(crate) fn try_into_message_components(self) -> Option<(MessageHeader, Vec)> { + pub(crate) fn try_into_message_components( + self, + ) -> Result<(MessageHeader, Vec), CompileError> { + let try_into_u8 = |num: usize| -> Result { + u8::try_from(num).map_err(|_| CompileError::AccountIndexOverflow) + }; + + let signers_len = self + .writable_signer_keys + .len() + .saturating_add(self.readonly_signer_keys.len()); + let header = MessageHeader { - num_required_signatures: u8::try_from( - self.writable_signer_keys - .len() - .checked_add(self.readonly_signer_keys.len())?, - ) - .ok()?, - num_readonly_signed_accounts: u8::try_from(self.readonly_signer_keys.len()).ok()?, - num_readonly_unsigned_accounts: u8::try_from(self.readonly_non_signer_keys.len()) - .ok()?, + num_required_signatures: try_into_u8(signers_len)?, + num_readonly_signed_accounts: try_into_u8(self.readonly_signer_keys.len())?, + num_readonly_unsigned_accounts: try_into_u8(self.readonly_non_signer_keys.len())?, }; let static_account_keys = std::iter::empty() @@ -85,8 +107,71 @@ impl CompiledKeys { .chain(self.readonly_non_signer_keys) .collect(); - Some((header, static_account_keys)) + Ok((header, static_account_keys)) } + + #[cfg(not(target_arch = "bpf"))] + pub(crate) fn try_extract_table_lookup( + &mut self, + lookup_table_account: &AddressLookupTableAccount, + ) -> Result, CompileError> { + let (writable_indexes, drained_writable_keys) = try_drain_keys_found_in_lookup_table( + &mut self.writable_non_signer_keys, + &lookup_table_account.addresses, + )?; + let (readonly_indexes, drained_readonly_keys) = try_drain_keys_found_in_lookup_table( + &mut self.readonly_non_signer_keys, + &lookup_table_account.addresses, + )?; + + // Don't extract lookup if no keys were found + if writable_indexes.is_empty() && readonly_indexes.is_empty() { + return Ok(None); + } + + Ok(Some(( + MessageAddressTableLookup { + account_key: lookup_table_account.key, + writable_indexes, + readonly_indexes, + }, + LoadedAddresses { + writable: drained_writable_keys, + readonly: drained_readonly_keys, + }, + ))) + } +} + +#[cfg_attr(target_arch = "bpf", allow(dead_code))] +fn try_drain_keys_found_in_lookup_table( + keys: &mut Vec, + lookup_table_addresses: &[Pubkey], +) -> Result<(Vec, Vec), CompileError> { + let mut lookup_table_indexes = Vec::new(); + let mut drained_keys = Vec::new(); + let mut i = 0; + while i < keys.len() { + let search_key = &keys[i]; + let mut lookup_table_index = None; + for (key_index, key) in lookup_table_addresses.iter().enumerate() { + if key == search_key { + lookup_table_index = Some( + u8::try_from(key_index) + .map_err(|_| CompileError::AddressTableLookupIndexOverflow)?, + ); + break; + } + } + + if let Some(index) = lookup_table_index { + lookup_table_indexes.push(index); + drained_keys.push(keys.remove(i)); + } else { + i = i.saturating_add(1); + } + } + Ok((lookup_table_indexes, drained_keys)) } #[cfg(test)] @@ -228,4 +313,225 @@ mod tests { } ); } + + #[test] + fn test_try_into_message_components() { + let keys = vec![ + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + ]; + + let compiled_keys = CompiledKeys { + writable_signer_keys: vec![keys[0]], + readonly_signer_keys: vec![keys[1]], + writable_non_signer_keys: vec![keys[2]], + readonly_non_signer_keys: vec![keys[3]], + }; + + let result = compiled_keys.try_into_message_components(); + assert_eq!(result.as_ref().err(), None); + let (header, static_keys) = result.unwrap(); + + assert_eq!(static_keys, keys); + assert_eq!( + header, + MessageHeader { + num_required_signatures: 2, + num_readonly_signed_accounts: 1, + num_readonly_unsigned_accounts: 1, + } + ); + } + + #[test] + fn test_try_into_message_components_with_too_many_keys() { + let too_many_keys_vec = vec![Pubkey::default(); 257]; + + let mut test_keys_list = vec![CompiledKeys::default(); 3]; + test_keys_list[0] + .writable_signer_keys + .extend(too_many_keys_vec.clone()); + test_keys_list[1] + .readonly_signer_keys + .extend(too_many_keys_vec.clone()); + // skip writable_non_signer_keys because it isn't used for creating header values + test_keys_list[2] + .readonly_non_signer_keys + .extend(too_many_keys_vec); + + for test_keys in test_keys_list { + assert_eq!( + test_keys.try_into_message_components(), + Err(CompileError::AccountIndexOverflow) + ); + } + } + + #[test] + fn test_try_extract_table_lookup() { + let writable_keys = vec![Pubkey::new_unique(), Pubkey::new_unique()]; + let readonly_keys = vec![Pubkey::new_unique(), Pubkey::new_unique()]; + + let mut compiled_keys = CompiledKeys { + writable_signer_keys: vec![writable_keys[0]], + readonly_signer_keys: vec![readonly_keys[0]], + writable_non_signer_keys: vec![writable_keys[1]], + readonly_non_signer_keys: vec![readonly_keys[1]], + }; + + let lookup_table_account = AddressLookupTableAccount { + key: Pubkey::new_unique(), + addresses: vec![ + writable_keys[0], + readonly_keys[0], + writable_keys[1], + readonly_keys[1], + // add some duplicates to ensure lowest index is selected + writable_keys[1], + readonly_keys[1], + ], + }; + + assert_eq!( + compiled_keys.try_extract_table_lookup(&lookup_table_account), + Ok(Some(( + MessageAddressTableLookup { + account_key: lookup_table_account.key, + writable_indexes: vec![2], + readonly_indexes: vec![3], + }, + LoadedAddresses { + writable: vec![writable_keys[1]], + readonly: vec![readonly_keys[1]], + }, + ))) + ); + } + + #[test] + fn test_try_extract_table_lookup_returns_none() { + let mut compiled_keys = CompiledKeys { + writable_non_signer_keys: vec![Pubkey::new_unique()], + readonly_non_signer_keys: vec![Pubkey::new_unique()], + ..CompiledKeys::default() + }; + + let lookup_table_account = AddressLookupTableAccount { + key: Pubkey::new_unique(), + addresses: vec![], + }; + + assert_eq!( + compiled_keys.try_extract_table_lookup(&lookup_table_account), + Ok(None) + ); + } + + #[test] + fn test_try_extract_table_lookup_for_invalid_table() { + let mut compiled_keys = CompiledKeys { + writable_non_signer_keys: vec![Pubkey::new_unique()], + readonly_non_signer_keys: vec![Pubkey::new_unique()], + ..CompiledKeys::default() + }; + + const MAX_LENGTH_WITHOUT_OVERFLOW: usize = u8::MAX as usize + 1; + let mut addresses = vec![Pubkey::default(); MAX_LENGTH_WITHOUT_OVERFLOW]; + addresses.push(compiled_keys.writable_non_signer_keys[0]); + + let lookup_table_account = AddressLookupTableAccount { + key: Pubkey::new_unique(), + addresses, + }; + + assert_eq!( + compiled_keys.try_extract_table_lookup(&lookup_table_account), + Err(CompileError::AddressTableLookupIndexOverflow), + ); + } + + #[test] + fn test_try_drain_keys_found_in_lookup_table() { + let orig_keys = vec![ + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + ]; + + let lookup_table_addresses = vec![ + Pubkey::new_unique(), + orig_keys[0], + Pubkey::new_unique(), + orig_keys[4], + Pubkey::new_unique(), + orig_keys[2], + Pubkey::new_unique(), + ]; + + let mut keys = orig_keys.clone(); + let drain_result = try_drain_keys_found_in_lookup_table(&mut keys, &lookup_table_addresses); + assert_eq!(drain_result.as_ref().err(), None); + let (lookup_table_indexes, drained_keys) = drain_result.unwrap(); + + assert_eq!(keys, vec![orig_keys[1], orig_keys[3]]); + assert_eq!(drained_keys, vec![orig_keys[0], orig_keys[2], orig_keys[4]]); + assert_eq!(lookup_table_indexes, vec![1, 5, 3]); + } + + #[test] + fn test_try_drain_keys_found_in_lookup_table_with_empty_keys() { + let mut keys = vec![]; + + let lookup_table_addresses = vec![ + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + ]; + + let drain_result = try_drain_keys_found_in_lookup_table(&mut keys, &lookup_table_addresses); + assert_eq!(drain_result.as_ref().err(), None); + let (lookup_table_indexes, drained_keys) = drain_result.unwrap(); + + assert!(keys.is_empty()); + assert!(drained_keys.is_empty()); + assert!(lookup_table_indexes.is_empty()); + } + + #[test] + fn test_try_drain_keys_found_in_lookup_table_with_empty_table() { + let original_keys = vec![ + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + ]; + + let lookup_table_addresses = vec![]; + + let mut keys = original_keys.clone(); + let drain_result = try_drain_keys_found_in_lookup_table(&mut keys, &lookup_table_addresses); + assert_eq!(drain_result.as_ref().err(), None); + let (lookup_table_indexes, drained_keys) = drain_result.unwrap(); + + assert_eq!(keys, original_keys); + assert!(drained_keys.is_empty()); + assert!(lookup_table_indexes.is_empty()); + } + + #[test] + fn test_try_drain_keys_found_in_lookup_table_with_too_many_addresses() { + let mut keys = vec![Pubkey::new_unique()]; + const MAX_LENGTH_WITHOUT_OVERFLOW: usize = u8::MAX as usize + 1; + let mut lookup_table_addresses = vec![Pubkey::default(); MAX_LENGTH_WITHOUT_OVERFLOW]; + lookup_table_addresses.push(keys[0]); + + let drain_result = try_drain_keys_found_in_lookup_table(&mut keys, &lookup_table_addresses); + assert_eq!( + drain_result.err(), + Some(CompileError::AddressTableLookupIndexOverflow) + ); + } } diff --git a/sdk/program/src/message/versions/v0/mod.rs b/sdk/program/src/message/versions/v0/mod.rs index 85ed791ae8..81c0461180 100644 --- a/sdk/program/src/message/versions/v0/mod.rs +++ b/sdk/program/src/message/versions/v0/mod.rs @@ -10,19 +10,22 @@ //! [future message format]: https://docs.solana.com/proposals/transactions-v2 use crate::{ + address_lookup_table_account::AddressLookupTableAccount, bpf_loader_upgradeable, hash::Hash, - instruction::CompiledInstruction, - message::{legacy::BUILTIN_PROGRAMS_KEYS, MessageHeader, MESSAGE_VERSION_PREFIX}, + instruction::{CompiledInstruction, Instruction}, + message::{ + compiled_keys::CompileError, legacy::BUILTIN_PROGRAMS_KEYS, AccountKeys, CompiledKeys, + MessageHeader, MESSAGE_VERSION_PREFIX, + }, pubkey::Pubkey, sanitize::{Sanitize, SanitizeError}, short_vec, sysvar, }; +pub use loaded::*; mod loaded; -pub use loaded::*; - /// Address table lookups describe an on-chain address lookup table to use /// for loading more readonly and writable accounts in a single tx. #[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq, Clone, AbiExample)] @@ -135,6 +138,118 @@ impl Sanitize for Message { } impl Message { + /// Create a signable transaction message from a `payer` public key, + /// `recent_blockhash`, list of `instructions`, and a list of + /// `address_lookup_table_accounts`. + /// + /// # Examples + /// + /// This example uses the [`solana_address_lookup_table_program`], [`solana_client`], [`solana_sdk`], and [`anyhow`] crates. + /// + /// [`solana_address_lookup_table_program`]: https://docs.rs/solana-address-lookup-table-program + /// [`solana_client`]: https://docs.rs/solana-client + /// [`solana_sdk`]: https://docs.rs/solana-sdk + /// [`anyhow`]: https://docs.rs/anyhow + /// + /// ``` + /// # use solana_program::example_mocks::{ + /// # solana_address_lookup_table_program, + /// # solana_client, + /// # solana_sdk, + /// # }; + /// # use std::borrow::Cow; + /// # use solana_sdk::account::Account; + /// use anyhow::Result; + /// use solana_address_lookup_table_program::state::AddressLookupTable; + /// use solana_client::rpc_client::RpcClient; + /// use solana_sdk::{ + /// address_lookup_table_account::AddressLookupTableAccount, + /// instruction::{AccountMeta, Instruction}, + /// message::{VersionedMessage, v0}, + /// pubkey::Pubkey, + /// signature::{Keypair, Signer}, + /// transaction::VersionedTransaction, + /// }; + /// + /// fn create_tx_with_address_table_lookup( + /// client: &RpcClient, + /// instruction: Instruction, + /// address_lookup_table_key: Pubkey, + /// payer: &Keypair, + /// ) -> Result { + /// # client.set_get_account_response(address_lookup_table_key, Account { + /// # lamports: 1, + /// # data: AddressLookupTable { + /// # addresses: Cow::Owned(instruction.accounts.iter().map(|meta| meta.pubkey).collect()), + /// # }.serialize_for_tests().unwrap(), + /// # owner: solana_address_lookup_table_program::ID, + /// # executable: false, + /// # rent_epoch: 1, + /// # }); + /// let raw_account = client.get_account(&address_lookup_table_key)?; + /// let address_lookup_table = AddressLookupTable::deserialize(&raw_account.data)?; + /// let address_lookup_table_account = AddressLookupTableAccount { + /// key: address_lookup_table_key, + /// addresses: address_lookup_table.addresses.to_vec(), + /// }; + /// + /// let blockhash = client.get_latest_blockhash()?; + /// let tx = VersionedTransaction::try_new( + /// VersionedMessage::V0(v0::Message::try_compile( + /// &payer.pubkey(), + /// &[instruction], + /// &[address_lookup_table_account], + /// blockhash, + /// )?), + /// &[payer], + /// )?; + /// + /// # assert!(tx.message.address_table_lookups().unwrap().len() > 0); + /// Ok(tx) + /// } + /// # + /// # let client = RpcClient::new(String::new()); + /// # let payer = Keypair::new(); + /// # let address_lookup_table_key = Pubkey::new_unique(); + /// # let instruction = Instruction::new_with_bincode(Pubkey::new_unique(), &(), vec![ + /// # AccountMeta::new(Pubkey::new_unique(), false), + /// # ]); + /// # create_tx_with_address_table_lookup(&client, instruction, address_lookup_table_key, &payer)?; + /// # Ok::<(), anyhow::Error>(()) + /// ``` + pub fn try_compile( + payer: &Pubkey, + instructions: &[Instruction], + address_lookup_table_accounts: &[AddressLookupTableAccount], + recent_blockhash: Hash, + ) -> Result { + let mut compiled_keys = CompiledKeys::compile(instructions, Some(*payer)); + + let mut address_table_lookups = Vec::with_capacity(address_lookup_table_accounts.len()); + let mut loaded_addresses_list = Vec::with_capacity(address_lookup_table_accounts.len()); + for lookup_table_account in address_lookup_table_accounts { + if let Some((lookup, loaded_addresses)) = + compiled_keys.try_extract_table_lookup(lookup_table_account)? + { + address_table_lookups.push(lookup); + loaded_addresses_list.push(loaded_addresses); + } + } + + let (header, static_keys) = compiled_keys.try_into_message_components()?; + let dynamic_keys = loaded_addresses_list.into_iter().collect(); + let account_keys = AccountKeys::new(&static_keys, Some(&dynamic_keys)); + let instructions = account_keys.try_compile_instructions(instructions)?; + + Ok(Self { + header, + account_keys: static_keys, + recent_blockhash, + instructions, + address_table_lookups, + }) + } + /// Serialize this message with a version #0 prefix using bincode encoding. pub fn serialize(&self) -> Vec { bincode::serialize(&(MESSAGE_VERSION_PREFIX, self)).unwrap() @@ -207,7 +322,10 @@ impl Message { #[cfg(test)] mod tests { - use {super::*, crate::message::VersionedMessage}; + use { + super::*, + crate::{instruction::AccountMeta, message::VersionedMessage}, + }; #[test] fn test_sanitize() { @@ -443,10 +561,71 @@ mod tests { .sanitize() .is_err()); } + #[test] fn test_serialize() { let message = Message::default(); let versioned_msg = VersionedMessage::V0(message.clone()); assert_eq!(message.serialize(), versioned_msg.serialize()); } + + #[test] + fn test_try_compile() { + let mut keys = vec![]; + keys.resize_with(8, Pubkey::new_unique); + + let payer = keys[0]; + let program_id = keys[7]; + let instructions = vec![Instruction { + program_id, + accounts: vec![ + AccountMeta::new(keys[1], true), + AccountMeta::new_readonly(keys[2], true), + AccountMeta::new(keys[3], false), + AccountMeta::new_readonly(keys[4], false), + AccountMeta::new(keys[5], false), + AccountMeta::new_readonly(keys[6], false), + ], + data: vec![], + }]; + let address_lookup_table_accounts = vec![ + AddressLookupTableAccount { + key: Pubkey::new_unique(), + addresses: vec![keys[5], keys[6], program_id], + }, + AddressLookupTableAccount { + key: Pubkey::new_unique(), + addresses: vec![], + }, + ]; + + let recent_blockhash = Hash::new_unique(); + assert_eq!( + Message::try_compile( + &payer, + &instructions, + &address_lookup_table_accounts, + recent_blockhash + ), + Ok(Message { + header: MessageHeader { + num_required_signatures: 3, + num_readonly_signed_accounts: 1, + num_readonly_unsigned_accounts: 1 + }, + recent_blockhash, + account_keys: vec![keys[0], keys[1], keys[2], keys[3], keys[4]], + instructions: vec![CompiledInstruction { + program_id_index: 7, + accounts: vec![1, 2, 3, 4, 5, 6], + data: vec![], + },], + address_table_lookups: vec![MessageAddressTableLookup { + account_key: address_lookup_table_accounts[0].key, + writable_indexes: vec![0], + readonly_indexes: vec![1, 2], + }], + }) + ); + } } diff --git a/sdk/program/src/system_instruction.rs b/sdk/program/src/system_instruction.rs index 25d72c61ca..3dc3a0ef5f 100644 --- a/sdk/program/src/system_instruction.rs +++ b/sdk/program/src/system_instruction.rs @@ -683,6 +683,7 @@ pub fn create_nonce_account( /// system_instruction, /// transaction::Transaction, /// }; +/// # use solana_sdk::account::Account; /// use std::path::Path; /// use anyhow::Result; /// # use anyhow::anyhow; @@ -722,7 +723,14 @@ pub fn create_nonce_account( /// /// // Sign the tx with nonce_account's `blockhash` instead of the /// // network's latest blockhash. -/// let nonce_account = client.get_account(&nonce_account_pubkey)?; +/// # client.set_get_account_response(*nonce_account_pubkey, Account { +/// # lamports: 1, +/// # data: vec![0], +/// # owner: solana_sdk::system_program::ID, +/// # executable: false, +/// # rent_epoch: 1, +/// # }); +/// let nonce_account = client.get_account(nonce_account_pubkey)?; /// let nonce_data = nonce_utils::data_from_account(&nonce_account)?; /// let blockhash = nonce_data.blockhash; ///