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)                                 │
19//! │                                                        │
20//! │ * NumAddresses (u8)                                    │
21//! │                                                        │
22//! │ * Addresses [[u8; 32] x NumAddresses]                  │
23//! │                                                        │
24//! │ * ConfigValues ([[u8; 4] * variable based on mask])    │
25//! │                                                        │
26//! │ * WireInstructionHeaders [WireInstructionHeader        │
27//! │                            x NumInstructions]          │
28//! │                                                        │
29//! │ * InstructionPayloads (variable based on headers)      │
30//! │    └─ Per NumInstructions:                             │
31//! │         +- [u8] account indices                        │
32//! │         └─ [u8] instruction data                       │
33//! └────────────────────────────────────────────────────────┘
34//! ```
35
36// Re-export for convenient access to the message builder in tests.
37#[cfg(test)]
38pub use self::tests::MessageBuilder;
39#[cfg(feature = "serde")]
40use serde_derive::{Deserialize, Serialize};
41#[cfg(feature = "stable-abi")]
42use solana_frozen_abi_macro::{AbiExample, StableAbi, StableAbiSample};
43#[cfg(feature = "wincode")]
44use {
45    crate::v1::{WireInstructionHeader, FIXED_HEADER_SIZE},
46    core::{mem::MaybeUninit, slice::from_raw_parts},
47    wincode::{
48        config::{Config, ConfigCore},
49        context,
50        io::{Reader, Writer},
51        len::SeqLen,
52        ReadResult, SchemaRead, SchemaReadContext, SchemaWrite, WriteResult,
53    },
54};
55use {
56    crate::{
57        compiled_instruction::CompiledInstruction,
58        compiled_keys::CompiledKeys,
59        v1::{MessageError, TransactionConfig, TransactionConfigMask},
60        AccountKeys, AddressSet, CompileError, MessageHeader,
61    },
62    alloc::vec::Vec,
63    core::mem::size_of,
64    solana_address::Address,
65    solana_hash::Hash,
66    solana_instruction::Instruction,
67    solana_sanitize::{Sanitize, SanitizeError},
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 = "stable-abi", derive(AbiExample, StableAbi, StableAbiSample))]
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    /// # extern crate alloc;
154    /// # use alloc::borrow::Cow;
155    /// # use solana_account::Account;
156    /// use anyhow::Result;
157    /// use solana_instruction::{AccountMeta, Instruction};
158    /// use solana_keypair::Keypair;
159    /// use solana_message::{VersionedMessage, v1, v1::TransactionConfig};
160    /// use solana_address::Address;
161    /// use solana_rpc_client::rpc_client::RpcClient;
162    /// use solana_signer::Signer;
163    /// # mod solana_transaction {
164    /// #     pub mod versioned {
165    /// #         use solana_example_mocks::{solana_keypair::Keypair, solana_signer::SignerError};
166    /// #         use solana_message::VersionedMessage;
167    /// #         pub struct VersionedTransaction {
168    /// #             pub message: solana_message::VersionedMessage,
169    /// #         }
170    /// #         impl VersionedTransaction {
171    /// #             pub fn try_new(
172    /// #                 message: VersionedMessage,
173    /// #                 _keypairs: &[&Keypair],
174    /// #             ) -> core::result::Result<Self, solana_example_mocks::solana_signer::SignerError> {
175    /// #                 Ok(VersionedTransaction {
176    /// #                     message,
177    /// #                 })
178    /// #             }
179    /// #         }
180    /// #     }
181    /// # }
182    /// use solana_transaction::versioned::VersionedTransaction;
183    ///
184    /// fn create_v1_tx(
185    ///     client: &RpcClient,
186    ///     instruction: Instruction,
187    ///     payer: &Keypair,
188    /// ) -> Result<VersionedTransaction> {
189    ///     let blockhash = client.get_latest_blockhash()?;
190    ///     let tx = VersionedTransaction::try_new(
191    ///         VersionedMessage::V1(v1::Message::try_compile_with_config(
192    ///             &payer.pubkey(),
193    ///             &[instruction],
194    ///             blockhash,
195    ///             TransactionConfig::empty().with_compute_unit_limit(100),
196    ///         )?),
197    ///         &[payer],
198    ///     )?;
199    ///
200    ///     Ok(tx)
201    /// }
202    /// #
203    /// # let client = RpcClient::new(String::new());
204    /// # let payer = Keypair::new();
205    /// # let instruction = Instruction::new_with_bincode(Address::new_unique(), &(), vec![
206    /// #   AccountMeta::new(Address::new_unique(), false),
207    /// # ]);
208    /// # create_v1_tx(&client, instruction, &payer)?;
209    /// # Ok::<(), anyhow::Error>(())
210    /// ```
211    pub fn try_compile_with_config(
212        payer: &Address,
213        instructions: &[Instruction],
214        recent_blockhash: Hash,
215        config: TransactionConfig,
216    ) -> Result<Self, CompileError> {
217        let compiled_keys = CompiledKeys::compile(instructions, Some(*payer));
218        let (header, static_keys) = compiled_keys.try_into_message_components()?;
219
220        let account_keys = AccountKeys::new(&static_keys, None);
221        let instructions = account_keys.try_compile_instructions(instructions)?;
222
223        Ok(Self {
224            header,
225            config,
226            lifetime_specifier: recent_blockhash,
227            account_keys: static_keys,
228            instructions,
229        })
230    }
231
232    /// Returns the fee payer address (first account key).
233    pub fn fee_payer(&self) -> Option<&Address> {
234        self.account_keys.first()
235    }
236
237    /// Account keys are ordered with signers first: `[signers..., non-signers...]`.
238    /// An index falls in the signer region if it's less than `num_required_signatures`.
239    pub fn is_signer(&self, index: usize) -> bool {
240        index < usize::from(self.header.num_required_signatures)
241    }
242
243    /// Returns true if the account at this index is both a signer and writable.
244    pub fn is_signer_writable(&self, index: usize) -> bool {
245        if !self.is_signer(index) {
246            return false;
247        }
248        // Within the signer region, the first (num_required_signatures - num_readonly_signed)
249        // accounts are writable signers.
250        let num_writable_signers = usize::from(self.header.num_required_signatures)
251            .saturating_sub(usize::from(self.header.num_readonly_signed_accounts));
252        index < num_writable_signers
253    }
254
255    /// Returns true if any instruction invokes the account at this index as a program.
256    pub fn is_key_called_as_program(&self, key_index: usize) -> bool {
257        crate::is_key_called_as_program(&self.instructions, key_index)
258    }
259
260    /// Returns `true` if the account at the specified index was requested to be
261    /// writable.
262    ///
263    /// This method should not be used directly.
264    #[inline(always)]
265    #[cfg(feature = "std")]
266    pub(crate) fn is_writable_index(&self, i: usize) -> bool {
267        crate::is_writable_index(i, self.header, &self.account_keys)
268    }
269
270    /// Returns true if the BPF upgradeable loader is present in the account keys.
271    pub fn is_upgradeable_loader_present(&self) -> bool {
272        crate::is_upgradeable_loader_present(&self.account_keys)
273    }
274
275    /// Returns `true` if the account at the specified index was requested as
276    /// writable.
277    ///
278    ///
279    /// # Important
280    ///
281    /// Before loading addresses, we can't demote write locks properly so this should
282    /// not be used by the runtime. The `reserved_addresses` parameter is optional to
283    /// allow clients to approximate writability without requiring fetching the latest
284    /// set of protocol-reserved addresses.
285    ///
286    /// Program accounts are demoted from writable to readonly, unless the upgradeable
287    /// loader is present in which case they are left as writable since upgradeable
288    /// programs need to be writable for upgrades.
289    pub fn is_maybe_writable_with_reserved_addresses(
290        &self,
291        key_index: usize,
292        reserved_addresses: Option<&impl AddressSet>,
293    ) -> bool {
294        crate::is_maybe_writable(
295            key_index,
296            self.header,
297            &self.account_keys,
298            &self.instructions,
299            reserved_addresses,
300        )
301    }
302
303    pub fn demote_program_id(&self, i: usize) -> bool {
304        crate::is_program_id_write_demoted(i, &self.account_keys, &self.instructions)
305    }
306
307    /// Serialize this message with the V1 version prefix byte.
308    #[cfg(feature = "wincode")]
309    pub fn serialize(&self) -> Vec<u8> {
310        wincode::serialize(&(crate::v1::V1_PREFIX, self)).unwrap()
311    }
312
313    /// Calculate the serialized size of the message in bytes.
314    #[allow(clippy::arithmetic_side_effects)]
315    #[inline(always)]
316    pub fn size(&self) -> usize {
317        size_of::<MessageHeader>()                           // legacy header
318            + size_of::<TransactionConfigMask>()             // config mask
319            + size_of::<Hash>()                              // lifetime specifier
320            + size_of::<u8>()                                // number of instructions
321            + size_of::<u8>()                                // number of addresses
322            + self.account_keys.len() * size_of::<Address>() // addresses
323            + self.config.size()                             // config values
324            + self.instructions.len()
325                * (
326                    size_of::<u8>()
327                    + size_of::<u8>()
328                    + size_of::<u16>()
329                )                                            // instruction headers
330            + self
331                .instructions
332                .iter()
333                .map(|ix| {
334                    (ix.accounts.len() * size_of::<u8>())
335                    + ix.data.len()
336                })
337                .sum::<usize>() // instruction payloads
338    }
339
340    pub fn validate(&self) -> Result<(), MessageError> {
341        // The instruction and address counts are encoded as u8 on the wire.
342        if self.instructions.len() > u8::MAX as usize {
343            return Err(MessageError::TooManyInstructions);
344        }
345
346        let num_account_keys = self.account_keys.len();
347
348        if num_account_keys > u8::MAX as usize {
349            return Err(MessageError::TooManyAddresses);
350        }
351
352        // `num_addresses` >= `num_required_signatures` + `num_readonly_unsigned_accounts`
353        let min_accounts = usize::from(self.header.num_required_signatures)
354            .saturating_add(usize::from(self.header.num_readonly_unsigned_accounts));
355
356        if num_account_keys < min_accounts {
357            return Err(MessageError::NotEnoughAddressesForSignatures);
358        }
359
360        // must have at least 1 RW fee-payer (`num_readonly_signed` < `num_required_signatures`)
361        if self.header.num_readonly_signed_accounts >= self.header.num_required_signatures {
362            return Err(MessageError::ZeroSigners);
363        }
364
365        // The config mask is regenerated from the typed `TransactionConfig` on
366        // serialization, so a malformed mask cannot exist here. Invalid/unknown mask
367        // bits only occur in raw wire bytes and are rejected during deserialization.
368
369        // instruction account indices must be < `num_addresses`
370        let max_account_index = num_account_keys
371            .checked_sub(1)
372            .ok_or(MessageError::NotEnoughAccountKeys)?;
373
374        for instruction in &self.instructions {
375            // program id must be in static accounts
376            if usize::from(instruction.program_id_index) > max_account_index {
377                return Err(MessageError::InvalidInstructionAccountIndex);
378            }
379
380            // program cannot be fee payer
381            if instruction.program_id_index == 0 {
382                return Err(MessageError::InvalidInstructionAccountIndex);
383            }
384
385            // instruction accounts count must fit in u8
386            if instruction.accounts.len() > u8::MAX as usize {
387                return Err(MessageError::InstructionAccountsTooLarge);
388            }
389
390            // instruction data length must fit in u16
391            if instruction.data.len() > u16::MAX as usize {
392                return Err(MessageError::InstructionDataTooLarge);
393            }
394
395            // all account indices must be valid
396            for &account_index in &instruction.accounts {
397                if usize::from(account_index) > max_account_index {
398                    return Err(MessageError::InvalidInstructionAccountIndex);
399                }
400            }
401        }
402
403        Ok(())
404    }
405}
406
407impl Sanitize for Message {
408    fn sanitize(&self) -> Result<(), SanitizeError> {
409        Ok(self.validate()?)
410    }
411}
412
413#[cfg(feature = "wincode")]
414unsafe impl<C: ConfigCore> SchemaWrite<C> for Message {
415    type Src = Self;
416
417    #[inline(always)]
418    fn size_of(src: &Self::Src) -> WriteResult<usize> {
419        Ok(src.size())
420    }
421
422    fn write(mut writer: impl Writer, src: &Self::Src) -> WriteResult<()> {
423        // SAFETY: `Message::size()` yields the exact number of bytes to be written.
424        let mut writer = unsafe { writer.as_trusted_for(src.size()) }?;
425        writer.write(&[
426            src.header.num_required_signatures,
427            src.header.num_readonly_signed_accounts,
428            src.header.num_readonly_unsigned_accounts,
429        ])?;
430        let mask = TransactionConfigMask::from(&src.config).0.to_le_bytes();
431        writer.write(&mask)?;
432        writer.write(src.lifetime_specifier.as_bytes())?;
433        writer.write(&[src.instructions.len() as u8, src.account_keys.len() as u8])?;
434
435        // SAFETY: `Address` is `#[repr(transparent)]` over `[u8; 32]`, so it is safe to
436        // treat as bytes.
437        #[expect(clippy::arithmetic_side_effects)]
438        let account_keys = unsafe {
439            from_raw_parts(
440                src.account_keys.as_ptr().cast::<u8>(),
441                src.account_keys.len() * size_of::<Address>(),
442            )
443        };
444        writer.write(account_keys)?;
445
446        if let Some(value) = src.config.priority_fee {
447            writer.write(&value.to_le_bytes())?;
448        }
449        if let Some(value) = src.config.compute_unit_limit {
450            writer.write(&value.to_le_bytes())?;
451        }
452        if let Some(value) = src.config.loaded_accounts_data_size_limit {
453            writer.write(&value.to_le_bytes())?;
454        }
455        if let Some(value) = src.config.heap_size {
456            writer.write(&value.to_le_bytes())?;
457        }
458
459        for ix in &src.instructions {
460            writer.write(&[ix.program_id_index, ix.accounts.len() as u8])?;
461            writer.write(&(ix.data.len() as u16).to_le_bytes())?;
462        }
463
464        for ix in &src.instructions {
465            writer.write(&ix.accounts)?;
466            writer.write(&ix.data)?;
467        }
468
469        writer.finish()?;
470
471        Ok(())
472    }
473}
474
475#[cfg(feature = "wincode")]
476unsafe impl<'de, C: Config> SchemaRead<'de, C> for Message {
477    type Dst = Message;
478
479    #[expect(clippy::arithmetic_side_effects)]
480    fn read(mut reader: impl Reader<'de>, dst: &mut MaybeUninit<Self::Dst>) -> ReadResult<()> {
481        let (header, lifetime_specifier, config_mask, num_instructions, num_addresses) = {
482            // SAFETY: the following reads consume exactly `FIXED_HEADER_SIZE` bytes.
483            // - MessageHeader (3 bytes)
484            // - TransactionConfigMask (4 bytes)
485            // - Hash (32 bytes)
486            // - num_instructions (1 byte)
487            // - num_addresses (1 byte)
488            let mut reader = unsafe { reader.as_trusted_for(FIXED_HEADER_SIZE)? };
489            let header = <MessageHeader as SchemaRead<C>>::get(reader.by_ref())?;
490            let config_mask = TransactionConfigMask(u32::from_le_bytes(reader.take_array()?));
491            let lifetime_specifier = <Hash as SchemaRead<C>>::get(reader.by_ref())?;
492            let num_instructions = reader.take_byte()? as usize;
493            let num_addresses = reader.take_byte()? as usize;
494            (
495                header,
496                lifetime_specifier,
497                config_mask,
498                num_instructions,
499                num_addresses,
500            )
501        };
502
503        // Reject masks we cannot round-trip. Unknown bits would be silently dropped
504        // on re-serialization (this message is signed, so that invalidates the
505        // signature), and a partial priority-fee bit pair is malformed. Support for
506        // new bits is added by a newer library release that promotes them to known.
507        if config_mask.has_unknown_bits() || config_mask.has_invalid_priority_fee_bits() {
508            return Err(wincode::error::invalid_value(
509                "invalid transaction config mask",
510            ));
511        }
512
513        <C::LengthEncoding as SeqLen<C>>::prealloc_check::<Address>(num_addresses)?;
514        let account_keys = <Vec<Address> as SchemaReadContext<C, context::Len>>::get_with_context(
515            context::Len(num_addresses),
516            reader.by_ref(),
517        )?;
518
519        let mut config = TransactionConfig::empty();
520        if config_mask.has_priority_fee() {
521            config.priority_fee = Some(u64::from_le_bytes(reader.take_array()?));
522        }
523        if config_mask.has_compute_unit_limit() {
524            config.compute_unit_limit = Some(u32::from_le_bytes(reader.take_array()?));
525        }
526        if config_mask.has_loaded_accounts_data_size() {
527            config.loaded_accounts_data_size_limit = Some(u32::from_le_bytes(reader.take_array()?));
528        }
529        if config_mask.has_heap_size() {
530            config.heap_size = Some(u32::from_le_bytes(reader.take_array()?));
531        }
532
533        // SAFETY:
534        // - `take_borrowed(num_instructions * size_of::<WireInstructionHeader>())` returns
535        //   exactly the requested number of bytes, or errors.
536        // - `take_borrowed` returns a stable borrow from the backing buffer, so the
537        //   resulting slice remains valid across subsequent reader operations.
538        // - The `const` block above pins `WireInstructionHeader`'s layout to a packed
539        //   4 bytes with fields in wire order, so the reinterpretation is exact.
540        let instruction_headers = unsafe {
541            from_raw_parts(
542                reader
543                    .take_borrowed(num_instructions * size_of::<WireInstructionHeader>())?
544                    .as_ptr() as *const WireInstructionHeader,
545                num_instructions,
546            )
547        };
548        let mut instructions = Vec::with_capacity(num_instructions);
549        for header in instruction_headers {
550            let program_id_index = header.program_id_index;
551            let num_accounts = header.num_accounts as usize;
552            let data_len = u16::from_le_bytes(header.data_len) as usize;
553
554            <C::LengthEncoding as SeqLen<C>>::prealloc_check::<u8>(num_accounts)?;
555            let accounts = <Vec<u8> as SchemaReadContext<C, context::Len>>::get_with_context(
556                context::Len(num_accounts),
557                reader.by_ref(),
558            )?;
559            <C::LengthEncoding as SeqLen<C>>::prealloc_check::<u8>(data_len)?;
560            let data = <Vec<u8> as SchemaReadContext<C, context::Len>>::get_with_context(
561                context::Len(data_len),
562                reader.by_ref(),
563            )?;
564
565            instructions.push(CompiledInstruction {
566                program_id_index,
567                accounts,
568                data,
569            });
570        }
571
572        dst.write(Message {
573            header,
574            lifetime_specifier,
575            config,
576            account_keys,
577            instructions,
578        });
579
580        Ok(())
581    }
582}
583
584/// Deserialize the message from the provided input buffer, returning the message and
585/// the number of bytes read.
586#[cfg(feature = "wincode")]
587#[inline]
588pub fn deserialize(input: &[u8]) -> wincode::ReadResult<Message> {
589    wincode::deserialize(input)
590}
591
592#[cfg(test)]
593mod tests {
594    use {
595        super::*,
596        crate::v1::{MAX_HEAP_SIZE, MIN_HEAP_SIZE},
597        alloc::vec,
598        solana_sdk_ids::bpf_loader_upgradeable,
599    };
600
601    /// Builder for constructing V1 messages.
602    ///
603    /// This is used in tests to simplify message construction and validation. For
604    /// client code, users should construct messages using
605    /// `try_compile_with_config`.
606    #[derive(Debug, Clone, Default)]
607    pub struct MessageBuilder {
608        header: MessageHeader,
609        config: TransactionConfig,
610        lifetime_specifier: Option<Hash>,
611        account_keys: Vec<Address>,
612        instructions: Vec<CompiledInstruction>,
613    }
614
615    impl MessageBuilder {
616        pub fn new() -> Self {
617            Self::default()
618        }
619
620        #[must_use]
621        pub fn required_signatures(mut self, count: u8) -> Self {
622            self.header.num_required_signatures = count;
623            self
624        }
625
626        #[must_use]
627        pub fn readonly_signed_accounts(mut self, count: u8) -> Self {
628            self.header.num_readonly_signed_accounts = count;
629            self
630        }
631
632        #[must_use]
633        pub fn readonly_unsigned_accounts(mut self, count: u8) -> Self {
634            self.header.num_readonly_unsigned_accounts = count;
635            self
636        }
637
638        #[must_use]
639        pub fn lifetime_specifier(mut self, hash: Hash) -> Self {
640            self.lifetime_specifier = Some(hash);
641            self
642        }
643
644        #[must_use]
645        pub fn config(mut self, config: TransactionConfig) -> Self {
646            self.config = config;
647            self
648        }
649
650        #[must_use]
651        pub fn priority_fee(mut self, fee: u64) -> Self {
652            self.config.priority_fee = Some(fee);
653            self
654        }
655
656        #[must_use]
657        pub fn compute_unit_limit(mut self, limit: u32) -> Self {
658            self.config.compute_unit_limit = Some(limit);
659            self
660        }
661
662        #[must_use]
663        pub fn loaded_accounts_data_size_limit(mut self, limit: u32) -> Self {
664            self.config.loaded_accounts_data_size_limit = Some(limit);
665            self
666        }
667
668        #[must_use]
669        pub fn heap_size(mut self, size: u32) -> Self {
670            self.config.heap_size = Some(size);
671            self
672        }
673
674        #[must_use]
675        pub fn account(mut self, key: Address) -> Self {
676            self.account_keys.push(key);
677            self
678        }
679
680        #[must_use]
681        pub fn accounts(mut self, keys: Vec<Address>) -> Self {
682            self.account_keys = keys;
683            self
684        }
685
686        #[must_use]
687        pub fn instruction(mut self, instruction: CompiledInstruction) -> Self {
688            self.instructions.push(instruction);
689            self
690        }
691
692        #[must_use]
693        pub fn instructions(mut self, instructions: Vec<CompiledInstruction>) -> Self {
694            self.instructions = instructions;
695            self
696        }
697
698        /// Build the message, validating all constraints.
699        pub fn build(self) -> Result<Message, MessageError> {
700            let lifetime_specifier = self
701                .lifetime_specifier
702                .ok_or(MessageError::MissingLifetimeSpecifier)?;
703
704            let message = Message::new(
705                self.header,
706                self.config,
707                lifetime_specifier,
708                self.account_keys,
709                self.instructions,
710            );
711
712            message.validate()?;
713
714            Ok(message)
715        }
716    }
717
718    fn create_test_message() -> Message {
719        MessageBuilder::new()
720            .required_signatures(1)
721            .readonly_unsigned_accounts(1)
722            .lifetime_specifier(Hash::new_unique())
723            .accounts(vec![
724                Address::new_unique(), // fee payer
725                Address::new_unique(), // program
726                Address::new_unique(), // readonly account
727            ])
728            .compute_unit_limit(200_000)
729            .instruction(CompiledInstruction {
730                program_id_index: 1,
731                accounts: vec![0, 2],
732                data: vec![1, 2, 3, 4],
733            })
734            .build()
735            .unwrap()
736    }
737
738    #[test]
739    fn fee_payer_returns_first_account() {
740        let fee_payer = Address::new_unique();
741        let message = MessageBuilder::new()
742            .required_signatures(1)
743            .lifetime_specifier(Hash::new_unique())
744            .accounts(vec![fee_payer, Address::new_unique()])
745            .build()
746            .unwrap();
747
748        assert_eq!(message.fee_payer(), Some(&fee_payer));
749    }
750
751    #[test]
752    fn fee_payer_returns_none_for_empty_accounts() {
753        // Direct construction to bypass builder validation
754        let message = Message::new(
755            MessageHeader::default(),
756            TransactionConfig::default(),
757            Hash::new_unique(),
758            vec![],
759            vec![],
760        );
761
762        assert_eq!(message.fee_payer(), None);
763    }
764
765    #[test]
766    fn is_signer_checks_signature_requirement() {
767        let message = create_test_message();
768        assert!(message.is_signer(0)); // Fee payer is signer
769        assert!(!message.is_signer(1)); // Program is not signer
770        assert!(!message.is_signer(2)); // Readonly account is not signer
771    }
772
773    #[test]
774    fn is_signer_writable_identifies_writable_signers() {
775        let message = MessageBuilder::new()
776            .required_signatures(3)
777            .readonly_signed_accounts(1) // Last signer is readonly
778            .lifetime_specifier(Hash::new_unique())
779            .accounts(vec![
780                Address::new_unique(), // 0: writable signer
781                Address::new_unique(), // 1: writable signer
782                Address::new_unique(), // 2: readonly signer
783                Address::new_unique(), // 3: non-signer
784            ])
785            .build()
786            .unwrap();
787
788        // Writable signers
789        assert!(message.is_signer_writable(0));
790        assert!(message.is_signer_writable(1));
791        // Readonly signer
792        assert!(!message.is_signer_writable(2));
793        // Non-signers
794        assert!(!message.is_signer_writable(3));
795        assert!(!message.is_signer_writable(100));
796    }
797
798    #[test]
799    fn is_signer_writable_all_writable_when_no_readonly() {
800        let message = MessageBuilder::new()
801            .required_signatures(2)
802            .readonly_signed_accounts(0) // All signers are writable
803            .lifetime_specifier(Hash::new_unique())
804            .accounts(vec![
805                Address::new_unique(),
806                Address::new_unique(),
807                Address::new_unique(),
808            ])
809            .build()
810            .unwrap();
811
812        assert!(message.is_signer_writable(0));
813        assert!(message.is_signer_writable(1));
814        assert!(!message.is_signer_writable(2)); // Not a signer
815    }
816
817    #[test]
818    fn is_key_called_as_program_detects_program_indices() {
819        let message = create_test_message();
820        // program_id_index = 1 in create_test_message
821        assert!(message.is_key_called_as_program(1));
822        assert!(!message.is_key_called_as_program(0));
823        assert!(!message.is_key_called_as_program(2));
824        // Index > u8::MAX can't match any program_id_index
825        assert!(!message.is_key_called_as_program(256));
826        assert!(!message.is_key_called_as_program(10_000));
827    }
828
829    #[test]
830    fn is_upgradeable_loader_present_detects_loader() {
831        let message = create_test_message();
832        assert!(!message.is_upgradeable_loader_present());
833
834        let mut message_with_loader = create_test_message();
835        message_with_loader
836            .account_keys
837            .push(bpf_loader_upgradeable::id());
838        assert!(message_with_loader.is_upgradeable_loader_present());
839    }
840
841    #[test]
842    fn is_writable_index_respects_header_layout() {
843        let message = create_test_message();
844        // Account layout: [writable signer (fee payer), writable unsigned (program), readonly unsigned]
845        assert!(message.is_writable_index(0)); // Fee payer is writable
846        assert!(message.is_writable_index(1)); // Program position is writable unsigned
847        assert!(!message.is_writable_index(2)); // Last account is readonly
848    }
849
850    #[test]
851    fn is_writable_index_handles_mixed_signer_permissions() {
852        let mut message = create_test_message();
853        // 2 signers: first writable, second readonly
854        message.header.num_required_signatures = 2;
855        message.header.num_readonly_signed_accounts = 1;
856        message.header.num_readonly_unsigned_accounts = 1;
857        message.account_keys = vec![
858            Address::new_unique(), // writable signer
859            Address::new_unique(), // readonly signer
860            Address::new_unique(), // readonly unsigned
861        ];
862        message.instructions[0].program_id_index = 2;
863        message.instructions[0].accounts = vec![0, 1];
864
865        assert!(message.sanitize().is_ok());
866        assert!(message.is_writable_index(0)); // writable signer
867        assert!(!message.is_writable_index(1)); // readonly signer
868        assert!(!message.is_writable_index(2)); // readonly unsigned
869        assert!(!message.is_writable_index(999)); // out of bounds
870    }
871
872    #[test]
873    fn sanitize_accepts_valid_message() {
874        let message = create_test_message();
875        assert!(message.sanitize().is_ok());
876    }
877
878    #[test]
879    fn sanitize_rejects_zero_signers() {
880        let mut message = create_test_message();
881        message.header.num_required_signatures = 0;
882        assert_eq!(message.sanitize(), Err(SanitizeError::InvalidValue));
883    }
884
885    #[test]
886    fn sanitize_accepts_over_12_signatures() {
887        let mut message = create_test_message();
888        message.header.num_required_signatures = 13;
889        message.account_keys = (0..14).map(|_| Address::new_unique()).collect();
890        assert!(message.sanitize().is_ok());
891    }
892
893    #[test]
894    fn sanitize_accepts_96_addresses() {
895        let mut message = create_test_message();
896        message.account_keys = (0..96).map(|_| Address::new_unique()).collect();
897        assert!(message.sanitize().is_ok());
898        let serialized = wincode::serialize(&message).unwrap();
899        assert_eq!(
900            wincode::deserialize::<Message>(&serialized).unwrap(),
901            message
902        );
903    }
904
905    #[test]
906    fn sanitize_rejects_addresses_exceeding_wire_count() {
907        let mut message = create_test_message();
908        message.account_keys = (0..=u8::MAX).map(|_| Address::new_unique()).collect();
909        assert_eq!(message.sanitize(), Err(SanitizeError::IndexOutOfBounds));
910    }
911
912    #[test]
913    fn sanitize_accepts_over_64_instructions() {
914        let mut message = create_test_message();
915        message.instructions = (0..65)
916            .map(|_| CompiledInstruction {
917                program_id_index: 1,
918                accounts: vec![0],
919                data: vec![],
920            })
921            .collect();
922        assert!(message.sanitize().is_ok());
923    }
924
925    #[test]
926    fn sanitize_rejects_instructions_exceeding_wire_count() {
927        let mut message = create_test_message();
928        message.instructions = vec![message.instructions[0].clone(); usize::from(u8::MAX) + 1];
929        assert_eq!(message.sanitize(), Err(SanitizeError::IndexOutOfBounds));
930    }
931
932    #[test]
933    fn sanitize_rejects_insufficient_accounts_for_header() {
934        let mut message = create_test_message();
935        // min_accounts = num_required_signatures + num_readonly_unsigned_accounts
936        // Set readonly_unsigned high so min_accounts > account_keys.len()
937        message.header.num_readonly_unsigned_accounts = 10;
938        assert_eq!(message.sanitize(), Err(SanitizeError::IndexOutOfBounds));
939    }
940
941    #[test]
942    fn sanitize_rejects_all_signers_readonly() {
943        let mut message = create_test_message();
944        message.header.num_readonly_signed_accounts = 1; // All signers readonly
945        assert_eq!(message.sanitize(), Err(SanitizeError::InvalidValue));
946    }
947
948    #[test]
949    fn sanitize_accepts_duplicate_addresses() {
950        let mut message = create_test_message();
951        let dup = message.account_keys[0];
952        message.account_keys[1] = dup;
953        assert!(message.sanitize().is_ok());
954    }
955
956    #[test]
957    fn sanitize_accepts_unaligned_heap_size() {
958        let mut message = create_test_message();
959        message.config.heap_size = Some(1025); // Not a multiple of 1024
960        assert!(message.sanitize().is_ok());
961    }
962
963    #[test]
964    fn sanitize_accepts_heap_size_below_minimum() {
965        let mut message = create_test_message();
966        message.config.heap_size = Some(MIN_HEAP_SIZE - 1);
967        assert!(message.sanitize().is_ok());
968    }
969
970    #[test]
971    fn sanitize_accepts_heap_size_above_maximum() {
972        let mut message = create_test_message();
973        message.config.heap_size = Some(MAX_HEAP_SIZE + 1);
974        assert!(message.sanitize().is_ok());
975    }
976
977    #[test]
978    fn sanitize_accepts_minimum_heap_size() {
979        let mut message = create_test_message();
980        message.config.heap_size = Some(MIN_HEAP_SIZE);
981        assert!(message.sanitize().is_ok());
982    }
983
984    #[test]
985    fn sanitize_accepts_maximum_heap_size() {
986        let mut message = create_test_message();
987        message.config.heap_size = Some(MAX_HEAP_SIZE);
988        assert!(message.sanitize().is_ok());
989    }
990
991    #[test]
992    fn sanitize_accepts_aligned_heap_size() {
993        let mut message = create_test_message();
994        message.config.heap_size = Some(65536); // 64KB, valid
995        assert!(message.sanitize().is_ok());
996    }
997
998    #[test]
999    fn sanitize_rejects_invalid_program_id_index() {
1000        let mut message = create_test_message();
1001        message.instructions[0].program_id_index = 99;
1002        assert_eq!(message.sanitize(), Err(SanitizeError::IndexOutOfBounds));
1003    }
1004
1005    #[test]
1006    fn sanitize_rejects_fee_payer_as_program() {
1007        let mut message = create_test_message();
1008        message.instructions[0].program_id_index = 0;
1009        assert_eq!(message.sanitize(), Err(SanitizeError::IndexOutOfBounds));
1010    }
1011
1012    #[test]
1013    fn sanitize_rejects_instruction_with_too_many_accounts() {
1014        let mut message = create_test_message();
1015        message.instructions[0].accounts = vec![0u8; (u8::MAX as usize) + 1];
1016        assert_eq!(message.sanitize(), Err(SanitizeError::InvalidValue));
1017    }
1018
1019    #[test]
1020    fn sanitize_rejects_invalid_instruction_account_index() {
1021        let mut message = create_test_message();
1022        message.instructions[0].accounts = vec![0, 99]; // 99 is out of bounds
1023        assert_eq!(message.sanitize(), Err(SanitizeError::IndexOutOfBounds));
1024    }
1025
1026    #[test]
1027    fn sanitize_accepts_64_addresses() {
1028        let mut message = create_test_message();
1029        message.account_keys = (0..64).map(|_| Address::new_unique()).collect();
1030        message.header.num_required_signatures = 1;
1031        message.header.num_readonly_signed_accounts = 0;
1032        message.header.num_readonly_unsigned_accounts = 1;
1033        message.instructions[0].program_id_index = 1;
1034        message.instructions[0].accounts = vec![0, 2];
1035        assert!(message.sanitize().is_ok());
1036    }
1037
1038    #[test]
1039    fn sanitize_accepts_64_instructions() {
1040        let mut message = create_test_message();
1041        message.instructions = (0..64)
1042            .map(|_| CompiledInstruction {
1043                program_id_index: 1,
1044                accounts: vec![0, 2],
1045                data: vec![1, 2, 3],
1046            })
1047            .collect();
1048        assert!(message.sanitize().is_ok());
1049    }
1050
1051    #[test]
1052    fn size_matches_serialized_length() {
1053        let test_cases = [
1054            // Minimal message
1055            MessageBuilder::new()
1056                .required_signatures(1)
1057                .lifetime_specifier(Hash::new_unique())
1058                .accounts(vec![Address::new_unique()])
1059                .build()
1060                .unwrap(),
1061            // With config
1062            MessageBuilder::new()
1063                .required_signatures(1)
1064                .lifetime_specifier(Hash::new_unique())
1065                .accounts(vec![Address::new_unique(), Address::new_unique()])
1066                .priority_fee(1000)
1067                .compute_unit_limit(200_000)
1068                .instruction(CompiledInstruction {
1069                    program_id_index: 1,
1070                    accounts: vec![0],
1071                    data: vec![1, 2, 3, 4],
1072                })
1073                .build()
1074                .unwrap(),
1075            // Multiple instructions with varying data
1076            MessageBuilder::new()
1077                .required_signatures(2)
1078                .readonly_signed_accounts(1)
1079                .readonly_unsigned_accounts(1)
1080                .lifetime_specifier(Hash::new_unique())
1081                .accounts(vec![
1082                    Address::new_unique(),
1083                    Address::new_unique(),
1084                    Address::new_unique(),
1085                    Address::new_unique(),
1086                ])
1087                .heap_size(65536)
1088                .instructions(vec![
1089                    CompiledInstruction {
1090                        program_id_index: 2,
1091                        accounts: vec![0, 1],
1092                        data: vec![],
1093                    },
1094                    CompiledInstruction {
1095                        program_id_index: 3,
1096                        accounts: vec![0, 1, 2],
1097                        data: vec![0xAA; 100],
1098                    },
1099                ])
1100                .build()
1101                .unwrap(),
1102        ];
1103
1104        for message in &test_cases {
1105            assert_eq!(message.size(), wincode::serialize(message).unwrap().len());
1106        }
1107    }
1108
1109    #[test]
1110    fn byte_layout_without_config() {
1111        let fee_payer = Address::new_from_array([1u8; 32]);
1112        let program = Address::new_from_array([2u8; 32]);
1113        let blockhash = Hash::new_from_array([0xAB; 32]);
1114
1115        let message = MessageBuilder::new()
1116            .required_signatures(1)
1117            .lifetime_specifier(blockhash)
1118            .accounts(vec![fee_payer, program])
1119            .instruction(CompiledInstruction {
1120                program_id_index: 1,
1121                accounts: vec![0],
1122                data: vec![0xDE, 0xAD],
1123            })
1124            .build()
1125            .unwrap();
1126
1127        let bytes = wincode::serialize(&message).unwrap();
1128
1129        // Build expected bytes manually per SIMD-0385
1130        //
1131        // num_required_signatures
1132        // num_readonly_signed_accounts
1133        // num_readonly_unsigned_accounts
1134        let mut expected = vec![1, 0, 0];
1135        expected.extend_from_slice(&0u32.to_le_bytes()); // ConfigMask = 0
1136        expected.extend_from_slice(&[0xAB; 32]); // LifetimeSpecifier
1137        expected.push(1); // NumInstructions
1138        expected.push(2); // NumAddresses
1139        expected.extend_from_slice(&[1u8; 32]); // fee_payer
1140        expected.extend_from_slice(&[2u8; 32]); // program
1141                                                // ConfigValues: none
1142        expected.push(1); // program_id_index
1143        expected.push(1); // num_accounts
1144        expected.extend_from_slice(&2u16.to_le_bytes()); // data_len
1145        expected.push(0); // account index 0
1146        expected.extend_from_slice(&[0xDE, 0xAD]); // data
1147        assert_eq!(bytes, expected);
1148    }
1149
1150    #[test]
1151    fn byte_layout_with_config() {
1152        let fee_payer = Address::new_from_array([1u8; 32]);
1153        let program = Address::new_from_array([2u8; 32]);
1154        let blockhash = Hash::new_from_array([0xBB; 32]);
1155
1156        let message = MessageBuilder::new()
1157            .required_signatures(1)
1158            .lifetime_specifier(blockhash)
1159            .accounts(vec![fee_payer, program])
1160            .priority_fee(0x0102030405060708u64)
1161            .compute_unit_limit(0x11223344u32)
1162            .instruction(CompiledInstruction {
1163                program_id_index: 1,
1164                accounts: vec![],
1165                data: vec![],
1166            })
1167            .build()
1168            .unwrap();
1169
1170        let bytes = wincode::serialize(&message).unwrap();
1171
1172        let mut expected = vec![1, 0, 0];
1173        // ConfigMask: priority fee (bits 0,1) + CU limit (bit 2) = 0b111 = 7
1174        expected.extend_from_slice(&7u32.to_le_bytes());
1175        expected.extend_from_slice(&[0xBB; 32]);
1176        expected.push(1);
1177        expected.push(2);
1178        expected.extend_from_slice(&[1u8; 32]);
1179        expected.extend_from_slice(&[2u8; 32]);
1180        // Priority fee as u64 LE
1181        expected.extend_from_slice(&0x0102030405060708u64.to_le_bytes());
1182        // Compute unit limit as u32 LE
1183        expected.extend_from_slice(&0x11223344u32.to_le_bytes());
1184        expected.push(1); // program_id_index
1185        expected.push(0); // num_accounts
1186        expected.extend_from_slice(&0u16.to_le_bytes()); // data_len
1187
1188        assert_eq!(bytes, expected);
1189    }
1190
1191    #[test]
1192    fn roundtrip_preserves_all_config_fields() {
1193        let message = MessageBuilder::new()
1194            .required_signatures(1)
1195            .lifetime_specifier(Hash::new_unique())
1196            .accounts(vec![Address::new_unique(), Address::new_unique()])
1197            .priority_fee(1000)
1198            .compute_unit_limit(200_000)
1199            .loaded_accounts_data_size_limit(1_000_000)
1200            .heap_size(65536)
1201            .instruction(CompiledInstruction {
1202                program_id_index: 1,
1203                accounts: vec![0],
1204                data: vec![],
1205            })
1206            .build()
1207            .unwrap();
1208
1209        let serialized = wincode::serialize(&message).unwrap();
1210        let deserialized = deserialize(&serialized).unwrap();
1211        assert_eq!(message.config, deserialized.config);
1212    }
1213
1214    #[test]
1215    fn deserialize_rejects_unknown_config_mask_bits() {
1216        let message = MessageBuilder::default()
1217            .required_signatures(1)
1218            .lifetime_specifier(Hash::new_unique())
1219            .accounts(vec![Address::new_unique(), Address::new_unique()])
1220            .instruction(CompiledInstruction {
1221                program_id_index: 1,
1222                accounts: vec![0],
1223                data: vec![],
1224            })
1225            .build()
1226            .unwrap();
1227
1228        let mut serialized = wincode::serialize(&message).unwrap();
1229        // Round-trips cleanly with a zero (all-known) mask.
1230        assert!(deserialize(&serialized).is_ok());
1231
1232        // The config mask is a u32 (little-endian) immediately after the 3-byte
1233        // header. Set the first bit past KNOWN_BITS; it must be rejected rather than
1234        // silently dropped, regardless of how many bits KNOWN_BITS covers.
1235        let unknown_bit = 1u32 << TransactionConfigMask::KNOWN_BITS.trailing_ones();
1236        let mask = u32::from_le_bytes(serialized[3..7].try_into().unwrap()) | unknown_bit;
1237        serialized[3..7].copy_from_slice(&mask.to_le_bytes());
1238        assert!(deserialize(&serialized).is_err());
1239    }
1240
1241    #[test]
1242    fn deserialize_rejects_partial_priority_fee_bits() {
1243        let message = MessageBuilder::default()
1244            .required_signatures(1)
1245            .lifetime_specifier(Hash::new_unique())
1246            .accounts(vec![Address::new_unique(), Address::new_unique()])
1247            .instruction(CompiledInstruction {
1248                program_id_index: 1,
1249                accounts: vec![0],
1250                data: vec![],
1251            })
1252            .build()
1253            .unwrap();
1254
1255        let mut serialized = wincode::serialize(&message).unwrap();
1256        // Set only one of the two priority-fee bits (bit 0).
1257        serialized[3] |= 0b1;
1258        assert!(deserialize(&serialized).is_err());
1259    }
1260}