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:
parent
9abebc2d64
commit
88326533ed
|
@ -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;
|
||||
///
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
use solana_program::pubkey::Pubkey;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct AddressLookupTableAccount {
|
||||
pub key: Pubkey,
|
||||
pub addresses: Vec<Pubkey>,
|
||||
}
|
|
@ -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()),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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],
|
||||
}],
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
///
|
||||
|
|
Loading…
Reference in New Issue