#[cfg(test)]
pub use self::tests::MessageBuilder;
#[cfg(feature = "serde")]
use serde_derive::{Deserialize, Serialize};
#[cfg(feature = "frozen-abi")]
use solana_frozen_abi_macro::AbiExample;
#[cfg(feature = "wincode")]
use {
crate::v1::{InstructionHeader, FIXED_HEADER_SIZE},
core::{mem::MaybeUninit, slice::from_raw_parts},
wincode::{
config::{Config, ConfigCore},
context,
io::{Reader, Writer},
len::SeqLen,
ReadResult, SchemaRead, SchemaReadContext, SchemaWrite, WriteResult,
},
};
use {
crate::{
compiled_instruction::CompiledInstruction,
compiled_keys::CompiledKeys,
v1::{
MessageError, TransactionConfig, TransactionConfigMask, MAX_ADDRESSES,
MAX_INSTRUCTIONS, MAX_SIGNATURES,
},
AccountKeys, CompileError, MessageHeader,
},
core::mem::size_of,
solana_address::Address,
solana_hash::Hash,
solana_instruction::Instruction,
solana_sanitize::{Sanitize, SanitizeError},
std::collections::HashSet,
};
#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(rename_all = "camelCase")
)]
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Message {
pub header: MessageHeader,
pub config: TransactionConfig,
pub lifetime_specifier: Hash,
pub account_keys: Vec<Address>,
pub instructions: Vec<CompiledInstruction>,
}
impl Message {
pub fn new(
header: MessageHeader,
config: TransactionConfig,
lifetime_specifier: Hash,
account_keys: Vec<Address>,
instructions: Vec<CompiledInstruction>,
) -> Self {
Self {
header,
config,
lifetime_specifier,
account_keys,
instructions,
}
}
pub fn try_compile(
payer: &Address,
instructions: &[Instruction],
recent_blockhash: Hash,
) -> Result<Self, CompileError> {
Self::try_compile_with_config(
payer,
instructions,
recent_blockhash,
TransactionConfig::empty(),
)
}
pub fn try_compile_with_config(
payer: &Address,
instructions: &[Instruction],
recent_blockhash: Hash,
config: TransactionConfig,
) -> Result<Self, CompileError> {
let compiled_keys = CompiledKeys::compile(instructions, Some(*payer));
let (header, static_keys) = compiled_keys.try_into_message_components()?;
let account_keys = AccountKeys::new(&static_keys, None);
let instructions = account_keys.try_compile_instructions(instructions)?;
Ok(Self {
header,
config,
lifetime_specifier: recent_blockhash,
account_keys: static_keys,
instructions,
})
}
pub fn fee_payer(&self) -> Option<&Address> {
self.account_keys.first()
}
pub fn is_signer(&self, index: usize) -> bool {
index < usize::from(self.header.num_required_signatures)
}
pub fn is_signer_writable(&self, index: usize) -> bool {
if !self.is_signer(index) {
return false;
}
let num_writable_signers = usize::from(self.header.num_required_signatures)
.saturating_sub(usize::from(self.header.num_readonly_signed_accounts));
index < num_writable_signers
}
pub fn is_key_called_as_program(&self, key_index: usize) -> bool {
crate::is_key_called_as_program(&self.instructions, key_index)
}
#[inline(always)]
pub(crate) fn is_writable_index(&self, i: usize) -> bool {
crate::is_writable_index(i, self.header, &self.account_keys)
}
pub fn is_upgradeable_loader_present(&self) -> bool {
crate::is_upgradeable_loader_present(&self.account_keys)
}
pub fn is_maybe_writable(
&self,
key_index: usize,
reserved_account_keys: Option<&HashSet<Address>>,
) -> bool {
crate::is_maybe_writable(
key_index,
self.header,
&self.account_keys,
&self.instructions,
reserved_account_keys,
)
}
pub fn demote_program_id(&self, i: usize) -> bool {
crate::is_program_id_write_demoted(i, &self.account_keys, &self.instructions)
}
#[allow(clippy::arithmetic_side_effects)]
#[inline(always)]
pub fn size(&self) -> usize {
size_of::<MessageHeader>() + size_of::<TransactionConfigMask>() + size_of::<Hash>() + size_of::<u8>() + size_of::<u8>() + self.account_keys.len() * size_of::<Address>() + self.config.size() + self.instructions.len()
* (
size_of::<u8>()
+ size_of::<u8>()
+ size_of::<u16>()
) + self
.instructions
.iter()
.map(|ix| {
(ix.accounts.len() * size_of::<u8>())
+ ix.data.len()
})
.sum::<usize>() }
pub fn validate(&self) -> Result<(), MessageError> {
if self.header.num_required_signatures > MAX_SIGNATURES {
return Err(MessageError::TooManySignatures);
}
if self.instructions.len() > MAX_INSTRUCTIONS as usize {
return Err(MessageError::TooManyInstructions);
}
let num_account_keys = self.account_keys.len();
if num_account_keys > MAX_ADDRESSES as usize {
return Err(MessageError::TooManyAddresses);
}
let min_accounts = usize::from(self.header.num_required_signatures)
.saturating_add(usize::from(self.header.num_readonly_unsigned_accounts));
if num_account_keys < min_accounts {
return Err(MessageError::NotEnoughAddressesForSignatures);
}
if self.header.num_readonly_signed_accounts >= self.header.num_required_signatures {
return Err(MessageError::ZeroSigners);
}
let unique_keys: HashSet<_> = self.account_keys.iter().collect();
if unique_keys.len() != num_account_keys {
return Err(MessageError::DuplicateAddresses);
}
let mask: TransactionConfigMask = self.config.into();
if mask.has_invalid_priority_fee_bits() {
return Err(MessageError::InvalidConfigMask);
}
if let Some(heap_size) = self.config.heap_size {
if heap_size % 1024 != 0 {
return Err(MessageError::InvalidHeapSize);
}
}
let max_account_index = num_account_keys
.checked_sub(1)
.ok_or(MessageError::NotEnoughAccountKeys)?;
for instruction in &self.instructions {
if usize::from(instruction.program_id_index) > max_account_index {
return Err(MessageError::InvalidInstructionAccountIndex);
}
if instruction.program_id_index == 0 {
return Err(MessageError::InvalidInstructionAccountIndex);
}
if instruction.accounts.len() > u8::MAX as usize {
return Err(MessageError::InstructionAccountsTooLarge);
}
if instruction.data.len() > u16::MAX as usize {
return Err(MessageError::InstructionDataTooLarge);
}
for &account_index in &instruction.accounts {
if usize::from(account_index) > max_account_index {
return Err(MessageError::InvalidInstructionAccountIndex);
}
}
}
Ok(())
}
}
impl Sanitize for Message {
fn sanitize(&self) -> Result<(), SanitizeError> {
Ok(self.validate()?)
}
}
#[cfg(feature = "wincode")]
unsafe impl<C: ConfigCore> SchemaWrite<C> for Message {
type Src = Self;
#[inline(always)]
fn size_of(src: &Self::Src) -> WriteResult<usize> {
Ok(src.size())
}
fn write(mut writer: impl Writer, src: &Self::Src) -> WriteResult<()> {
let mut writer = unsafe { writer.as_trusted_for(src.size()) }?;
writer.write(&[
src.header.num_required_signatures,
src.header.num_readonly_signed_accounts,
src.header.num_readonly_unsigned_accounts,
])?;
let mask = TransactionConfigMask::from(&src.config).0.to_le_bytes();
writer.write(&mask)?;
writer.write(src.lifetime_specifier.as_bytes())?;
writer.write(&[src.instructions.len() as u8, src.account_keys.len() as u8])?;
#[expect(clippy::arithmetic_side_effects)]
let account_keys = unsafe {
from_raw_parts(
src.account_keys.as_ptr().cast::<u8>(),
src.account_keys.len() * size_of::<Address>(),
)
};
writer.write(account_keys)?;
if let Some(value) = src.config.priority_fee {
writer.write(&value.to_le_bytes())?;
}
if let Some(value) = src.config.compute_unit_limit {
writer.write(&value.to_le_bytes())?;
}
if let Some(value) = src.config.loaded_accounts_data_size_limit {
writer.write(&value.to_le_bytes())?;
}
if let Some(value) = src.config.heap_size {
writer.write(&value.to_le_bytes())?;
}
for ix in &src.instructions {
writer.write(&[ix.program_id_index, ix.accounts.len() as u8])?;
writer.write(&(ix.data.len() as u16).to_le_bytes())?;
}
for ix in &src.instructions {
writer.write(&ix.accounts)?;
writer.write(&ix.data)?;
}
writer.finish()?;
Ok(())
}
}
#[cfg(feature = "wincode")]
#[inline]
pub fn serialize(message: &Message) -> Vec<u8> {
wincode::serialize(message).unwrap()
}
#[cfg(feature = "wincode")]
unsafe impl<'de, C: Config> SchemaRead<'de, C> for Message {
type Dst = Message;
#[expect(clippy::arithmetic_side_effects)]
fn read(mut reader: impl Reader<'de>, dst: &mut MaybeUninit<Self::Dst>) -> ReadResult<()> {
let (header, lifetime_specifier, config_mask, num_instructions, num_addresses) = {
let mut reader = unsafe { reader.as_trusted_for(FIXED_HEADER_SIZE)? };
let header = <MessageHeader as SchemaRead<C>>::get(reader.by_ref())?;
let config_mask = TransactionConfigMask(u32::from_le_bytes(reader.take_array()?));
let lifetime_specifier = <Hash as SchemaRead<C>>::get(reader.by_ref())?;
let num_instructions = reader.take_byte()? as usize;
let num_addresses = reader.take_byte()? as usize;
(
header,
lifetime_specifier,
config_mask,
num_instructions,
num_addresses,
)
};
<C::LengthEncoding as SeqLen<C>>::prealloc_check::<Address>(num_addresses)?;
let account_keys = <Vec<Address> as SchemaReadContext<C, context::Len>>::get_with_context(
context::Len(num_addresses),
reader.by_ref(),
)?;
let mut config = TransactionConfig::empty();
if config_mask.has_priority_fee() {
config.priority_fee = Some(u64::from_le_bytes(reader.take_array()?));
}
if config_mask.has_compute_unit_limit() {
config.compute_unit_limit = Some(u32::from_le_bytes(reader.take_array()?));
}
if config_mask.has_loaded_accounts_data_size() {
config.loaded_accounts_data_size_limit = Some(u32::from_le_bytes(reader.take_array()?));
}
if config_mask.has_heap_size() {
config.heap_size = Some(u32::from_le_bytes(reader.take_array()?));
}
let instruction_headers = unsafe {
from_raw_parts(
reader
.take_borrowed(num_instructions * size_of::<InstructionHeader>())?
.as_ptr() as *const InstructionHeader,
num_instructions,
)
};
let mut instructions = Vec::with_capacity(num_instructions);
for header in instruction_headers {
let program_id_index = header.0;
let num_accounts = header.1 as usize;
let data_len = u16::from_le_bytes(header.2) as usize;
<C::LengthEncoding as SeqLen<C>>::prealloc_check::<u8>(num_accounts)?;
let accounts = <Vec<u8> as SchemaReadContext<C, context::Len>>::get_with_context(
context::Len(num_accounts),
reader.by_ref(),
)?;
<C::LengthEncoding as SeqLen<C>>::prealloc_check::<u8>(data_len)?;
let data = <Vec<u8> as SchemaReadContext<C, context::Len>>::get_with_context(
context::Len(data_len),
reader.by_ref(),
)?;
instructions.push(CompiledInstruction {
program_id_index,
accounts,
data,
});
}
dst.write(Message {
header,
lifetime_specifier,
config,
account_keys,
instructions,
});
Ok(())
}
}
#[cfg(feature = "wincode")]
#[inline]
pub fn deserialize(input: &[u8]) -> wincode::ReadResult<Message> {
wincode::deserialize(input)
}
#[cfg(test)]
mod tests {
use {super::*, solana_sdk_ids::bpf_loader_upgradeable};
#[derive(Debug, Clone, Default)]
pub struct MessageBuilder {
header: MessageHeader,
config: TransactionConfig,
lifetime_specifier: Option<Hash>,
account_keys: Vec<Address>,
instructions: Vec<CompiledInstruction>,
}
impl MessageBuilder {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn required_signatures(mut self, count: u8) -> Self {
self.header.num_required_signatures = count;
self
}
#[must_use]
pub fn readonly_signed_accounts(mut self, count: u8) -> Self {
self.header.num_readonly_signed_accounts = count;
self
}
#[must_use]
pub fn readonly_unsigned_accounts(mut self, count: u8) -> Self {
self.header.num_readonly_unsigned_accounts = count;
self
}
#[must_use]
pub fn lifetime_specifier(mut self, hash: Hash) -> Self {
self.lifetime_specifier = Some(hash);
self
}
#[must_use]
pub fn config(mut self, config: TransactionConfig) -> Self {
self.config = config;
self
}
#[must_use]
pub fn priority_fee(mut self, fee: u64) -> Self {
self.config.priority_fee = Some(fee);
self
}
#[must_use]
pub fn compute_unit_limit(mut self, limit: u32) -> Self {
self.config.compute_unit_limit = Some(limit);
self
}
#[must_use]
pub fn loaded_accounts_data_size_limit(mut self, limit: u32) -> Self {
self.config.loaded_accounts_data_size_limit = Some(limit);
self
}
#[must_use]
pub fn heap_size(mut self, size: u32) -> Self {
self.config.heap_size = Some(size);
self
}
#[must_use]
pub fn account(mut self, key: Address) -> Self {
self.account_keys.push(key);
self
}
#[must_use]
pub fn accounts(mut self, keys: Vec<Address>) -> Self {
self.account_keys = keys;
self
}
#[must_use]
pub fn instruction(mut self, instruction: CompiledInstruction) -> Self {
self.instructions.push(instruction);
self
}
#[must_use]
pub fn instructions(mut self, instructions: Vec<CompiledInstruction>) -> Self {
self.instructions = instructions;
self
}
pub fn build(self) -> Result<Message, MessageError> {
let lifetime_specifier = self
.lifetime_specifier
.ok_or(MessageError::MissingLifetimeSpecifier)?;
let message = Message::new(
self.header,
self.config,
lifetime_specifier,
self.account_keys,
self.instructions,
);
message.validate()?;
Ok(message)
}
}
fn create_test_message() -> Message {
MessageBuilder::new()
.required_signatures(1)
.readonly_unsigned_accounts(1)
.lifetime_specifier(Hash::new_unique())
.accounts(vec![
Address::new_unique(), Address::new_unique(), Address::new_unique(), ])
.compute_unit_limit(200_000)
.instruction(CompiledInstruction {
program_id_index: 1,
accounts: vec![0, 2],
data: vec![1, 2, 3, 4],
})
.build()
.unwrap()
}
#[test]
fn fee_payer_returns_first_account() {
let fee_payer = Address::new_unique();
let message = MessageBuilder::new()
.required_signatures(1)
.lifetime_specifier(Hash::new_unique())
.accounts(vec![fee_payer, Address::new_unique()])
.build()
.unwrap();
assert_eq!(message.fee_payer(), Some(&fee_payer));
}
#[test]
fn fee_payer_returns_none_for_empty_accounts() {
let message = Message::new(
MessageHeader::default(),
TransactionConfig::default(),
Hash::new_unique(),
vec![],
vec![],
);
assert_eq!(message.fee_payer(), None);
}
#[test]
fn is_signer_checks_signature_requirement() {
let message = create_test_message();
assert!(message.is_signer(0)); assert!(!message.is_signer(1)); assert!(!message.is_signer(2)); }
#[test]
fn is_signer_writable_identifies_writable_signers() {
let message = MessageBuilder::new()
.required_signatures(3)
.readonly_signed_accounts(1) .lifetime_specifier(Hash::new_unique())
.accounts(vec![
Address::new_unique(), Address::new_unique(), Address::new_unique(), Address::new_unique(), ])
.build()
.unwrap();
assert!(message.is_signer_writable(0));
assert!(message.is_signer_writable(1));
assert!(!message.is_signer_writable(2));
assert!(!message.is_signer_writable(3));
assert!(!message.is_signer_writable(100));
}
#[test]
fn is_signer_writable_all_writable_when_no_readonly() {
let message = MessageBuilder::new()
.required_signatures(2)
.readonly_signed_accounts(0) .lifetime_specifier(Hash::new_unique())
.accounts(vec![
Address::new_unique(),
Address::new_unique(),
Address::new_unique(),
])
.build()
.unwrap();
assert!(message.is_signer_writable(0));
assert!(message.is_signer_writable(1));
assert!(!message.is_signer_writable(2)); }
#[test]
fn is_key_called_as_program_detects_program_indices() {
let message = create_test_message();
assert!(message.is_key_called_as_program(1));
assert!(!message.is_key_called_as_program(0));
assert!(!message.is_key_called_as_program(2));
assert!(!message.is_key_called_as_program(256));
assert!(!message.is_key_called_as_program(10_000));
}
#[test]
fn is_upgradeable_loader_present_detects_loader() {
let message = create_test_message();
assert!(!message.is_upgradeable_loader_present());
let mut message_with_loader = create_test_message();
message_with_loader
.account_keys
.push(bpf_loader_upgradeable::id());
assert!(message_with_loader.is_upgradeable_loader_present());
}
#[test]
fn is_writable_index_respects_header_layout() {
let message = create_test_message();
assert!(message.is_writable_index(0)); assert!(message.is_writable_index(1)); assert!(!message.is_writable_index(2)); }
#[test]
fn is_writable_index_handles_mixed_signer_permissions() {
let mut message = create_test_message();
message.header.num_required_signatures = 2;
message.header.num_readonly_signed_accounts = 1;
message.header.num_readonly_unsigned_accounts = 1;
message.account_keys = vec![
Address::new_unique(), Address::new_unique(), Address::new_unique(), ];
message.instructions[0].program_id_index = 2;
message.instructions[0].accounts = vec![0, 1];
assert!(message.sanitize().is_ok());
assert!(message.is_writable_index(0)); assert!(!message.is_writable_index(1)); assert!(!message.is_writable_index(2)); assert!(!message.is_writable_index(999)); }
#[test]
fn is_maybe_writable_returns_false_for_readonly_index() {
let message = create_test_message();
assert!(!message.is_writable_index(2));
assert!(!message.is_maybe_writable(2, None));
assert!(!message.is_maybe_writable(2, Some(&HashSet::new())));
}
#[test]
fn is_maybe_writable_demotes_reserved_accounts() {
let message = create_test_message();
let reserved = HashSet::from([message.account_keys[0]]);
assert!(message.is_writable_index(0));
assert!(!message.is_maybe_writable(0, Some(&reserved)));
}
#[test]
fn is_maybe_writable_demotes_programs_without_upgradeable_loader() {
let message = create_test_message();
assert!(message.is_writable_index(1));
assert!(message.is_key_called_as_program(1));
assert!(!message.is_upgradeable_loader_present());
assert!(!message.is_maybe_writable(1, None));
}
#[test]
fn is_maybe_writable_preserves_programs_with_upgradeable_loader() {
let mut message = create_test_message();
message.account_keys.push(bpf_loader_upgradeable::id());
assert!(message.sanitize().is_ok());
assert!(message.is_writable_index(1));
assert!(message.is_key_called_as_program(1));
assert!(message.is_upgradeable_loader_present());
assert!(message.is_maybe_writable(1, None));
}
#[test]
fn sanitize_accepts_valid_message() {
let message = create_test_message();
assert!(message.sanitize().is_ok());
}
#[test]
fn sanitize_rejects_zero_signers() {
let mut message = create_test_message();
message.header.num_required_signatures = 0;
assert_eq!(message.sanitize(), Err(SanitizeError::InvalidValue));
}
#[test]
fn sanitize_rejects_over_12_signatures() {
let mut message = create_test_message();
message.header.num_required_signatures = MAX_SIGNATURES + 1;
message.account_keys = (0..MAX_SIGNATURES + 1)
.map(|_| Address::new_unique())
.collect();
assert_eq!(message.sanitize(), Err(SanitizeError::IndexOutOfBounds));
}
#[test]
fn sanitize_rejects_over_64_addresses() {
let mut message = create_test_message();
message.account_keys = (0..65).map(|_| Address::new_unique()).collect();
assert_eq!(message.sanitize(), Err(SanitizeError::IndexOutOfBounds));
}
#[test]
fn sanitize_rejects_over_64_instructions() {
let mut message = create_test_message();
message.instructions = (0..65) .map(|_| CompiledInstruction {
program_id_index: 1,
accounts: vec![0],
data: vec![],
})
.collect();
assert_eq!(message.sanitize(), Err(SanitizeError::IndexOutOfBounds));
}
#[test]
fn sanitize_rejects_insufficient_accounts_for_header() {
let mut message = create_test_message();
message.header.num_readonly_unsigned_accounts = 10;
assert_eq!(message.sanitize(), Err(SanitizeError::IndexOutOfBounds));
}
#[test]
fn sanitize_rejects_all_signers_readonly() {
let mut message = create_test_message();
message.header.num_readonly_signed_accounts = 1; assert_eq!(message.sanitize(), Err(SanitizeError::InvalidValue));
}
#[test]
fn sanitize_rejects_duplicate_addresses() {
let mut message = create_test_message();
let dup = message.account_keys[0];
message.account_keys[1] = dup;
assert_eq!(message.sanitize(), Err(SanitizeError::InvalidValue));
}
#[test]
fn sanitize_rejects_unaligned_heap_size() {
let mut message = create_test_message();
message.config.heap_size = Some(1025); assert_eq!(message.sanitize(), Err(SanitizeError::InvalidValue));
}
#[test]
fn sanitize_accepts_aligned_heap_size() {
let mut message = create_test_message();
message.config.heap_size = Some(65536); assert!(message.sanitize().is_ok());
}
#[test]
fn sanitize_rejects_invalid_program_id_index() {
let mut message = create_test_message();
message.instructions[0].program_id_index = 99;
assert_eq!(message.sanitize(), Err(SanitizeError::IndexOutOfBounds));
}
#[test]
fn sanitize_rejects_fee_payer_as_program() {
let mut message = create_test_message();
message.instructions[0].program_id_index = 0;
assert_eq!(message.sanitize(), Err(SanitizeError::IndexOutOfBounds));
}
#[test]
fn sanitize_rejects_instruction_with_too_many_accounts() {
let mut message = create_test_message();
message.instructions[0].accounts = vec![0u8; (u8::MAX as usize) + 1];
assert_eq!(message.sanitize(), Err(SanitizeError::InvalidValue));
}
#[test]
fn sanitize_rejects_invalid_instruction_account_index() {
let mut message = create_test_message();
message.instructions[0].accounts = vec![0, 99]; assert_eq!(message.sanitize(), Err(SanitizeError::IndexOutOfBounds));
}
#[test]
fn sanitize_accepts_64_addresses() {
let mut message = create_test_message();
message.account_keys = (0..MAX_ADDRESSES).map(|_| Address::new_unique()).collect();
message.header.num_required_signatures = 1;
message.header.num_readonly_signed_accounts = 0;
message.header.num_readonly_unsigned_accounts = 1;
message.instructions[0].program_id_index = 1;
message.instructions[0].accounts = vec![0, 2];
assert!(message.sanitize().is_ok());
}
#[test]
fn sanitize_accepts_64_instructions() {
let mut message = create_test_message();
message.instructions = (0..MAX_INSTRUCTIONS)
.map(|_| CompiledInstruction {
program_id_index: 1,
accounts: vec![0, 2],
data: vec![1, 2, 3],
})
.collect();
assert!(message.sanitize().is_ok());
}
#[test]
fn size_matches_serialized_length() {
let test_cases = [
MessageBuilder::new()
.required_signatures(1)
.lifetime_specifier(Hash::new_unique())
.accounts(vec![Address::new_unique()])
.build()
.unwrap(),
MessageBuilder::new()
.required_signatures(1)
.lifetime_specifier(Hash::new_unique())
.accounts(vec![Address::new_unique(), Address::new_unique()])
.priority_fee(1000)
.compute_unit_limit(200_000)
.instruction(CompiledInstruction {
program_id_index: 1,
accounts: vec![0],
data: vec![1, 2, 3, 4],
})
.build()
.unwrap(),
MessageBuilder::new()
.required_signatures(2)
.readonly_signed_accounts(1)
.readonly_unsigned_accounts(1)
.lifetime_specifier(Hash::new_unique())
.accounts(vec![
Address::new_unique(),
Address::new_unique(),
Address::new_unique(),
Address::new_unique(),
])
.heap_size(65536)
.instructions(vec![
CompiledInstruction {
program_id_index: 2,
accounts: vec![0, 1],
data: vec![],
},
CompiledInstruction {
program_id_index: 3,
accounts: vec![0, 1, 2],
data: vec![0xAA; 100],
},
])
.build()
.unwrap(),
];
for message in &test_cases {
assert_eq!(message.size(), serialize(message).len());
}
}
#[test]
fn byte_layout_without_config() {
let fee_payer = Address::new_from_array([1u8; 32]);
let program = Address::new_from_array([2u8; 32]);
let blockhash = Hash::new_from_array([0xAB; 32]);
let message = MessageBuilder::new()
.required_signatures(1)
.lifetime_specifier(blockhash)
.accounts(vec![fee_payer, program])
.instruction(CompiledInstruction {
program_id_index: 1,
accounts: vec![0],
data: vec![0xDE, 0xAD],
})
.build()
.unwrap();
let bytes = serialize(&message);
let mut expected = vec![1, 0, 0];
expected.extend_from_slice(&0u32.to_le_bytes()); expected.extend_from_slice(&[0xAB; 32]); expected.push(1); expected.push(2); expected.extend_from_slice(&[1u8; 32]); expected.extend_from_slice(&[2u8; 32]); expected.push(1); expected.push(1); expected.extend_from_slice(&2u16.to_le_bytes()); expected.push(0); expected.extend_from_slice(&[0xDE, 0xAD]); assert_eq!(bytes, expected);
}
#[test]
fn byte_layout_with_config() {
let fee_payer = Address::new_from_array([1u8; 32]);
let program = Address::new_from_array([2u8; 32]);
let blockhash = Hash::new_from_array([0xBB; 32]);
let message = MessageBuilder::new()
.required_signatures(1)
.lifetime_specifier(blockhash)
.accounts(vec![fee_payer, program])
.priority_fee(0x0102030405060708u64)
.compute_unit_limit(0x11223344u32)
.instruction(CompiledInstruction {
program_id_index: 1,
accounts: vec![],
data: vec![],
})
.build()
.unwrap();
let bytes = serialize(&message);
let mut expected = vec![1, 0, 0];
expected.extend_from_slice(&7u32.to_le_bytes());
expected.extend_from_slice(&[0xBB; 32]);
expected.push(1);
expected.push(2);
expected.extend_from_slice(&[1u8; 32]);
expected.extend_from_slice(&[2u8; 32]);
expected.extend_from_slice(&0x0102030405060708u64.to_le_bytes());
expected.extend_from_slice(&0x11223344u32.to_le_bytes());
expected.push(1); expected.push(0); expected.extend_from_slice(&0u16.to_le_bytes());
assert_eq!(bytes, expected);
}
#[test]
fn roundtrip_preserves_all_config_fields() {
let message = MessageBuilder::new()
.required_signatures(1)
.lifetime_specifier(Hash::new_unique())
.accounts(vec![Address::new_unique(), Address::new_unique()])
.priority_fee(1000)
.compute_unit_limit(200_000)
.loaded_accounts_data_size_limit(1_000_000)
.heap_size(65536)
.instruction(CompiledInstruction {
program_id_index: 1,
accounts: vec![0],
data: vec![],
})
.build()
.unwrap();
let serialized = serialize(&message);
let deserialized = deserialize(&serialized).unwrap();
assert_eq!(message.config, deserialized.config);
}
}