#![allow(clippy::arithmetic_side_effects)]
use std::{
collections::HashSet,
convert::TryFrom,
str::FromStr,
sync::LazyLock,
time::{SystemTime, UNIX_EPOCH},
};
#[allow(deprecated)]
pub use builtins::{BUILTIN_PROGRAMS_KEYS, MAYBE_BUILTIN_KEY_OR_SYSVAR};
#[cfg(feature = "frozen-abi")]
use rialo_frozen_abi_macro::{frozen_abi, AbiExample};
#[allow(unused_imports)]
use rialo_hash::Hash;
use rialo_s_instruction::Instruction;
use rialo_s_pubkey::Pubkey;
use rialo_s_sdk_ids::{
bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable, system_program, sysvar,
};
use rialo_sanitize::{Sanitize, SanitizeError};
#[cfg(feature = "serde")]
use serde_derive::{Deserialize, Serialize};
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
use wasm_bindgen::prelude::wasm_bindgen;
use crate::{
compiled_instruction::CompiledInstruction, compiled_keys::CompiledKeys, ConfigHashPrefix,
MessageHeader,
};
static ALL_IDS: LazyLock<Vec<Pubkey>> = LazyLock::new(|| {
vec![
sysvar::clock::id(),
sysvar::epoch_schedule::id(),
sysvar::fees::id(),
sysvar::recent_blockhashes::id(),
sysvar::rent::id(),
sysvar::rewards::id(),
sysvar::stake_history::id(),
sysvar::instructions::id(),
]
});
fn is_sysvar_id(id: &Pubkey) -> bool {
ALL_IDS.iter().any(|key| key == id)
}
#[deprecated(
since = "2.0.0",
note = "please use `rialo_s_sdk::reserved_account_keys::ReservedAccountKeys` instead"
)]
#[allow(deprecated)]
mod builtins {
use std::sync::LazyLock;
use super::*;
pub static BUILTIN_PROGRAMS_KEYS: LazyLock<[Pubkey; 9]> = LazyLock::new(|| {
let parse = |s| Pubkey::from_str(s).unwrap();
[
parse("Config1111111111111111111111111111111111111"),
parse("Feature111111111111111111111111111111111111"),
parse("NativeLoader1111111111111111111111111111111"),
parse("Stake11111111111111111111111111111111111111"),
parse("Vote111111111111111111111111111111111111111"),
system_program::id(),
bpf_loader::id(),
bpf_loader_deprecated::id(),
bpf_loader_upgradeable::id(),
]
});
pub static MAYBE_BUILTIN_KEY_OR_SYSVAR: LazyLock<[bool; 256]> = LazyLock::new(|| {
let mut temp_table: [bool; 256] = [false; 256];
BUILTIN_PROGRAMS_KEYS
.iter()
.for_each(|key| temp_table[key.as_ref()[0] as usize] = true);
ALL_IDS
.iter()
.for_each(|key| temp_table[key.as_ref()[0] as usize] = true);
temp_table
});
}
#[deprecated(
since = "2.0.0",
note = "please use `rialo_s_sdk::reserved_account_keys::ReservedAccountKeys::is_reserved` instead"
)]
#[allow(deprecated)]
pub fn is_builtin_key_or_sysvar(key: &Pubkey) -> bool {
if MAYBE_BUILTIN_KEY_OR_SYSVAR[key.as_ref()[0] as usize] {
return is_sysvar_id(key) || BUILTIN_PROGRAMS_KEYS.contains(key);
}
false
}
fn position(keys: &[Pubkey], key: &Pubkey) -> u8 {
keys.iter().position(|k| k == key).unwrap() as u8
}
fn compile_instruction(ix: &Instruction, keys: &[Pubkey]) -> CompiledInstruction {
let accounts: Vec<_> = ix
.accounts
.iter()
.map(|account_meta| position(keys, &account_meta.pubkey))
.collect();
CompiledInstruction {
program_id_index: position(keys, &ix.program_id),
data: ix.data.clone(),
accounts,
}
}
fn compile_instructions(ixs: &[Instruction], keys: &[Pubkey]) -> Vec<CompiledInstruction> {
ixs.iter().map(|ix| compile_instruction(ix, keys)).collect()
}
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
#[cfg_attr(
feature = "frozen-abi",
frozen_abi(digest = "2THeaWnXSGDTsiadKytJTcbjrk4KjfMww9arRLZcwGnw"),
derive(AbiExample)
)]
#[cfg_attr(
feature = "serde",
derive(Deserialize, Serialize),
serde(rename_all = "camelCase")
)]
#[derive(Default, Debug, PartialEq, Eq, Clone)]
pub struct Message {
pub header: MessageHeader,
#[cfg_attr(feature = "serde", serde(with = "rialo_s_short_vec"))]
pub account_keys: Vec<Pubkey>,
pub valid_from: i64,
pub config_hash_prefix: ConfigHashPrefix,
pub occ: bool,
#[cfg_attr(feature = "serde", serde(with = "rialo_s_short_vec"))]
pub instructions: Vec<CompiledInstruction>,
}
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
#[wasm_bindgen]
#[cfg_attr(
feature = "frozen-abi",
frozen_abi(digest = "2THeaWnXSGDTsiadKytJTcbjrk4KjfMww9arRLZcwGnw"),
derive(AbiExample)
)]
#[cfg_attr(
feature = "serde",
derive(Deserialize, Serialize),
serde(rename_all = "camelCase")
)]
#[derive(Default, Debug, PartialEq, Eq, Clone)]
pub struct Message {
#[wasm_bindgen(skip)]
pub header: MessageHeader,
#[wasm_bindgen(skip)]
#[cfg_attr(feature = "serde", serde(with = "rialo_s_short_vec"))]
pub account_keys: Vec<Pubkey>,
pub valid_from: i64,
#[wasm_bindgen(skip)]
pub config_hash_prefix: ConfigHashPrefix,
#[wasm_bindgen(skip)]
pub occ: bool,
#[wasm_bindgen(skip)]
#[cfg_attr(feature = "serde", serde(with = "rialo_s_short_vec"))]
pub instructions: Vec<CompiledInstruction>,
}
impl Sanitize for Message {
fn sanitize(&self) -> std::result::Result<(), SanitizeError> {
if self.header.num_required_signatures as usize
+ self.header.num_readonly_unsigned_accounts as usize
> self.account_keys.len()
{
return Err(SanitizeError::IndexOutOfBounds);
}
if self.header.num_readonly_signed_accounts >= self.header.num_required_signatures {
return Err(SanitizeError::IndexOutOfBounds);
}
for ci in &self.instructions {
if ci.program_id_index as usize >= self.account_keys.len() {
return Err(SanitizeError::IndexOutOfBounds);
}
if ci.program_id_index == 0 {
return Err(SanitizeError::IndexOutOfBounds);
}
for ai in &ci.accounts {
if *ai as usize >= self.account_keys.len() {
return Err(SanitizeError::IndexOutOfBounds);
}
}
}
self.account_keys.sanitize()?;
self.instructions.sanitize()?;
Ok(())
}
}
impl Message {
pub fn new(
instructions: &[Instruction],
payer: Option<&Pubkey>,
config_hash_prefix: ConfigHashPrefix,
occ: bool,
) -> Self {
let valid_from = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Failed to get current time")
.as_millis()
.try_into()
.unwrap_or_default();
Self::new_with_valid_from(instructions, payer, valid_from, config_hash_prefix, occ)
}
pub fn new_with_valid_from(
instructions: &[Instruction],
payer: Option<&Pubkey>,
valid_from: i64,
config_hash_prefix: ConfigHashPrefix,
occ: bool,
) -> Self {
let compiled_keys = CompiledKeys::compile(instructions, payer.cloned());
let (header, account_keys) = compiled_keys
.try_into_message_components()
.expect("overflow when compiling message keys");
let instructions = compile_instructions(instructions, &account_keys);
Self::new_with_compiled_instructions(
header.num_required_signatures,
header.num_readonly_signed_accounts,
header.num_readonly_unsigned_accounts,
account_keys,
valid_from,
config_hash_prefix,
occ,
instructions,
)
}
pub fn new_with_compiled_instructions(
num_required_signatures: u8,
num_readonly_signed_accounts: u8,
num_readonly_unsigned_accounts: u8,
account_keys: Vec<Pubkey>,
valid_from: i64,
config_hash_prefix: ConfigHashPrefix,
occ: bool,
instructions: Vec<CompiledInstruction>,
) -> Self {
Self {
header: MessageHeader {
num_required_signatures,
num_readonly_signed_accounts,
num_readonly_unsigned_accounts,
},
account_keys,
valid_from,
config_hash_prefix,
occ,
instructions,
}
}
#[cfg(all(not(target_os = "solana"), feature = "bincode", feature = "blake3"))]
pub fn hash(&self) -> Hash {
let message_bytes = self.serialize();
Self::hash_raw_message(&message_bytes)
}
#[cfg(all(not(target_os = "solana"), feature = "blake3"))]
pub fn hash_raw_message(message_bytes: &[u8]) -> Hash {
use blake3::traits::digest::Digest;
use rialo_hash::HASH_BYTES;
let mut hasher = blake3::Hasher::new();
hasher.update(b"solana-tx-message-v1");
hasher.update(message_bytes);
let hash_bytes: [u8; HASH_BYTES] = hasher.finalize().into();
hash_bytes.into()
}
pub fn compile_instruction(&self, ix: &Instruction) -> CompiledInstruction {
compile_instruction(ix, &self.account_keys)
}
#[cfg(feature = "bincode")]
pub fn serialize(&self) -> Vec<u8> {
bincode::serialize(self).unwrap()
}
pub fn config_hash_prefix(&self) -> ConfigHashPrefix {
self.config_hash_prefix
}
pub fn program_id(&self, instruction_index: usize) -> Option<&Pubkey> {
Some(
&self.account_keys[self.instructions.get(instruction_index)?.program_id_index as usize],
)
}
pub fn program_index(&self, instruction_index: usize) -> Option<usize> {
Some(self.instructions.get(instruction_index)?.program_id_index as usize)
}
pub fn program_ids(&self) -> Vec<&Pubkey> {
self.instructions
.iter()
.map(|ix| &self.account_keys[ix.program_id_index as usize])
.collect()
}
#[deprecated(since = "2.0.0", note = "Please use `is_instruction_account` instead")]
pub fn is_key_passed_to_program(&self, key_index: usize) -> bool {
self.is_instruction_account(key_index)
}
pub fn is_instruction_account(&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
}
}
pub fn is_key_called_as_program(&self, key_index: usize) -> bool {
if let Ok(key_index) = u8::try_from(key_index) {
self.instructions
.iter()
.any(|ix| ix.program_id_index == key_index)
} else {
false
}
}
#[deprecated(
since = "2.0.0",
note = "Please use `is_key_called_as_program` and `is_instruction_account` directly"
)]
pub fn is_non_loader_key(&self, key_index: usize) -> bool {
!self.is_key_called_as_program(key_index) || self.is_instruction_account(key_index)
}
pub fn program_position(&self, index: usize) -> Option<usize> {
let program_ids = self.program_ids();
program_ids
.iter()
.position(|&&pubkey| pubkey == self.account_keys[index])
}
pub fn maybe_executable(&self, i: usize) -> bool {
self.program_position(i).is_some()
}
pub fn demote_program_id(&self, i: usize) -> bool {
self.is_key_called_as_program(i) && !self.is_upgradeable_loader_present()
}
pub(super) fn is_writable_index(&self, i: usize) -> bool {
i < (self.header.num_required_signatures - self.header.num_readonly_signed_accounts)
as usize
|| (i >= self.header.num_required_signatures as usize
&& i < self.account_keys.len()
- self.header.num_readonly_unsigned_accounts as usize)
}
#[deprecated(since = "2.0.0", note = "Please use `is_maybe_writable` instead")]
#[allow(deprecated)]
pub fn is_writable(&self, i: usize) -> bool {
(self.is_writable_index(i))
&& !is_builtin_key_or_sysvar(&self.account_keys[i])
&& !self.demote_program_id(i)
}
pub fn is_maybe_writable(
&self,
i: usize,
reserved_account_keys: Option<&HashSet<Pubkey>>,
) -> bool {
(self.is_writable_index(i))
&& !self.is_account_maybe_reserved(i, reserved_account_keys)
&& !self.demote_program_id(i)
}
fn is_account_maybe_reserved(
&self,
key_index: usize,
reserved_account_keys: Option<&HashSet<Pubkey>>,
) -> bool {
let mut is_maybe_reserved = false;
if let Some(reserved_account_keys) = reserved_account_keys {
if let Some(key) = self.account_keys.get(key_index) {
is_maybe_reserved = reserved_account_keys.contains(key);
}
}
is_maybe_reserved
}
pub fn is_signer(&self, i: usize) -> bool {
i < self.header.num_required_signatures as usize
}
pub fn signer_keys(&self) -> Vec<&Pubkey> {
let last_key = self
.account_keys
.len()
.min(self.header.num_required_signatures as usize);
self.account_keys[..last_key].iter().collect()
}
pub fn has_duplicates(&self) -> bool {
for i in 1..self.account_keys.len() {
#[allow(clippy::arithmetic_side_effects)]
if self.account_keys[i..].contains(&self.account_keys[i - 1]) {
return true;
}
}
false
}
pub fn is_upgradeable_loader_present(&self) -> bool {
self.account_keys
.iter()
.any(|&key| key == bpf_loader_upgradeable::id())
}
}
#[cfg(test)]
mod tests {
#![allow(deprecated)]
use std::collections::HashSet;
use rialo_s_instruction::AccountMeta;
use rialo_s_sha256_hasher::hash;
use super::*;
use crate::MESSAGE_HEADER_LENGTH;
fn now_unix_epoch() -> i64 {
::std::time::SystemTime::now()
.duration_since(::std::time::UNIX_EPOCH)
.expect("Couldn't get UNIX timestamp")
.as_secs()
.try_into()
.unwrap()
}
#[test]
#[ignore] fn test_builtin_program_keys() {
let keys: HashSet<Pubkey> = BUILTIN_PROGRAMS_KEYS.iter().copied().collect();
assert_eq!(keys.len(), 10);
for k in keys {
let k = format!("{k}");
assert!(k.ends_with("11111111111111111111111"));
}
}
#[test]
#[ignore] fn test_builtin_program_keys_abi_freeze() {
let builtins = format!("{:?}", *BUILTIN_PROGRAMS_KEYS);
assert_eq!(
format!("{}", hash(builtins.as_bytes())),
"ACqmMkYbo9eqK6QrRSrB3HLyR6uHhLf31SCfGUAJjiWj"
);
}
#[test]
fn test_message_signed_keys_len() {
let program_id = Pubkey::default();
let id0 = Pubkey::default();
let ix = Instruction::new_with_bincode(program_id, &0, vec![AccountMeta::new(id0, false)]);
let message = Message::new(&[ix], None, ConfigHashPrefix::new(0), false);
assert_eq!(message.header.num_required_signatures, 0);
let ix = Instruction::new_with_bincode(program_id, &0, vec![AccountMeta::new(id0, true)]);
let message = Message::new(&[ix], Some(&id0), ConfigHashPrefix::new(0), false);
assert_eq!(message.header.num_required_signatures, 1);
}
#[test]
fn test_message_kitchen_sink() {
let program_id0 = Pubkey::new_unique();
let program_id1 = Pubkey::new_unique();
let id0 = Pubkey::default();
let id1 = Pubkey::new_unique();
let message = Message::new(
&[
Instruction::new_with_bincode(program_id0, &0, vec![AccountMeta::new(id0, false)]),
Instruction::new_with_bincode(program_id1, &0, vec![AccountMeta::new(id1, true)]),
Instruction::new_with_bincode(program_id0, &0, vec![AccountMeta::new(id1, false)]),
],
Some(&id1),
ConfigHashPrefix::new(0),
false,
);
assert_eq!(
message.instructions[0],
CompiledInstruction::new(2, &0, vec![1])
);
assert_eq!(
message.instructions[1],
CompiledInstruction::new(3, &0, vec![0])
);
assert_eq!(
message.instructions[2],
CompiledInstruction::new(2, &0, vec![0])
);
}
#[test]
fn test_message_payer_first() {
let program_id = Pubkey::default();
let payer = Pubkey::new_unique();
let id0 = Pubkey::default();
let ix = Instruction::new_with_bincode(program_id, &0, vec![AccountMeta::new(id0, false)]);
let message = Message::new(&[ix], Some(&payer), ConfigHashPrefix::new(0), false);
assert_eq!(message.header.num_required_signatures, 1);
let ix = Instruction::new_with_bincode(program_id, &0, vec![AccountMeta::new(id0, true)]);
let message = Message::new(&[ix], Some(&payer), ConfigHashPrefix::new(0), false);
assert_eq!(message.header.num_required_signatures, 2);
let ix = Instruction::new_with_bincode(
program_id,
&0,
vec![AccountMeta::new(payer, true), AccountMeta::new(id0, true)],
);
let message = Message::new(&[ix], Some(&payer), ConfigHashPrefix::new(0), false);
assert_eq!(message.header.num_required_signatures, 2);
}
#[test]
fn test_program_position() {
let program_id0 = Pubkey::default();
let program_id1 = Pubkey::new_unique();
let id = Pubkey::new_unique();
let message = Message::new(
&[
Instruction::new_with_bincode(program_id0, &0, vec![AccountMeta::new(id, false)]),
Instruction::new_with_bincode(program_id1, &0, vec![AccountMeta::new(id, true)]),
],
Some(&id),
ConfigHashPrefix::new(0),
false,
);
assert_eq!(message.program_position(0), None);
assert_eq!(message.program_position(1), Some(0));
assert_eq!(message.program_position(2), Some(1));
}
#[test]
fn test_is_writable() {
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 message = Message {
header: MessageHeader {
num_required_signatures: 3,
num_readonly_signed_accounts: 2,
num_readonly_unsigned_accounts: 1,
},
account_keys: vec![key0, key1, key2, key3, key4, key5],
valid_from: now_unix_epoch(),
config_hash_prefix: ConfigHashPrefix::new(0),
occ: false,
instructions: vec![],
};
assert!(message.is_writable(0));
assert!(!message.is_writable(1));
assert!(!message.is_writable(2));
assert!(message.is_writable(3));
assert!(message.is_writable(4));
assert!(!message.is_writable(5));
}
#[test]
fn test_is_maybe_writable() {
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 message = Message {
header: MessageHeader {
num_required_signatures: 3,
num_readonly_signed_accounts: 2,
num_readonly_unsigned_accounts: 1,
},
account_keys: vec![key0, key1, key2, key3, key4, key5],
valid_from: now_unix_epoch(),
config_hash_prefix: ConfigHashPrefix::new(0),
occ: false,
instructions: vec![],
};
let reserved_account_keys = HashSet::from([key3]);
assert!(message.is_maybe_writable(0, Some(&reserved_account_keys)));
assert!(!message.is_maybe_writable(1, Some(&reserved_account_keys)));
assert!(!message.is_maybe_writable(2, Some(&reserved_account_keys)));
assert!(!message.is_maybe_writable(3, Some(&reserved_account_keys)));
assert!(message.is_maybe_writable(3, None));
assert!(message.is_maybe_writable(4, Some(&reserved_account_keys)));
assert!(!message.is_maybe_writable(5, Some(&reserved_account_keys)));
assert!(!message.is_maybe_writable(6, Some(&reserved_account_keys)));
}
#[test]
fn test_is_account_maybe_reserved() {
let key0 = Pubkey::new_unique();
let key1 = Pubkey::new_unique();
let message = Message {
account_keys: vec![key0, key1],
..Message::default()
};
let reserved_account_keys = HashSet::from([key1]);
assert!(!message.is_account_maybe_reserved(0, Some(&reserved_account_keys)));
assert!(message.is_account_maybe_reserved(1, Some(&reserved_account_keys)));
assert!(!message.is_account_maybe_reserved(2, Some(&reserved_account_keys)));
assert!(!message.is_account_maybe_reserved(0, None));
assert!(!message.is_account_maybe_reserved(1, None));
assert!(!message.is_account_maybe_reserved(2, None));
}
#[test]
fn test_program_ids() {
let key0 = Pubkey::new_unique();
let key1 = Pubkey::new_unique();
let loader2 = Pubkey::new_unique();
let instructions = vec![CompiledInstruction::new(2, &(), vec![0, 1])];
let message = Message::new_with_compiled_instructions(
1,
0,
2,
vec![key0, key1, loader2],
now_unix_epoch(),
ConfigHashPrefix::new(0),
false,
instructions,
);
assert_eq!(message.program_ids(), vec![&loader2]);
}
#[test]
fn test_is_instruction_account() {
let key0 = Pubkey::new_unique();
let key1 = Pubkey::new_unique();
let loader2 = Pubkey::new_unique();
let instructions = vec![CompiledInstruction::new(2, &(), vec![0, 1])];
let message = Message::new_with_compiled_instructions(
1,
0,
2,
vec![key0, key1, loader2],
now_unix_epoch(),
ConfigHashPrefix::new(0),
false,
instructions,
);
assert!(message.is_instruction_account(0));
assert!(message.is_instruction_account(1));
assert!(!message.is_instruction_account(2));
}
#[test]
fn test_is_non_loader_key() {
#![allow(deprecated)]
let key0 = Pubkey::new_unique();
let key1 = Pubkey::new_unique();
let loader2 = Pubkey::new_unique();
let instructions = vec![CompiledInstruction::new(2, &(), vec![0, 1])];
let message = Message::new_with_compiled_instructions(
1,
0,
2,
vec![key0, key1, loader2],
now_unix_epoch(),
ConfigHashPrefix::new(0),
false,
instructions,
);
assert!(message.is_non_loader_key(0));
assert!(message.is_non_loader_key(1));
assert!(!message.is_non_loader_key(2));
}
#[test]
fn test_message_header_len_constant() {
assert_eq!(
bincode::serialized_size(&MessageHeader::default()).unwrap() as usize,
MESSAGE_HEADER_LENGTH
);
}
#[test]
#[ignore] fn test_message_hash() {
let program_id0 = Pubkey::from_str("4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM").unwrap();
let program_id1 = Pubkey::from_str("8opHzTAnfzRpPEx21XtnrVTX28YQuCpAjcn1PczScKh").unwrap();
let id0 = Pubkey::from_str("CiDwVBFgWV9E5MvXWoLgnEgn2hK7rJikbvfWavzAQz3").unwrap();
let id1 = Pubkey::from_str("GcdayuLaLyrdmUu324nahyv33G5poQdLUEZ1nEytDeP").unwrap();
let id2 = Pubkey::from_str("LX3EUdRUBUa3TbsYXLEUdj9J3prXkWXvLYSWyYyc2Jj").unwrap();
let id3 = Pubkey::from_str("QRSsyMWN1yHT9ir42bgNZUNZ4PdEhcSWCrL2AryKpy5").unwrap();
let instructions = vec![
Instruction::new_with_bincode(program_id0, &0, vec![AccountMeta::new(id0, false)]),
Instruction::new_with_bincode(program_id0, &0, vec![AccountMeta::new(id1, true)]),
Instruction::new_with_bincode(
program_id1,
&0,
vec![AccountMeta::new_readonly(id2, false)],
),
Instruction::new_with_bincode(
program_id1,
&0,
vec![AccountMeta::new_readonly(id3, true)],
),
];
let message = Message::new(&instructions, Some(&id1), ConfigHashPrefix::new(0), false);
assert_eq!(
message.hash(),
Hash::from_str("7VWCF4quo2CcWQFNUayZiorxpiR5ix8YzLebrXKf3fMF").unwrap()
)
}
#[test]
#[ignore] fn test_inline_all_ids() {
assert_eq!(rialo_s_sysvar::ALL_IDS.to_vec(), ALL_IDS.to_vec());
}
}