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