533 lines
18 KiB
Rust
533 lines
18 KiB
Rust
use {
|
|
crate::{
|
|
hash::Hash,
|
|
instruction::CompiledInstruction,
|
|
message::{
|
|
legacy,
|
|
v0::{self, LoadedAddresses},
|
|
AccountKeys, AddressLoader, AddressLoaderError, MessageHeader,
|
|
SanitizedVersionedMessage, VersionedMessage,
|
|
},
|
|
nonce::NONCED_TX_MARKER_IX_INDEX,
|
|
program_utils::limited_deserialize,
|
|
pubkey::Pubkey,
|
|
sanitize::{Sanitize, SanitizeError},
|
|
solana_program::{system_instruction::SystemInstruction, system_program},
|
|
sysvar::instructions::{BorrowedAccountMeta, BorrowedInstruction},
|
|
},
|
|
std::{borrow::Cow, convert::TryFrom},
|
|
thiserror::Error,
|
|
};
|
|
|
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
|
pub struct LegacyMessage<'a> {
|
|
/// Legacy message
|
|
pub message: Cow<'a, legacy::Message>,
|
|
/// List of boolean with same length as account_keys(), each boolean value indicates if
|
|
/// corresponding account key is writable or not.
|
|
pub is_writable_account_cache: Vec<bool>,
|
|
}
|
|
|
|
impl<'a> LegacyMessage<'a> {
|
|
pub fn new(message: legacy::Message) -> Self {
|
|
let is_writable_account_cache = message
|
|
.account_keys
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, _key)| message.is_writable(i))
|
|
.collect::<Vec<_>>();
|
|
Self {
|
|
message: Cow::Owned(message),
|
|
is_writable_account_cache,
|
|
}
|
|
}
|
|
|
|
pub fn has_duplicates(&self) -> bool {
|
|
self.message.has_duplicates()
|
|
}
|
|
|
|
pub fn is_key_called_as_program(&self, key_index: usize) -> bool {
|
|
self.message.is_key_called_as_program(key_index)
|
|
}
|
|
|
|
/// Inspect all message keys for the bpf upgradeable loader
|
|
pub fn is_upgradeable_loader_present(&self) -> bool {
|
|
self.message.is_upgradeable_loader_present()
|
|
}
|
|
|
|
/// Returns the full list of account keys.
|
|
pub fn account_keys(&self) -> AccountKeys {
|
|
AccountKeys::new(&self.message.account_keys, None)
|
|
}
|
|
|
|
pub fn is_writable(&self, index: usize) -> bool {
|
|
*self.is_writable_account_cache.get(index).unwrap_or(&false)
|
|
}
|
|
}
|
|
|
|
/// Sanitized message of a transaction.
|
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
|
pub enum SanitizedMessage {
|
|
/// Sanitized legacy message
|
|
Legacy(LegacyMessage<'static>),
|
|
/// Sanitized version #0 message with dynamically loaded addresses
|
|
V0(v0::LoadedMessage<'static>),
|
|
}
|
|
|
|
#[derive(PartialEq, Debug, Error, Eq, Clone)]
|
|
pub enum SanitizeMessageError {
|
|
#[error("index out of bounds")]
|
|
IndexOutOfBounds,
|
|
#[error("value out of bounds")]
|
|
ValueOutOfBounds,
|
|
#[error("invalid value")]
|
|
InvalidValue,
|
|
#[error("{0}")]
|
|
AddressLoaderError(#[from] AddressLoaderError),
|
|
}
|
|
|
|
impl From<SanitizeError> for SanitizeMessageError {
|
|
fn from(err: SanitizeError) -> Self {
|
|
match err {
|
|
SanitizeError::IndexOutOfBounds => Self::IndexOutOfBounds,
|
|
SanitizeError::ValueOutOfBounds => Self::ValueOutOfBounds,
|
|
SanitizeError::InvalidValue => Self::InvalidValue,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TryFrom<legacy::Message> for SanitizedMessage {
|
|
type Error = SanitizeMessageError;
|
|
fn try_from(message: legacy::Message) -> Result<Self, Self::Error> {
|
|
message.sanitize()?;
|
|
Ok(Self::Legacy(LegacyMessage::new(message)))
|
|
}
|
|
}
|
|
|
|
impl SanitizedMessage {
|
|
/// Create a sanitized message from a sanitized versioned message.
|
|
/// If the input message uses address tables, attempt to look up the
|
|
/// address for each table index.
|
|
pub fn try_new(
|
|
sanitized_msg: SanitizedVersionedMessage,
|
|
address_loader: impl AddressLoader,
|
|
) -> Result<Self, SanitizeMessageError> {
|
|
Ok(match sanitized_msg.message {
|
|
VersionedMessage::Legacy(message) => {
|
|
SanitizedMessage::Legacy(LegacyMessage::new(message))
|
|
}
|
|
VersionedMessage::V0(message) => {
|
|
let loaded_addresses =
|
|
address_loader.load_addresses(&message.address_table_lookups)?;
|
|
SanitizedMessage::V0(v0::LoadedMessage::new(message, loaded_addresses))
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Return true if this message contains duplicate account keys
|
|
pub fn has_duplicates(&self) -> bool {
|
|
match self {
|
|
SanitizedMessage::Legacy(message) => message.has_duplicates(),
|
|
SanitizedMessage::V0(message) => message.has_duplicates(),
|
|
}
|
|
}
|
|
|
|
/// Message header which identifies the number of signer and writable or
|
|
/// readonly accounts
|
|
pub fn header(&self) -> &MessageHeader {
|
|
match self {
|
|
Self::Legacy(legacy_message) => &legacy_message.message.header,
|
|
Self::V0(loaded_msg) => &loaded_msg.message.header,
|
|
}
|
|
}
|
|
|
|
/// Returns a legacy message if this sanitized message wraps one
|
|
pub fn legacy_message(&self) -> Option<&legacy::Message> {
|
|
if let Self::Legacy(legacy_message) = &self {
|
|
Some(&legacy_message.message)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Returns the fee payer for the transaction
|
|
pub fn fee_payer(&self) -> &Pubkey {
|
|
self.account_keys()
|
|
.get(0)
|
|
.expect("sanitized message always has non-program fee payer at index 0")
|
|
}
|
|
|
|
/// The hash of a recent block, used for timing out a transaction
|
|
pub fn recent_blockhash(&self) -> &Hash {
|
|
match self {
|
|
Self::Legacy(legacy_message) => &legacy_message.message.recent_blockhash,
|
|
Self::V0(loaded_msg) => &loaded_msg.message.recent_blockhash,
|
|
}
|
|
}
|
|
|
|
/// Program instructions that will be executed in sequence and committed in
|
|
/// one atomic transaction if all succeed.
|
|
pub fn instructions(&self) -> &[CompiledInstruction] {
|
|
match self {
|
|
Self::Legacy(legacy_message) => &legacy_message.message.instructions,
|
|
Self::V0(loaded_msg) => &loaded_msg.message.instructions,
|
|
}
|
|
}
|
|
|
|
/// Program instructions iterator which includes each instruction's program
|
|
/// id.
|
|
pub fn program_instructions_iter(
|
|
&self,
|
|
) -> impl Iterator<Item = (&Pubkey, &CompiledInstruction)> {
|
|
self.instructions().iter().map(move |ix| {
|
|
(
|
|
self.account_keys()
|
|
.get(usize::from(ix.program_id_index))
|
|
.expect("program id index is sanitized"),
|
|
ix,
|
|
)
|
|
})
|
|
}
|
|
|
|
/// Returns the list of account keys that are loaded for this message.
|
|
pub fn account_keys(&self) -> AccountKeys {
|
|
match self {
|
|
Self::Legacy(message) => message.account_keys(),
|
|
Self::V0(message) => message.account_keys(),
|
|
}
|
|
}
|
|
|
|
/// Returns true if the account at the specified index is an input to some
|
|
/// program instruction in this message.
|
|
fn is_key_passed_to_program(&self, key_index: usize) -> bool {
|
|
if let Ok(key_index) = u8::try_from(key_index) {
|
|
self.instructions()
|
|
.iter()
|
|
.any(|ix| ix.accounts.contains(&key_index))
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
/// Returns true if the account at the specified index is invoked as a
|
|
/// program in this message.
|
|
pub fn is_invoked(&self, key_index: usize) -> bool {
|
|
match self {
|
|
Self::Legacy(message) => message.is_key_called_as_program(key_index),
|
|
Self::V0(message) => message.is_key_called_as_program(key_index),
|
|
}
|
|
}
|
|
|
|
/// Returns true if the account at the specified index is not invoked as a
|
|
/// program or, if invoked, is passed to a program.
|
|
pub fn is_non_loader_key(&self, key_index: usize) -> bool {
|
|
!self.is_invoked(key_index) || self.is_key_passed_to_program(key_index)
|
|
}
|
|
|
|
/// Returns true if the account at the specified index is writable by the
|
|
/// instructions in this message.
|
|
pub fn is_writable(&self, index: usize) -> bool {
|
|
match self {
|
|
Self::Legacy(message) => message.is_writable(index),
|
|
Self::V0(message) => message.is_writable(index),
|
|
}
|
|
}
|
|
|
|
/// Returns true if the account at the specified index signed this
|
|
/// message.
|
|
pub fn is_signer(&self, index: usize) -> bool {
|
|
index < usize::from(self.header().num_required_signatures)
|
|
}
|
|
|
|
/// Return the resolved addresses for this message if it has any.
|
|
fn loaded_lookup_table_addresses(&self) -> Option<&LoadedAddresses> {
|
|
match &self {
|
|
SanitizedMessage::V0(message) => Some(&message.loaded_addresses),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Return the number of readonly accounts loaded by this message.
|
|
pub fn num_readonly_accounts(&self) -> usize {
|
|
let loaded_readonly_addresses = self
|
|
.loaded_lookup_table_addresses()
|
|
.map(|keys| keys.readonly.len())
|
|
.unwrap_or_default();
|
|
loaded_readonly_addresses
|
|
.saturating_add(usize::from(self.header().num_readonly_signed_accounts))
|
|
.saturating_add(usize::from(self.header().num_readonly_unsigned_accounts))
|
|
}
|
|
|
|
/// Decompile message instructions without cloning account keys
|
|
pub fn decompile_instructions(&self) -> Vec<BorrowedInstruction> {
|
|
let account_keys = self.account_keys();
|
|
self.program_instructions_iter()
|
|
.map(|(program_id, instruction)| {
|
|
let accounts = instruction
|
|
.accounts
|
|
.iter()
|
|
.map(|account_index| {
|
|
let account_index = *account_index as usize;
|
|
BorrowedAccountMeta {
|
|
is_signer: self.is_signer(account_index),
|
|
is_writable: self.is_writable(account_index),
|
|
pubkey: account_keys.get(account_index).unwrap(),
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
BorrowedInstruction {
|
|
accounts,
|
|
data: &instruction.data,
|
|
program_id,
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Inspect all message keys for the bpf upgradeable loader
|
|
pub fn is_upgradeable_loader_present(&self) -> bool {
|
|
match self {
|
|
Self::Legacy(message) => message.is_upgradeable_loader_present(),
|
|
Self::V0(message) => message.is_upgradeable_loader_present(),
|
|
}
|
|
}
|
|
|
|
/// Get a list of signers for the instruction at the given index
|
|
pub fn get_ix_signers(&self, ix_index: usize) -> impl Iterator<Item = &Pubkey> {
|
|
self.instructions()
|
|
.get(ix_index)
|
|
.into_iter()
|
|
.flat_map(|ix| {
|
|
ix.accounts
|
|
.iter()
|
|
.copied()
|
|
.map(usize::from)
|
|
.filter(|index| self.is_signer(*index))
|
|
.filter_map(|signer_index| self.account_keys().get(signer_index))
|
|
})
|
|
}
|
|
|
|
/// If the message uses a durable nonce, return the pubkey of the nonce account
|
|
pub fn get_durable_nonce(&self) -> Option<&Pubkey> {
|
|
self.instructions()
|
|
.get(NONCED_TX_MARKER_IX_INDEX as usize)
|
|
.filter(
|
|
|ix| match self.account_keys().get(ix.program_id_index as usize) {
|
|
Some(program_id) => system_program::check_id(program_id),
|
|
_ => false,
|
|
},
|
|
)
|
|
.filter(|ix| {
|
|
matches!(
|
|
limited_deserialize(&ix.data, 4 /* serialized size of AdvanceNonceAccount */),
|
|
Ok(SystemInstruction::AdvanceNonceAccount)
|
|
)
|
|
})
|
|
.and_then(|ix| {
|
|
ix.accounts.first().and_then(|idx| {
|
|
let idx = *idx as usize;
|
|
if !self.is_writable(idx) {
|
|
None
|
|
} else {
|
|
self.account_keys().get(idx)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use {super::*, crate::message::v0, std::collections::HashSet};
|
|
|
|
#[test]
|
|
fn test_try_from_message() {
|
|
let legacy_message_with_no_signers = legacy::Message {
|
|
account_keys: vec![Pubkey::new_unique()],
|
|
..legacy::Message::default()
|
|
};
|
|
|
|
assert_eq!(
|
|
SanitizedMessage::try_from(legacy_message_with_no_signers).err(),
|
|
Some(SanitizeMessageError::IndexOutOfBounds),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_is_non_loader_key() {
|
|
let key0 = Pubkey::new_unique();
|
|
let key1 = Pubkey::new_unique();
|
|
let loader_key = Pubkey::new_unique();
|
|
let instructions = vec![
|
|
CompiledInstruction::new(1, &(), vec![0]),
|
|
CompiledInstruction::new(2, &(), vec![0, 1]),
|
|
];
|
|
|
|
let message = SanitizedMessage::try_from(legacy::Message::new_with_compiled_instructions(
|
|
1,
|
|
0,
|
|
2,
|
|
vec![key0, key1, loader_key],
|
|
Hash::default(),
|
|
instructions,
|
|
))
|
|
.unwrap();
|
|
|
|
assert!(message.is_non_loader_key(0));
|
|
assert!(message.is_non_loader_key(1));
|
|
assert!(!message.is_non_loader_key(2));
|
|
}
|
|
|
|
#[test]
|
|
fn test_num_readonly_accounts() {
|
|
let key0 = Pubkey::new_unique();
|
|
let key1 = Pubkey::new_unique();
|
|
let key2 = Pubkey::new_unique();
|
|
let key3 = Pubkey::new_unique();
|
|
let key4 = Pubkey::new_unique();
|
|
let key5 = Pubkey::new_unique();
|
|
|
|
let legacy_message = SanitizedMessage::try_from(legacy::Message {
|
|
header: MessageHeader {
|
|
num_required_signatures: 2,
|
|
num_readonly_signed_accounts: 1,
|
|
num_readonly_unsigned_accounts: 1,
|
|
},
|
|
account_keys: vec![key0, key1, key2, key3],
|
|
..legacy::Message::default()
|
|
})
|
|
.unwrap();
|
|
|
|
assert_eq!(legacy_message.num_readonly_accounts(), 2);
|
|
|
|
let v0_message = SanitizedMessage::V0(v0::LoadedMessage::new(
|
|
v0::Message {
|
|
header: MessageHeader {
|
|
num_required_signatures: 2,
|
|
num_readonly_signed_accounts: 1,
|
|
num_readonly_unsigned_accounts: 1,
|
|
},
|
|
account_keys: vec![key0, key1, key2, key3],
|
|
..v0::Message::default()
|
|
},
|
|
LoadedAddresses {
|
|
writable: vec![key4],
|
|
readonly: vec![key5],
|
|
},
|
|
));
|
|
|
|
assert_eq!(v0_message.num_readonly_accounts(), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn test_get_ix_signers() {
|
|
let signer0 = Pubkey::new_unique();
|
|
let signer1 = Pubkey::new_unique();
|
|
let non_signer = Pubkey::new_unique();
|
|
let loader_key = Pubkey::new_unique();
|
|
let instructions = vec![
|
|
CompiledInstruction::new(3, &(), vec![2, 0]),
|
|
CompiledInstruction::new(3, &(), vec![0, 1]),
|
|
CompiledInstruction::new(3, &(), vec![0, 0]),
|
|
];
|
|
|
|
let message = SanitizedMessage::try_from(legacy::Message::new_with_compiled_instructions(
|
|
2,
|
|
1,
|
|
2,
|
|
vec![signer0, signer1, non_signer, loader_key],
|
|
Hash::default(),
|
|
instructions,
|
|
))
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
message.get_ix_signers(0).collect::<HashSet<_>>(),
|
|
HashSet::from_iter([&signer0])
|
|
);
|
|
assert_eq!(
|
|
message.get_ix_signers(1).collect::<HashSet<_>>(),
|
|
HashSet::from_iter([&signer0, &signer1])
|
|
);
|
|
assert_eq!(
|
|
message.get_ix_signers(2).collect::<HashSet<_>>(),
|
|
HashSet::from_iter([&signer0])
|
|
);
|
|
assert_eq!(
|
|
message.get_ix_signers(3).collect::<HashSet<_>>(),
|
|
HashSet::default()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[allow(clippy::get_first)]
|
|
fn test_is_writable_account_cache() {
|
|
let key0 = Pubkey::new_unique();
|
|
let key1 = Pubkey::new_unique();
|
|
let key2 = Pubkey::new_unique();
|
|
let key3 = Pubkey::new_unique();
|
|
let key4 = Pubkey::new_unique();
|
|
let key5 = Pubkey::new_unique();
|
|
|
|
let legacy_message = SanitizedMessage::try_from(legacy::Message {
|
|
header: MessageHeader {
|
|
num_required_signatures: 2,
|
|
num_readonly_signed_accounts: 1,
|
|
num_readonly_unsigned_accounts: 1,
|
|
},
|
|
account_keys: vec![key0, key1, key2, key3],
|
|
..legacy::Message::default()
|
|
})
|
|
.unwrap();
|
|
match legacy_message {
|
|
SanitizedMessage::Legacy(message) => {
|
|
assert_eq!(
|
|
message.is_writable_account_cache.len(),
|
|
message.account_keys().len()
|
|
);
|
|
assert!(message.is_writable_account_cache.get(0).unwrap());
|
|
assert!(!message.is_writable_account_cache.get(1).unwrap());
|
|
assert!(message.is_writable_account_cache.get(2).unwrap());
|
|
assert!(!message.is_writable_account_cache.get(3).unwrap());
|
|
}
|
|
_ => {
|
|
panic!("Expect to be SanitizedMessage::LegacyMessage")
|
|
}
|
|
}
|
|
|
|
let v0_message = SanitizedMessage::V0(v0::LoadedMessage::new(
|
|
v0::Message {
|
|
header: MessageHeader {
|
|
num_required_signatures: 2,
|
|
num_readonly_signed_accounts: 1,
|
|
num_readonly_unsigned_accounts: 1,
|
|
},
|
|
account_keys: vec![key0, key1, key2, key3],
|
|
..v0::Message::default()
|
|
},
|
|
LoadedAddresses {
|
|
writable: vec![key4],
|
|
readonly: vec![key5],
|
|
},
|
|
));
|
|
match v0_message {
|
|
SanitizedMessage::V0(message) => {
|
|
assert_eq!(
|
|
message.is_writable_account_cache.len(),
|
|
message.account_keys().len()
|
|
);
|
|
assert!(message.is_writable_account_cache.get(0).unwrap());
|
|
assert!(!message.is_writable_account_cache.get(1).unwrap());
|
|
assert!(message.is_writable_account_cache.get(2).unwrap());
|
|
assert!(!message.is_writable_account_cache.get(3).unwrap());
|
|
assert!(message.is_writable_account_cache.get(4).unwrap());
|
|
assert!(!message.is_writable_account_cache.get(5).unwrap());
|
|
}
|
|
_ => {
|
|
panic!("Expect to be SanitizedMessage::V0")
|
|
}
|
|
}
|
|
}
|
|
}
|