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