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
This commit is contained in:
Justin Starry 2022-03-31 17:44:20 +08:00 committed by GitHub
parent 9abebc2d64
commit 88326533ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 740 additions and 50 deletions

View File

@ -113,7 +113,7 @@ pub fn account_identity_ok<T: ReadableAccount>(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<T: ReadableAccount + StateMut<Versions>>(
///
/// // 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;
///

View File

@ -0,0 +1,7 @@
use solana_program::pubkey::Pubkey;
#[derive(Debug, PartialEq, Clone)]
pub struct AddressLookupTableAccount {
pub key: Pubkey,
pub addresses: Vec<Pubkey>,
}

View File

@ -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<RefCell<HashMap<Pubkey, Account>>>,
}
impl RpcClient {
pub fn new(_url: String) -> Self {
RpcClient
RpcClient::default()
}
pub fn get_latest_blockhash(&self) -> ClientResult<Hash> {
@ -74,8 +80,19 @@ pub mod solana_client {
Ok(0)
}
pub fn get_account(&self, _pubkey: &Pubkey) -> Result<Account, ClientError> {
Ok(Account {})
pub fn get_account(&self, pubkey: &Pubkey) -> ClientResult<Account> {
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<u64> {
@ -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<u8>,
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<T: Signer> Signers for [&T; 1] {}
impl<T: Signer> 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<Signature>,
pub message: VersionedMessage,
}
impl VersionedTransaction {
pub fn try_new<T: Signers>(
message: VersionedMessage,
_keypairs: &T,
) -> std::result::Result<Self, SignerError> {
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<Vec<u8>, 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<AddressLookupTable<'a>, InstructionError> {
Ok(Self {
addresses: Cow::Borrowed(bytemuck::try_cast_slice(data).unwrap()),
})
}
}
}
}

View File

@ -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;

View File

@ -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<CompiledInstruction> {
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<Vec<CompiledInstruction>, 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<u8, CompileError> {
account_index_map
.get(key)
.cloned()
.ok_or(CompileError::UnknownInstructionKey(*key))
};
instructions
.iter()
@ -95,14 +125,14 @@ impl<'a> AccountKeys<'a> {
let accounts: Vec<u8> = 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::<Result<Vec<u8>, 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)
);
}
}

View File

@ -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<Pubkey>,
readonly_signer_keys: Vec<Pubkey>,
@ -12,6 +18,17 @@ pub(crate) struct CompiledKeys {
readonly_non_signer_keys: Vec<Pubkey>,
}
#[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<Pubkey>)> {
pub(crate) fn try_into_message_components(
self,
) -> Result<(MessageHeader, Vec<Pubkey>), CompileError> {
let try_into_u8 = |num: usize| -> Result<u8, CompileError> {
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<Option<(MessageAddressTableLookup, LoadedAddresses)>, 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<Pubkey>,
lookup_table_addresses: &[Pubkey],
) -> Result<(Vec<u8>, Vec<Pubkey>), 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)
);
}
}

View File

@ -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<VersionedTransaction> {
/// # 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<Self, CompileError> {
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<u8> {
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],
}],
})
);
}
}

View File

@ -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;
///