Skip to main content

solana_message/versions/v1/
message.rs

1//! Core Message type for V1 transactions (SIMD-0385).
2//!
3//! A new transaction format that is designed to enable larger transactions
4//! sizes while not having the address lookup table features introduced in
5//! v0 transactions. The v1 transaction format also does not require compute
6//! budget instructions to be present within the transaction.
7//!
8//! # Binary Format
9//!
10//! ```text
11//! ┌────────────────────────────────────────────────────────┐
12//! │ * LegacyHeader (3 x u8)                                │
13//! │                                                        │
14//! │ * TransactionConfigMask (u32, little-endian)           │
15//! │                                                        │
16//! │ * LifetimeSpecifier [u8; 32] (blockhash)               │
17//! │                                                        │
18//! │ * NumInstructions (u8, max 64)                         │
19//! │                                                        │
20//! │ * NumAddresses (u8, max 64)                            │
21//! │                                                        │
22//! │ * Addresses [[u8; 32] x NumAddresses]                  │
23//! │                                                        │
24//! │ * ConfigValues ([[u8; 4] * variable based on mask])    │
25//! │                                                        │
26//! │ * InstructionHeaders [(u8, u8, u16) x NumInstructions] │
27//! │                                                        │
28//! │ * InstructionPayloads (variable based on headers)      │
29//! │    └─ Per NumInstructions:                             │
30//! │         +- [u8] account indices                        │
31//! │         └─ [u8] instruction data                       │
32//! └────────────────────────────────────────────────────────┘
33//! ```
34
35// Re-export for convenient access to the message builder in tests.
36#[cfg(test)]
37pub use self::tests::MessageBuilder;
38#[cfg(feature = "serde")]
39use serde_derive::{Deserialize, Serialize};
40#[cfg(feature = "frozen-abi")]
41use solana_frozen_abi_macro::AbiExample;
42#[cfg(feature = "wincode")]
43use {
44    crate::v1::MAX_TRANSACTION_SIZE,
45    wincode::{
46        config::ConfigCore,
47        error::invalid_tag_encoding,
48        io::{Reader, Writer},
49        ReadResult, SchemaRead, SchemaWrite, WriteResult,
50    },
51};
52use {
53    crate::{
54        compiled_instruction::CompiledInstruction,
55        compiled_keys::CompiledKeys,
56        v1::{
57            InstructionHeader, MessageError, TransactionConfig, TransactionConfigMask,
58            FIXED_HEADER_SIZE, MAX_ADDRESSES, MAX_INSTRUCTIONS, MAX_SIGNATURES,
59        },
60        AccountKeys, CompileError, MessageHeader,
61    },
62    core::{mem::size_of, ptr::copy_nonoverlapping},
63    solana_address::Address,
64    solana_hash::Hash,
65    solana_instruction::Instruction,
66    solana_sanitize::{Sanitize, SanitizeError},
67    std::{collections::HashSet, mem::MaybeUninit},
68};
69
70/// A V1 transaction message (SIMD-0385) supporting 4KB transactions with inline compute budget.
71///
72/// # Important
73///
74/// This message format does not support bincode binary serialization. Use the provided
75/// `serialize` and `deserialize` functions for binary encoding/decoding.
76#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
77#[cfg_attr(
78    feature = "serde",
79    derive(Serialize, Deserialize),
80    serde(rename_all = "camelCase")
81)]
82#[derive(Debug, Clone, PartialEq, Eq, Default)]
83pub struct Message {
84    /// The message header describing signer/readonly account counts.
85    pub header: MessageHeader,
86
87    /// Configuration for transaction parameters.
88    pub config: TransactionConfig,
89
90    /// The lifetime specifier (blockhash) that determines when this transaction expires.
91    pub lifetime_specifier: Hash,
92
93    /// All account addresses referenced by this message.
94    ///
95    /// The length should be specified as an `u8`. Unlike V0, V1 does not support
96    /// address lookup tables. The ordering of the addresses is unchanged from prior
97    /// transaction formats:
98    ///
99    ///   - `num_required_signatures-num_readonly_signed_accounts` additional addresses
100    ///     for which the transaction contains signatures and are loaded as writable, of
101    ///     which the first is the fee payer.
102    ///
103    ///   - `num_readonly_signed_accounts` addresses for which the transaction contains
104    ///     signatures and are loaded as readonly.
105    ///
106    ///   - `num_addresses-num_required_signatures-num_readonly_unsigned_accounts` addresses
107    ///     for which the transaction does not contain signatures and are loaded as writable.
108    ///
109    ///   - `num_readonly_unsigned_accounts` addresses for which the transaction does not
110    ///     contain signatures and are loaded as readonly.
111    pub account_keys: Vec<Address>,
112
113    /// Program instructions to execute.
114    pub instructions: Vec<CompiledInstruction>,
115}
116
117impl Message {
118    /// Create a new V1 message.
119    pub fn new(
120        header: MessageHeader,
121        config: TransactionConfig,
122        lifetime_specifier: Hash,
123        account_keys: Vec<Address>,
124        instructions: Vec<CompiledInstruction>,
125    ) -> Self {
126        Self {
127            header,
128            config,
129            lifetime_specifier,
130            account_keys,
131            instructions,
132        }
133    }
134
135    /// Create a signable transaction message from a `payer` public key,
136    /// `recent_blockhash`, list of `instructions` and a transaction `config`.
137    ///
138    /// # Examples
139    ///
140    /// This example uses the [`solana_rpc_client`], [`solana_account`], and [`anyhow`] crates.
141    ///
142    /// [`solana_rpc_client`]: https://docs.rs/solana-rpc-client
143    /// [`solana_account`]: https://docs.rs/solana-account
144    /// [`anyhow`]: https://docs.rs/anyhow
145    ///
146    /// ```
147    /// # use solana_example_mocks::{
148    /// #     solana_rpc_client,
149    /// #     solana_account,
150    /// #     solana_signer,
151    /// #     solana_keypair,
152    /// # };
153    /// # use std::borrow::Cow;
154    /// # use solana_account::Account;
155    /// use anyhow::Result;
156    /// use solana_instruction::{AccountMeta, Instruction};
157    /// use solana_keypair::Keypair;
158    /// use solana_message::{VersionedMessage, v1};
159    /// use solana_address::Address;
160    /// use solana_rpc_client::rpc_client::RpcClient;
161    /// use solana_signer::Signer;
162    /// # mod solana_transaction {
163    /// #     pub mod versioned {
164    /// #         use solana_example_mocks::{solana_keypair::Keypair, solana_signer::SignerError};
165    /// #         use solana_message::VersionedMessage;
166    /// #         pub struct VersionedTransaction {
167    /// #             pub message: solana_message::VersionedMessage,
168    /// #         }
169    /// #         impl VersionedTransaction {
170    /// #             pub fn try_new(
171    /// #                 message: VersionedMessage,
172    /// #                 _keypairs: &[&Keypair],
173    /// #             ) -> std::result::Result<Self, solana_example_mocks::solana_signer::SignerError> {
174    /// #                 Ok(VersionedTransaction {
175    /// #                     message,
176    /// #                 })
177    /// #             }
178    /// #         }
179    /// #     }
180    /// # }
181    /// use solana_transaction::versioned::VersionedTransaction;
182    ///
183    /// fn create_v1_tx(
184    ///     client: &RpcClient,
185    ///     instruction: Instruction,
186    ///     payer: &Keypair,
187    /// ) -> Result<VersionedTransaction> {
188    ///     let blockhash = client.get_latest_blockhash()?;
189    ///     let tx = VersionedTransaction::try_new(
190    ///         VersionedMessage::V1(v1::Message::try_compile(
191    ///             &payer.pubkey(),
192    ///             &[instruction],
193    ///             blockhash,
194    ///         )?),
195    ///         &[payer],
196    ///     )?;
197    ///
198    ///     Ok(tx)
199    /// }
200    /// #
201    /// # let client = RpcClient::new(String::new());
202    /// # let payer = Keypair::new();
203    /// # let instruction = Instruction::new_with_bincode(Address::new_unique(), &(), vec![
204    /// #   AccountMeta::new(Address::new_unique(), false),
205    /// # ]);
206    /// # create_v1_tx(&client, instruction, &payer)?;
207    /// # Ok::<(), anyhow::Error>(())
208    /// ```
209    pub fn try_compile(
210        payer: &Address,
211        instructions: &[Instruction],
212        recent_blockhash: Hash,
213    ) -> Result<Self, CompileError> {
214        Self::try_compile_with_config(
215            payer,
216            instructions,
217            recent_blockhash,
218            TransactionConfig::empty(),
219        )
220    }
221
222    /// Create a signable transaction message from a `payer` public key,
223    /// `recent_blockhash`, list of `instructions` and a transaction `config`.
224    ///
225    /// # Examples
226    ///
227    /// This example uses the [`solana_rpc_client`], [`solana_account`], and [`anyhow`] crates.
228    ///
229    /// [`solana_rpc_client`]: https://docs.rs/solana-rpc-client
230    /// [`solana_account`]: https://docs.rs/solana-account
231    /// [`anyhow`]: https://docs.rs/anyhow
232    ///
233    /// ```
234    /// # use solana_example_mocks::{
235    /// #     solana_rpc_client,
236    /// #     solana_account,
237    /// #     solana_signer,
238    /// #     solana_keypair,
239    /// # };
240    /// # use std::borrow::Cow;
241    /// # use solana_account::Account;
242    /// use anyhow::Result;
243    /// use solana_instruction::{AccountMeta, Instruction};
244    /// use solana_keypair::Keypair;
245    /// use solana_message::{VersionedMessage, v1, v1::TransactionConfig};
246    /// use solana_address::Address;
247    /// use solana_rpc_client::rpc_client::RpcClient;
248    /// use solana_signer::Signer;
249    /// # mod solana_transaction {
250    /// #     pub mod versioned {
251    /// #         use solana_example_mocks::{solana_keypair::Keypair, solana_signer::SignerError};
252    /// #         use solana_message::VersionedMessage;
253    /// #         pub struct VersionedTransaction {
254    /// #             pub message: solana_message::VersionedMessage,
255    /// #         }
256    /// #         impl VersionedTransaction {
257    /// #             pub fn try_new(
258    /// #                 message: VersionedMessage,
259    /// #                 _keypairs: &[&Keypair],
260    /// #             ) -> std::result::Result<Self, solana_example_mocks::solana_signer::SignerError> {
261    /// #                 Ok(VersionedTransaction {
262    /// #                     message,
263    /// #                 })
264    /// #             }
265    /// #         }
266    /// #     }
267    /// # }
268    /// use solana_transaction::versioned::VersionedTransaction;
269    ///
270    /// fn create_v1_tx(
271    ///     client: &RpcClient,
272    ///     instruction: Instruction,
273    ///     payer: &Keypair,
274    /// ) -> Result<VersionedTransaction> {
275    ///     let blockhash = client.get_latest_blockhash()?;
276    ///     let tx = VersionedTransaction::try_new(
277    ///         VersionedMessage::V1(v1::Message::try_compile_with_config(
278    ///             &payer.pubkey(),
279    ///             &[instruction],
280    ///             blockhash,
281    ///             TransactionConfig::empty().with_compute_unit_limit(100),
282    ///         )?),
283    ///         &[payer],
284    ///     )?;
285    ///
286    ///     Ok(tx)
287    /// }
288    /// #
289    /// # let client = RpcClient::new(String::new());
290    /// # let payer = Keypair::new();
291    /// # let instruction = Instruction::new_with_bincode(Address::new_unique(), &(), vec![
292    /// #   AccountMeta::new(Address::new_unique(), false),
293    /// # ]);
294    /// # create_v1_tx(&client, instruction, &payer)?;
295    /// # Ok::<(), anyhow::Error>(())
296    /// ```
297    pub fn try_compile_with_config(
298        payer: &Address,
299        instructions: &[Instruction],
300        recent_blockhash: Hash,
301        config: TransactionConfig,
302    ) -> Result<Self, CompileError> {
303        let compiled_keys = CompiledKeys::compile(instructions, Some(*payer));
304        let (header, static_keys) = compiled_keys.try_into_message_components()?;
305
306        let account_keys = AccountKeys::new(&static_keys, None);
307        let instructions = account_keys.try_compile_instructions(instructions)?;
308
309        Ok(Self {
310            header,
311            config,
312            lifetime_specifier: recent_blockhash,
313            account_keys: static_keys,
314            instructions,
315        })
316    }
317
318    /// Returns the fee payer address (first account key).
319    pub fn fee_payer(&self) -> Option<&Address> {
320        self.account_keys.first()
321    }
322
323    /// Account keys are ordered with signers first: `[signers..., non-signers...]`.
324    /// An index falls in the signer region if it's less than `num_required_signatures`.
325    pub fn is_signer(&self, index: usize) -> bool {
326        index < usize::from(self.header.num_required_signatures)
327    }
328
329    /// Returns true if the account at this index is both a signer and writable.
330    pub fn is_signer_writable(&self, index: usize) -> bool {
331        if !self.is_signer(index) {
332            return false;
333        }
334        // Within the signer region, the first (num_required_signatures - num_readonly_signed)
335        // accounts are writable signers.
336        let num_writable_signers = usize::from(self.header.num_required_signatures)
337            .saturating_sub(usize::from(self.header.num_readonly_signed_accounts));
338        index < num_writable_signers
339    }
340
341    /// Returns true if any instruction invokes the account at this index as a program.
342    pub fn is_key_called_as_program(&self, key_index: usize) -> bool {
343        crate::is_key_called_as_program(&self.instructions, key_index)
344    }
345
346    /// Returns `true` if the account at the specified index was requested to be
347    /// writable.
348    ///
349    /// This method should not be used directly.
350    #[inline(always)]
351    pub(crate) fn is_writable_index(&self, i: usize) -> bool {
352        crate::is_writable_index(i, self.header, &self.account_keys)
353    }
354
355    /// Returns true if the BPF upgradeable loader is present in the account keys.
356    pub fn is_upgradeable_loader_present(&self) -> bool {
357        crate::is_upgradeable_loader_present(&self.account_keys)
358    }
359
360    /// Returns `true` if the account at the specified index was requested as
361    /// writable.
362    ///
363    ///
364    /// # Important
365    ///
366    /// Before loading addresses, we can't demote write locks properly so this should
367    /// not be used by the runtime. The `reserved_account_keys` parameter is optional
368    /// to allow clients to approximate writability without requiring fetching the latest
369    /// set of reserved account keys.
370    ///
371    /// Program accounts are demoted from writable to readonly, unless the upgradeable
372    /// loader is present in which case they are left as writable since upgradeable
373    /// programs need to be writable for upgrades.
374    pub fn is_maybe_writable(
375        &self,
376        key_index: usize,
377        reserved_account_keys: Option<&HashSet<Address>>,
378    ) -> bool {
379        crate::is_maybe_writable(
380            key_index,
381            self.header,
382            &self.account_keys,
383            &self.instructions,
384            reserved_account_keys,
385        )
386    }
387
388    pub fn demote_program_id(&self, i: usize) -> bool {
389        crate::is_program_id_write_demoted(i, &self.account_keys, &self.instructions)
390    }
391
392    /// Calculate the serialized size of the message in bytes.
393    #[allow(clippy::arithmetic_side_effects)]
394    #[inline(always)]
395    pub fn size(&self) -> usize {
396        size_of::<MessageHeader>()                           // legacy header
397            + size_of::<TransactionConfigMask>()             // config mask
398            + size_of::<Hash>()                              // lifetime specifier
399            + size_of::<u8>()                                // number of instructions
400            + size_of::<u8>()                                // number of addresses
401            + self.account_keys.len() * size_of::<Address>() // addresses
402            + self.config.size()                             // config values
403            + self.instructions.len()
404                * (
405                    size_of::<u8>()
406                    + size_of::<u8>()
407                    + size_of::<u16>()
408                )                                            // instruction headers
409            + self
410                .instructions
411                .iter()
412                .map(|ix| {
413                    (ix.accounts.len() * size_of::<u8>())
414                    + ix.data.len()
415                })
416                .sum::<usize>() // instruction payloads
417    }
418
419    pub fn validate(&self) -> Result<(), MessageError> {
420        // `num_required_signatures` <= 12
421        if self.header.num_required_signatures > MAX_SIGNATURES {
422            return Err(MessageError::TooManySignatures);
423        }
424
425        // `num_instructions` <= 64
426        if self.instructions.len() > MAX_INSTRUCTIONS as usize {
427            return Err(MessageError::TooManyInstructions);
428        }
429
430        let num_account_keys = self.account_keys.len();
431
432        // `num_addresses` <= 64
433        if num_account_keys > MAX_ADDRESSES as usize {
434            return Err(MessageError::TooManyAddresses);
435        }
436
437        // `num_addresses` >= `num_required_signatures` + `num_readonly_unsigned_accounts`
438        let min_accounts = usize::from(self.header.num_required_signatures)
439            .saturating_add(usize::from(self.header.num_readonly_unsigned_accounts));
440
441        if num_account_keys < min_accounts {
442            return Err(MessageError::NotEnoughAddressesForSignatures);
443        }
444
445        // must have at least 1 RW fee-payer (`num_readonly_signed` < `num_required_signatures`)
446        if self.header.num_readonly_signed_accounts >= self.header.num_required_signatures {
447            return Err(MessageError::ZeroSigners);
448        }
449
450        // no duplicate addresses
451        let unique_keys: HashSet<_> = self.account_keys.iter().collect();
452        if unique_keys.len() != num_account_keys {
453            return Err(MessageError::DuplicateAddresses);
454        }
455
456        // validate config mask (2-bit fields must have both bits set or neither)
457        let mask: TransactionConfigMask = self.config.into();
458
459        if mask.has_invalid_priority_fee_bits() {
460            return Err(MessageError::InvalidConfigMask);
461        }
462
463        // heap size must be a multiple of 1024
464        if let Some(heap_size) = self.config.heap_size {
465            if heap_size % 1024 != 0 {
466                return Err(MessageError::InvalidHeapSize);
467            }
468        }
469
470        // instruction account indices must be < `num_addresses`
471        let max_account_index = num_account_keys
472            .checked_sub(1)
473            .ok_or(MessageError::NotEnoughAccountKeys)?;
474
475        for instruction in &self.instructions {
476            // program id must be in static accounts
477            if usize::from(instruction.program_id_index) > max_account_index {
478                return Err(MessageError::InvalidInstructionAccountIndex);
479            }
480
481            // program cannot be fee payer
482            if instruction.program_id_index == 0 {
483                return Err(MessageError::InvalidInstructionAccountIndex);
484            }
485
486            // instruction accounts count must fit in u8
487            if instruction.accounts.len() > u8::MAX as usize {
488                return Err(MessageError::InstructionAccountsTooLarge);
489            }
490
491            // instruction data length must fit in u16
492            if instruction.data.len() > u16::MAX as usize {
493                return Err(MessageError::InstructionDataTooLarge);
494            }
495
496            // all account indices must be valid
497            for &account_index in &instruction.accounts {
498                if usize::from(account_index) > max_account_index {
499                    return Err(MessageError::InvalidInstructionAccountIndex);
500                }
501            }
502        }
503
504        Ok(())
505    }
506}
507
508impl Sanitize for Message {
509    fn sanitize(&self) -> Result<(), SanitizeError> {
510        Ok(self.validate()?)
511    }
512}
513
514#[cfg(feature = "wincode")]
515unsafe impl<C: ConfigCore> SchemaWrite<C> for Message {
516    type Src = Self;
517
518    #[allow(clippy::arithmetic_side_effects)]
519    #[inline(always)]
520    fn size_of(src: &Self::Src) -> WriteResult<usize> {
521        Ok(src.size())
522    }
523
524    // V0 and V1 add +1 for message version prefix
525    #[inline(always)]
526    fn write(mut writer: impl Writer, src: &Self::Src) -> WriteResult<()> {
527        // SAFETY: Serializing a slice of `[u8]`.
528        unsafe {
529            writer
530                .write_slice_t(&serialize(src))
531                .map_err(wincode::WriteError::Io)
532        }
533    }
534}
535
536#[cfg(feature = "wincode")]
537unsafe impl<'de, C: ConfigCore> SchemaRead<'de, C> for Message {
538    type Dst = Self;
539
540    fn read(mut reader: impl Reader<'de>, dst: &mut MaybeUninit<Self::Dst>) -> ReadResult<()> {
541        let bytes = reader.fill_buf(MAX_TRANSACTION_SIZE)?;
542        let (message, consumed) = deserialize(bytes).map_err(|_| invalid_tag_encoding(1))?;
543
544        // SAFETY: `deserialize` validates that we read `consumed` bytes.
545        unsafe { reader.consume_unchecked(consumed) };
546
547        dst.write(message);
548
549        Ok(())
550    }
551}
552
553/// Serialize the message.
554pub fn serialize(message: &Message) -> Vec<u8> {
555    let total = message.size();
556    let mut buffer = Vec::<u8>::with_capacity(total);
557
558    // SAFETY: `buffer` has sufficient capacity for serialization.
559    unsafe {
560        serialize_into(message, buffer.as_mut_ptr());
561        buffer.set_len(total);
562    }
563
564    buffer
565}
566
567/// Serialize the message into the provided buffer.
568///
569/// # Safety
570///
571/// The caller must ensure that the provided `dst` pointer has at least
572/// `message.size()` bytes of capacity.
573pub(crate) unsafe fn serialize_into(message: &Message, mut dst: *mut u8) {
574    // header
575    dst.write(message.header.num_required_signatures);
576    dst = dst.add(1);
577    dst.write(message.header.num_readonly_signed_accounts);
578    dst = dst.add(1);
579    dst.write(message.header.num_readonly_unsigned_accounts);
580    dst = dst.add(1);
581
582    // config mask
583    let mask = TransactionConfigMask::from(&message.config).0.to_le_bytes();
584    copy_nonoverlapping(mask.as_ptr(), dst, mask.len());
585    dst = dst.add(mask.len());
586
587    // lifetime specifier
588    let lifetime = message.lifetime_specifier.as_ref();
589    copy_nonoverlapping(lifetime.as_ptr(), dst, lifetime.len());
590    dst = dst.add(lifetime.len());
591
592    // counts
593    dst.write(message.instructions.len() as u8);
594    dst = dst.add(1);
595    dst.write(message.account_keys.len() as u8);
596    dst = dst.add(1);
597
598    // addresses
599    for addr in &message.account_keys {
600        let a = addr.as_ref();
601        copy_nonoverlapping(a.as_ptr(), dst, a.len());
602        dst = dst.add(a.len());
603    }
604
605    // config values
606    if let Some(value) = message.config.priority_fee {
607        let bytes = value.to_le_bytes();
608        copy_nonoverlapping(bytes.as_ptr(), dst, bytes.len());
609        dst = dst.add(bytes.len());
610    }
611    if let Some(value) = message.config.compute_unit_limit {
612        let bytes = value.to_le_bytes();
613        copy_nonoverlapping(bytes.as_ptr(), dst, bytes.len());
614        dst = dst.add(bytes.len());
615    }
616    if let Some(value) = message.config.loaded_accounts_data_size_limit {
617        let bytes = value.to_le_bytes();
618        copy_nonoverlapping(bytes.as_ptr(), dst, bytes.len());
619        dst = dst.add(bytes.len());
620    }
621    if let Some(value) = message.config.heap_size {
622        let bytes = value.to_le_bytes();
623        copy_nonoverlapping(bytes.as_ptr(), dst, bytes.len());
624        dst = dst.add(bytes.len());
625    }
626
627    // instruction headers
628    for ix in &message.instructions {
629        dst.write(ix.program_id_index);
630        dst = dst.add(1);
631        dst.write(ix.accounts.len() as u8);
632        dst = dst.add(1);
633
634        let len = (ix.data.len() as u16).to_le_bytes();
635        copy_nonoverlapping(len.as_ptr(), dst, 2);
636        dst = dst.add(2);
637    }
638
639    // instruction payloads
640    for ix in &message.instructions {
641        copy_nonoverlapping(ix.accounts.as_ptr(), dst, ix.accounts.len());
642        dst = dst.add(ix.accounts.len());
643
644        copy_nonoverlapping(ix.data.as_ptr(), dst, ix.data.len());
645        dst = dst.add(ix.data.len());
646    }
647}
648
649/// Deserialize the message from the provided input buffer, returning the message and
650/// the number of bytes read.
651#[allow(clippy::arithmetic_side_effects)]
652pub fn deserialize(input: &[u8]) -> Result<(Message, usize), MessageError> {
653    if input.len() < FIXED_HEADER_SIZE {
654        return Err(MessageError::BufferTooSmall);
655    }
656
657    let mut input_ptr = input.as_ptr();
658
659    // SAFETY: input length has been checked against `FIXED_HEADER_SIZE`.
660    let header = unsafe {
661        let mut header = MaybeUninit::<MessageHeader>::uninit();
662        let dst = header.as_mut_ptr() as *mut u8;
663
664        // num_required_signatures
665        dst.write(input_ptr.read());
666        // num_readonly_signed_accounts
667        dst.add(1).write(input_ptr.add(1).read());
668        // num_readonly_unsigned_accounts
669        dst.add(2).write(input_ptr.add(2).read());
670
671        // Advance input pointer past header.
672        input_ptr = input_ptr.add(3);
673
674        header.assume_init()
675    };
676
677    // config mask
678    //
679    // SAFETY: input length has been checked against `FIXED_HEADER_SIZE`.
680    let config_mask = unsafe {
681        let mask = TransactionConfigMask(u32::from_le_bytes(*(input_ptr as *const [u8; 4])));
682        input_ptr = input_ptr.add(4);
683
684        mask
685    };
686
687    // lifetime specifier
688    //
689    // SAFETY: input length has been checked against `FIXED_HEADER_SIZE`.
690    let lifetime_specifier = unsafe {
691        let specifier = Hash::new_from_array(*(input_ptr as *const [u8; 32]));
692        input_ptr = input_ptr.add(32);
693
694        specifier
695    };
696
697    // counts
698    //
699    // SAFETY: input length has been checked against `FIXED_HEADER_SIZE`.
700    let num_instructions = unsafe {
701        let num_instructions = input_ptr.read() as usize;
702        input_ptr = input_ptr.add(1);
703
704        num_instructions
705    };
706    // SAFETY: input length has been checked against `FIXED_HEADER_SIZE`.
707    let num_addresses = unsafe {
708        let num_addresses = input_ptr.read() as usize;
709        input_ptr = input_ptr.add(1);
710
711        num_addresses
712    };
713
714    // Track the offset for input. This is the value returned to indicate
715    // how many bytes were read.
716    let mut offset = FIXED_HEADER_SIZE + num_addresses * size_of::<Address>();
717
718    // addresses
719
720    if input.len() < offset {
721        return Err(MessageError::BufferTooSmall);
722    }
723
724    let mut account_keys = Vec::with_capacity(num_addresses);
725    // SAFETY: input length has been checked against the required size
726    // for the addresses.
727    unsafe {
728        let dst = account_keys.as_mut_ptr();
729        copy_nonoverlapping(input_ptr as *const Address, dst, num_addresses);
730        account_keys.set_len(num_addresses);
731        input_ptr = input_ptr.add(num_addresses * size_of::<Address>());
732    }
733
734    // config values
735    offset += config_mask.size_of_config();
736
737    if input.len() < offset {
738        return Err(MessageError::BufferTooSmall);
739    }
740
741    let mut config = TransactionConfig::empty();
742
743    if config_mask.has_priority_fee() {
744        // SAFETY: input length has been checked against the required size
745        // for the config.
746        let value = unsafe { u64::from_le_bytes(*(input_ptr as *const [u8; 8])) };
747        config = config.with_priority_fee(value);
748        input_ptr = unsafe { input_ptr.add(size_of::<u64>()) };
749    }
750
751    if config_mask.has_compute_unit_limit() {
752        // SAFETY: input length has been checked against the required size
753        // for the config.
754        let value = unsafe { u32::from_le_bytes(*(input_ptr as *const [u8; 4])) };
755        config = config.with_compute_unit_limit(value);
756        input_ptr = unsafe { input_ptr.add(size_of::<u32>()) };
757    }
758
759    if config_mask.has_loaded_accounts_data_size() {
760        // SAFETY: input length has been checked against the required size
761        // for the config.
762        let value = unsafe { u32::from_le_bytes(*(input_ptr as *const [u8; 4])) };
763        config = config.with_loaded_accounts_data_size_limit(value);
764        input_ptr = unsafe { input_ptr.add(size_of::<u32>()) };
765    }
766
767    if config_mask.has_heap_size() {
768        // SAFETY: input length has been checked against the required size
769        // for the config.
770        let value = unsafe { u32::from_le_bytes(*(input_ptr as *const [u8; 4])) };
771        config = config.with_heap_size(value);
772        input_ptr = unsafe { input_ptr.add(size_of::<u32>()) };
773    }
774
775    // instruction headers
776
777    offset += num_instructions * size_of::<InstructionHeader>();
778
779    if input.len() < offset {
780        return Err(MessageError::BufferTooSmall);
781    }
782
783    // SAFETY: input length has been checked against the required size
784    // for the instruction headers.
785    let instruction_headers: &[InstructionHeader] = unsafe {
786        core::slice::from_raw_parts(input_ptr as *const InstructionHeader, num_instructions)
787    };
788
789    input_ptr = unsafe { input_ptr.add(num_instructions * size_of::<InstructionHeader>()) };
790
791    // instruction payloads
792
793    let mut instructions = Vec::with_capacity(num_instructions);
794
795    for header in instruction_headers {
796        let program_id_index = header.0;
797        let num_accounts = header.1 as usize;
798        let data_len = u16::from_le_bytes(header.2) as usize;
799
800        offset += num_accounts + data_len;
801
802        if input.len() < offset {
803            return Err(MessageError::BufferTooSmall);
804        }
805
806        // SAFETY: input length has been checked against the required size
807        // for the instruction payload.
808        let accounts = unsafe { core::slice::from_raw_parts(input_ptr, num_accounts).to_vec() };
809
810        let data = unsafe {
811            input_ptr = input_ptr.add(num_accounts);
812            core::slice::from_raw_parts(input_ptr, data_len).to_vec()
813        };
814
815        input_ptr = unsafe { input_ptr.add(data_len) };
816
817        instructions.push(CompiledInstruction {
818            program_id_index,
819            accounts,
820            data,
821        });
822    }
823
824    Ok((
825        Message {
826            header,
827            config,
828            lifetime_specifier,
829            account_keys,
830            instructions,
831        },
832        offset,
833    ))
834}
835
836#[cfg(test)]
837mod tests {
838    use {super::*, solana_sdk_ids::bpf_loader_upgradeable};
839
840    /// Builder for constructing V1 messages.
841    ///
842    /// This is used in tests to simplify message construction and validation. For
843    /// client code, users should construct messages using `try_compile` or
844    /// `try_compile_with_config`.
845    #[derive(Debug, Clone, Default)]
846    pub struct MessageBuilder {
847        header: MessageHeader,
848        config: TransactionConfig,
849        lifetime_specifier: Option<Hash>,
850        account_keys: Vec<Address>,
851        instructions: Vec<CompiledInstruction>,
852    }
853
854    impl MessageBuilder {
855        pub fn new() -> Self {
856            Self::default()
857        }
858
859        #[must_use]
860        pub fn required_signatures(mut self, count: u8) -> Self {
861            self.header.num_required_signatures = count;
862            self
863        }
864
865        #[must_use]
866        pub fn readonly_signed_accounts(mut self, count: u8) -> Self {
867            self.header.num_readonly_signed_accounts = count;
868            self
869        }
870
871        #[must_use]
872        pub fn readonly_unsigned_accounts(mut self, count: u8) -> Self {
873            self.header.num_readonly_unsigned_accounts = count;
874            self
875        }
876
877        #[must_use]
878        pub fn lifetime_specifier(mut self, hash: Hash) -> Self {
879            self.lifetime_specifier = Some(hash);
880            self
881        }
882
883        #[must_use]
884        pub fn config(mut self, config: TransactionConfig) -> Self {
885            self.config = config;
886            self
887        }
888
889        #[must_use]
890        pub fn priority_fee(mut self, fee: u64) -> Self {
891            self.config.priority_fee = Some(fee);
892            self
893        }
894
895        #[must_use]
896        pub fn compute_unit_limit(mut self, limit: u32) -> Self {
897            self.config.compute_unit_limit = Some(limit);
898            self
899        }
900
901        #[must_use]
902        pub fn loaded_accounts_data_size_limit(mut self, limit: u32) -> Self {
903            self.config.loaded_accounts_data_size_limit = Some(limit);
904            self
905        }
906
907        #[must_use]
908        pub fn heap_size(mut self, size: u32) -> Self {
909            self.config.heap_size = Some(size);
910            self
911        }
912
913        #[must_use]
914        pub fn account(mut self, key: Address) -> Self {
915            self.account_keys.push(key);
916            self
917        }
918
919        #[must_use]
920        pub fn accounts(mut self, keys: Vec<Address>) -> Self {
921            self.account_keys = keys;
922            self
923        }
924
925        #[must_use]
926        pub fn instruction(mut self, instruction: CompiledInstruction) -> Self {
927            self.instructions.push(instruction);
928            self
929        }
930
931        #[must_use]
932        pub fn instructions(mut self, instructions: Vec<CompiledInstruction>) -> Self {
933            self.instructions = instructions;
934            self
935        }
936
937        /// Build the message, validating all constraints.
938        pub fn build(self) -> Result<Message, MessageError> {
939            let lifetime_specifier = self
940                .lifetime_specifier
941                .ok_or(MessageError::MissingLifetimeSpecifier)?;
942
943            let message = Message::new(
944                self.header,
945                self.config,
946                lifetime_specifier,
947                self.account_keys,
948                self.instructions,
949            );
950
951            message.validate()?;
952
953            Ok(message)
954        }
955    }
956
957    fn create_test_message() -> Message {
958        MessageBuilder::new()
959            .required_signatures(1)
960            .readonly_unsigned_accounts(1)
961            .lifetime_specifier(Hash::new_unique())
962            .accounts(vec![
963                Address::new_unique(), // fee payer
964                Address::new_unique(), // program
965                Address::new_unique(), // readonly account
966            ])
967            .compute_unit_limit(200_000)
968            .instruction(CompiledInstruction {
969                program_id_index: 1,
970                accounts: vec![0, 2],
971                data: vec![1, 2, 3, 4],
972            })
973            .build()
974            .unwrap()
975    }
976
977    #[test]
978    fn fee_payer_returns_first_account() {
979        let fee_payer = Address::new_unique();
980        let message = MessageBuilder::new()
981            .required_signatures(1)
982            .lifetime_specifier(Hash::new_unique())
983            .accounts(vec![fee_payer, Address::new_unique()])
984            .build()
985            .unwrap();
986
987        assert_eq!(message.fee_payer(), Some(&fee_payer));
988    }
989
990    #[test]
991    fn fee_payer_returns_none_for_empty_accounts() {
992        // Direct construction to bypass builder validation
993        let message = Message::new(
994            MessageHeader::default(),
995            TransactionConfig::default(),
996            Hash::new_unique(),
997            vec![],
998            vec![],
999        );
1000
1001        assert_eq!(message.fee_payer(), None);
1002    }
1003
1004    #[test]
1005    fn is_signer_checks_signature_requirement() {
1006        let message = create_test_message();
1007        assert!(message.is_signer(0)); // Fee payer is signer
1008        assert!(!message.is_signer(1)); // Program is not signer
1009        assert!(!message.is_signer(2)); // Readonly account is not signer
1010    }
1011
1012    #[test]
1013    fn is_signer_writable_identifies_writable_signers() {
1014        let message = MessageBuilder::new()
1015            .required_signatures(3)
1016            .readonly_signed_accounts(1) // Last signer is readonly
1017            .lifetime_specifier(Hash::new_unique())
1018            .accounts(vec![
1019                Address::new_unique(), // 0: writable signer
1020                Address::new_unique(), // 1: writable signer
1021                Address::new_unique(), // 2: readonly signer
1022                Address::new_unique(), // 3: non-signer
1023            ])
1024            .build()
1025            .unwrap();
1026
1027        // Writable signers
1028        assert!(message.is_signer_writable(0));
1029        assert!(message.is_signer_writable(1));
1030        // Readonly signer
1031        assert!(!message.is_signer_writable(2));
1032        // Non-signers
1033        assert!(!message.is_signer_writable(3));
1034        assert!(!message.is_signer_writable(100));
1035    }
1036
1037    #[test]
1038    fn is_signer_writable_all_writable_when_no_readonly() {
1039        let message = MessageBuilder::new()
1040            .required_signatures(2)
1041            .readonly_signed_accounts(0) // All signers are writable
1042            .lifetime_specifier(Hash::new_unique())
1043            .accounts(vec![
1044                Address::new_unique(),
1045                Address::new_unique(),
1046                Address::new_unique(),
1047            ])
1048            .build()
1049            .unwrap();
1050
1051        assert!(message.is_signer_writable(0));
1052        assert!(message.is_signer_writable(1));
1053        assert!(!message.is_signer_writable(2)); // Not a signer
1054    }
1055
1056    #[test]
1057    fn is_key_called_as_program_detects_program_indices() {
1058        let message = create_test_message();
1059        // program_id_index = 1 in create_test_message
1060        assert!(message.is_key_called_as_program(1));
1061        assert!(!message.is_key_called_as_program(0));
1062        assert!(!message.is_key_called_as_program(2));
1063        // Index > u8::MAX can't match any program_id_index
1064        assert!(!message.is_key_called_as_program(256));
1065        assert!(!message.is_key_called_as_program(10_000));
1066    }
1067
1068    #[test]
1069    fn is_upgradeable_loader_present_detects_loader() {
1070        let message = create_test_message();
1071        assert!(!message.is_upgradeable_loader_present());
1072
1073        let mut message_with_loader = create_test_message();
1074        message_with_loader
1075            .account_keys
1076            .push(bpf_loader_upgradeable::id());
1077        assert!(message_with_loader.is_upgradeable_loader_present());
1078    }
1079
1080    #[test]
1081    fn is_writable_index_respects_header_layout() {
1082        let message = create_test_message();
1083        // Account layout: [writable signer (fee payer), writable unsigned (program), readonly unsigned]
1084        assert!(message.is_writable_index(0)); // Fee payer is writable
1085        assert!(message.is_writable_index(1)); // Program position is writable unsigned
1086        assert!(!message.is_writable_index(2)); // Last account is readonly
1087    }
1088
1089    #[test]
1090    fn is_writable_index_handles_mixed_signer_permissions() {
1091        let mut message = create_test_message();
1092        // 2 signers: first writable, second readonly
1093        message.header.num_required_signatures = 2;
1094        message.header.num_readonly_signed_accounts = 1;
1095        message.header.num_readonly_unsigned_accounts = 1;
1096        message.account_keys = vec![
1097            Address::new_unique(), // writable signer
1098            Address::new_unique(), // readonly signer
1099            Address::new_unique(), // readonly unsigned
1100        ];
1101        message.instructions[0].program_id_index = 2;
1102        message.instructions[0].accounts = vec![0, 1];
1103
1104        assert!(message.sanitize().is_ok());
1105        assert!(message.is_writable_index(0)); // writable signer
1106        assert!(!message.is_writable_index(1)); // readonly signer
1107        assert!(!message.is_writable_index(2)); // readonly unsigned
1108        assert!(!message.is_writable_index(999)); // out of bounds
1109    }
1110
1111    #[test]
1112    fn is_maybe_writable_returns_false_for_readonly_index() {
1113        let message = create_test_message();
1114        // Index 2 is readonly unsigned
1115        assert!(!message.is_writable_index(2));
1116        assert!(!message.is_maybe_writable(2, None));
1117        // Even with empty reserved set
1118        assert!(!message.is_maybe_writable(2, Some(&HashSet::new())));
1119    }
1120
1121    #[test]
1122    fn is_maybe_writable_demotes_reserved_accounts() {
1123        let message = create_test_message();
1124        let reserved = HashSet::from([message.account_keys[0]]);
1125        // Fee payer is writable by index, but reserved → demoted
1126        assert!(message.is_writable_index(0));
1127        assert!(!message.is_maybe_writable(0, Some(&reserved)));
1128    }
1129
1130    #[test]
1131    fn is_maybe_writable_demotes_programs_without_upgradeable_loader() {
1132        let message = create_test_message();
1133        // Index 1 is writable unsigned, called as program, no upgradeable loader
1134        assert!(message.is_writable_index(1));
1135        assert!(message.is_key_called_as_program(1));
1136        assert!(!message.is_upgradeable_loader_present());
1137        assert!(!message.is_maybe_writable(1, None));
1138    }
1139
1140    #[test]
1141    fn is_maybe_writable_preserves_programs_with_upgradeable_loader() {
1142        let mut message = create_test_message();
1143        // Add upgradeable loader to account keys
1144        message.account_keys.push(bpf_loader_upgradeable::id());
1145
1146        assert!(message.sanitize().is_ok());
1147        assert!(message.is_writable_index(1));
1148        assert!(message.is_key_called_as_program(1));
1149        assert!(message.is_upgradeable_loader_present());
1150        // Program not demoted because upgradeable loader is present
1151        assert!(message.is_maybe_writable(1, None));
1152    }
1153
1154    #[test]
1155    fn sanitize_accepts_valid_message() {
1156        let message = create_test_message();
1157        assert!(message.sanitize().is_ok());
1158    }
1159
1160    #[test]
1161    fn sanitize_rejects_zero_signers() {
1162        let mut message = create_test_message();
1163        message.header.num_required_signatures = 0;
1164        assert_eq!(message.sanitize(), Err(SanitizeError::InvalidValue));
1165    }
1166
1167    #[test]
1168    fn sanitize_rejects_over_12_signatures() {
1169        let mut message = create_test_message();
1170        message.header.num_required_signatures = MAX_SIGNATURES + 1;
1171        message.account_keys = (0..MAX_SIGNATURES + 1)
1172            .map(|_| Address::new_unique())
1173            .collect();
1174        assert_eq!(message.sanitize(), Err(SanitizeError::IndexOutOfBounds));
1175    }
1176
1177    #[test]
1178    fn sanitize_rejects_over_64_addresses() {
1179        let mut message = create_test_message();
1180        message.account_keys = (0..65).map(|_| Address::new_unique()).collect();
1181        assert_eq!(message.sanitize(), Err(SanitizeError::IndexOutOfBounds));
1182    }
1183
1184    #[test]
1185    fn sanitize_rejects_over_64_instructions() {
1186        let mut message = create_test_message();
1187        message.instructions = (0..65) // exceeds 64 max
1188            .map(|_| CompiledInstruction {
1189                program_id_index: 1,
1190                accounts: vec![0],
1191                data: vec![],
1192            })
1193            .collect();
1194        assert_eq!(message.sanitize(), Err(SanitizeError::IndexOutOfBounds));
1195    }
1196
1197    #[test]
1198    fn sanitize_rejects_insufficient_accounts_for_header() {
1199        let mut message = create_test_message();
1200        // min_accounts = num_required_signatures + num_readonly_unsigned_accounts
1201        // Set readonly_unsigned high so min_accounts > account_keys.len()
1202        message.header.num_readonly_unsigned_accounts = 10;
1203        assert_eq!(message.sanitize(), Err(SanitizeError::IndexOutOfBounds));
1204    }
1205
1206    #[test]
1207    fn sanitize_rejects_all_signers_readonly() {
1208        let mut message = create_test_message();
1209        message.header.num_readonly_signed_accounts = 1; // All signers readonly
1210        assert_eq!(message.sanitize(), Err(SanitizeError::InvalidValue));
1211    }
1212
1213    #[test]
1214    fn sanitize_rejects_duplicate_addresses() {
1215        let mut message = create_test_message();
1216        let dup = message.account_keys[0];
1217        message.account_keys[1] = dup;
1218        assert_eq!(message.sanitize(), Err(SanitizeError::InvalidValue));
1219    }
1220
1221    #[test]
1222    fn sanitize_rejects_unaligned_heap_size() {
1223        let mut message = create_test_message();
1224        message.config.heap_size = Some(1025); // Not a multiple of 1024
1225        assert_eq!(message.sanitize(), Err(SanitizeError::InvalidValue));
1226    }
1227
1228    #[test]
1229    fn sanitize_accepts_aligned_heap_size() {
1230        let mut message = create_test_message();
1231        message.config.heap_size = Some(65536); // 64KB, valid
1232        assert!(message.sanitize().is_ok());
1233    }
1234
1235    #[test]
1236    fn sanitize_rejects_invalid_program_id_index() {
1237        let mut message = create_test_message();
1238        message.instructions[0].program_id_index = 99;
1239        assert_eq!(message.sanitize(), Err(SanitizeError::IndexOutOfBounds));
1240    }
1241
1242    #[test]
1243    fn sanitize_rejects_fee_payer_as_program() {
1244        let mut message = create_test_message();
1245        message.instructions[0].program_id_index = 0;
1246        assert_eq!(message.sanitize(), Err(SanitizeError::IndexOutOfBounds));
1247    }
1248
1249    #[test]
1250    fn sanitize_rejects_instruction_with_too_many_accounts() {
1251        let mut message = create_test_message();
1252        message.instructions[0].accounts = vec![0u8; (u8::MAX as usize) + 1];
1253        assert_eq!(message.sanitize(), Err(SanitizeError::InvalidValue));
1254    }
1255
1256    #[test]
1257    fn sanitize_rejects_invalid_instruction_account_index() {
1258        let mut message = create_test_message();
1259        message.instructions[0].accounts = vec![0, 99]; // 99 is out of bounds
1260        assert_eq!(message.sanitize(), Err(SanitizeError::IndexOutOfBounds));
1261    }
1262
1263    #[test]
1264    fn sanitize_accepts_64_addresses() {
1265        let mut message = create_test_message();
1266        message.account_keys = (0..MAX_ADDRESSES).map(|_| Address::new_unique()).collect();
1267        message.header.num_required_signatures = 1;
1268        message.header.num_readonly_signed_accounts = 0;
1269        message.header.num_readonly_unsigned_accounts = 1;
1270        message.instructions[0].program_id_index = 1;
1271        message.instructions[0].accounts = vec![0, 2];
1272        assert!(message.sanitize().is_ok());
1273    }
1274
1275    #[test]
1276    fn sanitize_accepts_64_instructions() {
1277        let mut message = create_test_message();
1278        message.instructions = (0..MAX_INSTRUCTIONS)
1279            .map(|_| CompiledInstruction {
1280                program_id_index: 1,
1281                accounts: vec![0, 2],
1282                data: vec![1, 2, 3],
1283            })
1284            .collect();
1285        assert!(message.sanitize().is_ok());
1286    }
1287
1288    #[test]
1289    fn size_matches_serialized_length() {
1290        let test_cases = [
1291            // Minimal message
1292            MessageBuilder::new()
1293                .required_signatures(1)
1294                .lifetime_specifier(Hash::new_unique())
1295                .accounts(vec![Address::new_unique()])
1296                .build()
1297                .unwrap(),
1298            // With config
1299            MessageBuilder::new()
1300                .required_signatures(1)
1301                .lifetime_specifier(Hash::new_unique())
1302                .accounts(vec![Address::new_unique(), Address::new_unique()])
1303                .priority_fee(1000)
1304                .compute_unit_limit(200_000)
1305                .instruction(CompiledInstruction {
1306                    program_id_index: 1,
1307                    accounts: vec![0],
1308                    data: vec![1, 2, 3, 4],
1309                })
1310                .build()
1311                .unwrap(),
1312            // Multiple instructions with varying data
1313            MessageBuilder::new()
1314                .required_signatures(2)
1315                .readonly_signed_accounts(1)
1316                .readonly_unsigned_accounts(1)
1317                .lifetime_specifier(Hash::new_unique())
1318                .accounts(vec![
1319                    Address::new_unique(),
1320                    Address::new_unique(),
1321                    Address::new_unique(),
1322                    Address::new_unique(),
1323                ])
1324                .heap_size(65536)
1325                .instructions(vec![
1326                    CompiledInstruction {
1327                        program_id_index: 2,
1328                        accounts: vec![0, 1],
1329                        data: vec![],
1330                    },
1331                    CompiledInstruction {
1332                        program_id_index: 3,
1333                        accounts: vec![0, 1, 2],
1334                        data: vec![0xAA; 100],
1335                    },
1336                ])
1337                .build()
1338                .unwrap(),
1339        ];
1340
1341        for message in &test_cases {
1342            assert_eq!(message.size(), serialize(message).len());
1343        }
1344    }
1345
1346    #[test]
1347    fn byte_layout_without_config() {
1348        let fee_payer = Address::new_from_array([1u8; 32]);
1349        let program = Address::new_from_array([2u8; 32]);
1350        let blockhash = Hash::new_from_array([0xAB; 32]);
1351
1352        let message = MessageBuilder::new()
1353            .required_signatures(1)
1354            .lifetime_specifier(blockhash)
1355            .accounts(vec![fee_payer, program])
1356            .instruction(CompiledInstruction {
1357                program_id_index: 1,
1358                accounts: vec![0],
1359                data: vec![0xDE, 0xAD],
1360            })
1361            .build()
1362            .unwrap();
1363
1364        let bytes = serialize(&message);
1365
1366        // Build expected bytes manually per SIMD-0385
1367        //
1368        // num_required_signatures
1369        // num_readonly_signed_accounts
1370        // num_readonly_unsigned_accounts
1371        let mut expected = vec![1, 0, 0];
1372        expected.extend_from_slice(&0u32.to_le_bytes()); // ConfigMask = 0
1373        expected.extend_from_slice(&[0xAB; 32]); // LifetimeSpecifier
1374        expected.push(1); // NumInstructions
1375        expected.push(2); // NumAddresses
1376        expected.extend_from_slice(&[1u8; 32]); // fee_payer
1377        expected.extend_from_slice(&[2u8; 32]); // program
1378                                                // ConfigValues: none
1379        expected.push(1); // program_id_index
1380        expected.push(1); // num_accounts
1381        expected.extend_from_slice(&2u16.to_le_bytes()); // data_len
1382        expected.push(0); // account index 0
1383        expected.extend_from_slice(&[0xDE, 0xAD]); // data
1384        assert_eq!(bytes, expected);
1385    }
1386
1387    #[test]
1388    fn byte_layout_with_config() {
1389        let fee_payer = Address::new_from_array([1u8; 32]);
1390        let program = Address::new_from_array([2u8; 32]);
1391        let blockhash = Hash::new_from_array([0xBB; 32]);
1392
1393        let message = MessageBuilder::new()
1394            .required_signatures(1)
1395            .lifetime_specifier(blockhash)
1396            .accounts(vec![fee_payer, program])
1397            .priority_fee(0x0102030405060708u64)
1398            .compute_unit_limit(0x11223344u32)
1399            .instruction(CompiledInstruction {
1400                program_id_index: 1,
1401                accounts: vec![],
1402                data: vec![],
1403            })
1404            .build()
1405            .unwrap();
1406
1407        let bytes = serialize(&message);
1408
1409        let mut expected = vec![1, 0, 0];
1410        // ConfigMask: priority fee (bits 0,1) + CU limit (bit 2) = 0b111 = 7
1411        expected.extend_from_slice(&7u32.to_le_bytes());
1412        expected.extend_from_slice(&[0xBB; 32]);
1413        expected.push(1);
1414        expected.push(2);
1415        expected.extend_from_slice(&[1u8; 32]);
1416        expected.extend_from_slice(&[2u8; 32]);
1417        // Priority fee as u64 LE
1418        expected.extend_from_slice(&0x0102030405060708u64.to_le_bytes());
1419        // Compute unit limit as u32 LE
1420        expected.extend_from_slice(&0x11223344u32.to_le_bytes());
1421        expected.push(1); // program_id_index
1422        expected.push(0); // num_accounts
1423        expected.extend_from_slice(&0u16.to_le_bytes()); // data_len
1424
1425        assert_eq!(bytes, expected);
1426    }
1427
1428    #[test]
1429    fn roundtrip_preserves_all_config_fields() {
1430        let message = MessageBuilder::new()
1431            .required_signatures(1)
1432            .lifetime_specifier(Hash::new_unique())
1433            .accounts(vec![Address::new_unique(), Address::new_unique()])
1434            .priority_fee(1000)
1435            .compute_unit_limit(200_000)
1436            .loaded_accounts_data_size_limit(1_000_000)
1437            .heap_size(65536)
1438            .instruction(CompiledInstruction {
1439                program_id_index: 1,
1440                accounts: vec![0],
1441                data: vec![],
1442            })
1443            .build()
1444            .unwrap();
1445
1446        let serialized = serialize(&message);
1447        let (deserialized, _) = deserialize(&serialized).unwrap();
1448        assert_eq!(message.config, deserialized.config);
1449    }
1450}