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, AddressSet, 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    #[deprecated(
384        since = "4.4.0",
385        note = "Use `is_maybe_writable_with_reserved_addresses` instead"
386    )]
387    pub fn is_maybe_writable(
388        &self,
389        key_index: usize,
390        reserved_account_keys: Option<&HashSet<Address>>,
391    ) -> bool {
392        self.is_maybe_writable_with_reserved_addresses(key_index, reserved_account_keys)
393    }
394
395    /// Returns `true` if the account at the specified index was requested as
396    /// writable.
397    ///
398    ///
399    /// # Important
400    ///
401    /// Before loading addresses, we can't demote write locks properly so this should
402    /// not be used by the runtime. The `reserved_addresses` parameter is optional to
403    /// allow clients to approximate writability without requiring fetching the latest
404    /// set of protocol-reserved addresses.
405    ///
406    /// Program accounts are demoted from writable to readonly, unless the upgradeable
407    /// loader is present in which case they are left as writable since upgradeable
408    /// programs need to be writable for upgrades.
409    pub fn is_maybe_writable_with_reserved_addresses(
410        &self,
411        key_index: usize,
412        reserved_addresses: Option<&impl AddressSet>,
413    ) -> bool {
414        crate::is_maybe_writable(
415            key_index,
416            self.header,
417            &self.account_keys,
418            &self.instructions,
419            reserved_addresses,
420        )
421    }
422
423    pub fn demote_program_id(&self, i: usize) -> bool {
424        crate::is_program_id_write_demoted(i, &self.account_keys, &self.instructions)
425    }
426
427    /// Serialize this message with the V1 version prefix byte.
428    #[cfg(feature = "wincode")]
429    pub fn serialize(&self) -> Vec<u8> {
430        wincode::serialize(&(crate::v1::V1_PREFIX, self)).unwrap()
431    }
432
433    /// Calculate the serialized size of the message in bytes.
434    #[allow(clippy::arithmetic_side_effects)]
435    #[inline(always)]
436    pub fn size(&self) -> usize {
437        size_of::<MessageHeader>()                           // legacy header
438            + size_of::<TransactionConfigMask>()             // config mask
439            + size_of::<Hash>()                              // lifetime specifier
440            + size_of::<u8>()                                // number of instructions
441            + size_of::<u8>()                                // number of addresses
442            + self.account_keys.len() * size_of::<Address>() // addresses
443            + self.config.size()                             // config values
444            + self.instructions.len()
445                * (
446                    size_of::<u8>()
447                    + size_of::<u8>()
448                    + size_of::<u16>()
449                )                                            // instruction headers
450            + self
451                .instructions
452                .iter()
453                .map(|ix| {
454                    (ix.accounts.len() * size_of::<u8>())
455                    + ix.data.len()
456                })
457                .sum::<usize>() // instruction payloads
458    }
459
460    pub fn validate(&self) -> Result<(), MessageError> {
461        // `num_required_signatures` <= 12
462        if self.header.num_required_signatures > MAX_SIGNATURES {
463            return Err(MessageError::TooManySignatures);
464        }
465
466        // `num_instructions` <= 64
467        if self.instructions.len() > MAX_INSTRUCTIONS as usize {
468            return Err(MessageError::TooManyInstructions);
469        }
470
471        let num_account_keys = self.account_keys.len();
472
473        // `num_addresses` <= 64
474        if num_account_keys > MAX_ADDRESSES as usize {
475            return Err(MessageError::TooManyAddresses);
476        }
477
478        // `num_addresses` >= `num_required_signatures` + `num_readonly_unsigned_accounts`
479        let min_accounts = usize::from(self.header.num_required_signatures)
480            .saturating_add(usize::from(self.header.num_readonly_unsigned_accounts));
481
482        if num_account_keys < min_accounts {
483            return Err(MessageError::NotEnoughAddressesForSignatures);
484        }
485
486        // must have at least 1 RW fee-payer (`num_readonly_signed` < `num_required_signatures`)
487        if self.header.num_readonly_signed_accounts >= self.header.num_required_signatures {
488            return Err(MessageError::ZeroSigners);
489        }
490
491        // no duplicate addresses
492        let unique_keys: BTreeSet<_> = self.account_keys.iter().collect();
493        if unique_keys.len() != num_account_keys {
494            return Err(MessageError::DuplicateAddresses);
495        }
496
497        // The config mask is regenerated from the typed `TransactionConfig` on
498        // serialization, so a malformed mask cannot exist here. Invalid/unknown mask
499        // bits only occur in raw wire bytes and are rejected during deserialization.
500
501        // if specified, heap size must be a multiple of 1024 and within valid bounds
502        if let Some(heap_size) = self.config.heap_size {
503            if !heap_size.is_multiple_of(1024) {
504                return Err(MessageError::InvalidHeapSize);
505            }
506
507            if !(MIN_HEAP_SIZE..=MAX_HEAP_SIZE).contains(&heap_size) {
508                return Err(MessageError::InvalidHeapSize);
509            }
510        }
511
512        // instruction account indices must be < `num_addresses`
513        let max_account_index = num_account_keys
514            .checked_sub(1)
515            .ok_or(MessageError::NotEnoughAccountKeys)?;
516
517        for instruction in &self.instructions {
518            // program id must be in static accounts
519            if usize::from(instruction.program_id_index) > max_account_index {
520                return Err(MessageError::InvalidInstructionAccountIndex);
521            }
522
523            // program cannot be fee payer
524            if instruction.program_id_index == 0 {
525                return Err(MessageError::InvalidInstructionAccountIndex);
526            }
527
528            // instruction accounts count must fit in u8
529            if instruction.accounts.len() > u8::MAX as usize {
530                return Err(MessageError::InstructionAccountsTooLarge);
531            }
532
533            // instruction data length must fit in u16
534            if instruction.data.len() > u16::MAX as usize {
535                return Err(MessageError::InstructionDataTooLarge);
536            }
537
538            // all account indices must be valid
539            for &account_index in &instruction.accounts {
540                if usize::from(account_index) > max_account_index {
541                    return Err(MessageError::InvalidInstructionAccountIndex);
542                }
543            }
544        }
545
546        Ok(())
547    }
548}
549
550impl Sanitize for Message {
551    fn sanitize(&self) -> Result<(), SanitizeError> {
552        Ok(self.validate()?)
553    }
554}
555
556#[cfg(feature = "wincode")]
557unsafe impl<C: ConfigCore> SchemaWrite<C> for Message {
558    type Src = Self;
559
560    #[inline(always)]
561    fn size_of(src: &Self::Src) -> WriteResult<usize> {
562        Ok(src.size())
563    }
564
565    fn write(mut writer: impl Writer, src: &Self::Src) -> WriteResult<()> {
566        // SAFETY: `Message::size()` yields the exact number of bytes to be written.
567        let mut writer = unsafe { writer.as_trusted_for(src.size()) }?;
568        writer.write(&[
569            src.header.num_required_signatures,
570            src.header.num_readonly_signed_accounts,
571            src.header.num_readonly_unsigned_accounts,
572        ])?;
573        let mask = TransactionConfigMask::from(&src.config).0.to_le_bytes();
574        writer.write(&mask)?;
575        writer.write(src.lifetime_specifier.as_bytes())?;
576        writer.write(&[src.instructions.len() as u8, src.account_keys.len() as u8])?;
577
578        // SAFETY: `Address` is `#[repr(transparent)]` over `[u8; 32]`, so it is safe to
579        // treat as bytes.
580        #[expect(clippy::arithmetic_side_effects)]
581        let account_keys = unsafe {
582            from_raw_parts(
583                src.account_keys.as_ptr().cast::<u8>(),
584                src.account_keys.len() * size_of::<Address>(),
585            )
586        };
587        writer.write(account_keys)?;
588
589        if let Some(value) = src.config.priority_fee {
590            writer.write(&value.to_le_bytes())?;
591        }
592        if let Some(value) = src.config.compute_unit_limit {
593            writer.write(&value.to_le_bytes())?;
594        }
595        if let Some(value) = src.config.loaded_accounts_data_size_limit {
596            writer.write(&value.to_le_bytes())?;
597        }
598        if let Some(value) = src.config.heap_size {
599            writer.write(&value.to_le_bytes())?;
600        }
601
602        for ix in &src.instructions {
603            writer.write(&[ix.program_id_index, ix.accounts.len() as u8])?;
604            writer.write(&(ix.data.len() as u16).to_le_bytes())?;
605        }
606
607        for ix in &src.instructions {
608            writer.write(&ix.accounts)?;
609            writer.write(&ix.data)?;
610        }
611
612        writer.finish()?;
613
614        Ok(())
615    }
616}
617
618/// Serialize the message.
619#[cfg(feature = "wincode")]
620#[inline]
621#[deprecated(since = "4.1.2", note = "use `Message::serialize` instead")]
622pub fn serialize(message: &Message) -> Vec<u8> {
623    wincode::serialize(message).unwrap()
624}
625
626#[cfg(feature = "wincode")]
627unsafe impl<'de, C: Config> SchemaRead<'de, C> for Message {
628    type Dst = Message;
629
630    #[expect(clippy::arithmetic_side_effects)]
631    fn read(mut reader: impl Reader<'de>, dst: &mut MaybeUninit<Self::Dst>) -> ReadResult<()> {
632        let (header, lifetime_specifier, config_mask, num_instructions, num_addresses) = {
633            // SAFETY: the following reads consume exactly `FIXED_HEADER_SIZE` bytes.
634            // - MessageHeader (3 bytes)
635            // - TransactionConfigMask (4 bytes)
636            // - Hash (32 bytes)
637            // - num_instructions (1 byte)
638            // - num_addresses (1 byte)
639            let mut reader = unsafe { reader.as_trusted_for(FIXED_HEADER_SIZE)? };
640            let header = <MessageHeader as SchemaRead<C>>::get(reader.by_ref())?;
641            let config_mask = TransactionConfigMask(u32::from_le_bytes(reader.take_array()?));
642            let lifetime_specifier = <Hash as SchemaRead<C>>::get(reader.by_ref())?;
643            let num_instructions = reader.take_byte()? as usize;
644            let num_addresses = reader.take_byte()? as usize;
645            (
646                header,
647                lifetime_specifier,
648                config_mask,
649                num_instructions,
650                num_addresses,
651            )
652        };
653
654        // Reject masks we cannot round-trip. Unknown bits would be silently dropped
655        // on re-serialization (this message is signed, so that invalidates the
656        // signature), and a partial priority-fee bit pair is malformed. Support for
657        // new bits is added by a newer library release that promotes them to known.
658        if config_mask.has_unknown_bits() || config_mask.has_invalid_priority_fee_bits() {
659            return Err(wincode::error::invalid_value(
660                "invalid transaction config mask",
661            ));
662        }
663
664        <C::LengthEncoding as SeqLen<C>>::prealloc_check::<Address>(num_addresses)?;
665        let account_keys = <Vec<Address> as SchemaReadContext<C, context::Len>>::get_with_context(
666            context::Len(num_addresses),
667            reader.by_ref(),
668        )?;
669
670        let mut config = TransactionConfig::empty();
671        if config_mask.has_priority_fee() {
672            config.priority_fee = Some(u64::from_le_bytes(reader.take_array()?));
673        }
674        if config_mask.has_compute_unit_limit() {
675            config.compute_unit_limit = Some(u32::from_le_bytes(reader.take_array()?));
676        }
677        if config_mask.has_loaded_accounts_data_size() {
678            config.loaded_accounts_data_size_limit = Some(u32::from_le_bytes(reader.take_array()?));
679        }
680        if config_mask.has_heap_size() {
681            config.heap_size = Some(u32::from_le_bytes(reader.take_array()?));
682        }
683
684        // SAFETY:
685        // - `take_borrowed(num_instructions * size_of::<WireInstructionHeader>())` returns
686        //   exactly the requested number of bytes, or errors.
687        // - `take_borrowed` returns a stable borrow from the backing buffer, so the
688        //   resulting slice remains valid across subsequent reader operations.
689        // - The `const` block above pins `WireInstructionHeader`'s layout to a packed
690        //   4 bytes with fields in wire order, so the reinterpretation is exact.
691        let instruction_headers = unsafe {
692            from_raw_parts(
693                reader
694                    .take_borrowed(num_instructions * size_of::<WireInstructionHeader>())?
695                    .as_ptr() as *const WireInstructionHeader,
696                num_instructions,
697            )
698        };
699        let mut instructions = Vec::with_capacity(num_instructions);
700        for header in instruction_headers {
701            let program_id_index = header.program_id_index;
702            let num_accounts = header.num_accounts as usize;
703            let data_len = u16::from_le_bytes(header.data_len) as usize;
704
705            <C::LengthEncoding as SeqLen<C>>::prealloc_check::<u8>(num_accounts)?;
706            let accounts = <Vec<u8> as SchemaReadContext<C, context::Len>>::get_with_context(
707                context::Len(num_accounts),
708                reader.by_ref(),
709            )?;
710            <C::LengthEncoding as SeqLen<C>>::prealloc_check::<u8>(data_len)?;
711            let data = <Vec<u8> as SchemaReadContext<C, context::Len>>::get_with_context(
712                context::Len(data_len),
713                reader.by_ref(),
714            )?;
715
716            instructions.push(CompiledInstruction {
717                program_id_index,
718                accounts,
719                data,
720            });
721        }
722
723        dst.write(Message {
724            header,
725            lifetime_specifier,
726            config,
727            account_keys,
728            instructions,
729        });
730
731        Ok(())
732    }
733}
734
735/// Deserialize the message from the provided input buffer, returning the message and
736/// the number of bytes read.
737#[cfg(feature = "wincode")]
738#[inline]
739pub fn deserialize(input: &[u8]) -> wincode::ReadResult<Message> {
740    wincode::deserialize(input)
741}
742
743#[cfg(test)]
744mod tests {
745    use {super::*, alloc::vec, solana_sdk_ids::bpf_loader_upgradeable};
746
747    /// Builder for constructing V1 messages.
748    ///
749    /// This is used in tests to simplify message construction and validation. For
750    /// client code, users should construct messages using `try_compile` or
751    /// `try_compile_with_config`.
752    #[derive(Debug, Clone, Default)]
753    pub struct MessageBuilder {
754        header: MessageHeader,
755        config: TransactionConfig,
756        lifetime_specifier: Option<Hash>,
757        account_keys: Vec<Address>,
758        instructions: Vec<CompiledInstruction>,
759    }
760
761    impl MessageBuilder {
762        pub fn new() -> Self {
763            Self::default()
764        }
765
766        #[must_use]
767        pub fn required_signatures(mut self, count: u8) -> Self {
768            self.header.num_required_signatures = count;
769            self
770        }
771
772        #[must_use]
773        pub fn readonly_signed_accounts(mut self, count: u8) -> Self {
774            self.header.num_readonly_signed_accounts = count;
775            self
776        }
777
778        #[must_use]
779        pub fn readonly_unsigned_accounts(mut self, count: u8) -> Self {
780            self.header.num_readonly_unsigned_accounts = count;
781            self
782        }
783
784        #[must_use]
785        pub fn lifetime_specifier(mut self, hash: Hash) -> Self {
786            self.lifetime_specifier = Some(hash);
787            self
788        }
789
790        #[must_use]
791        pub fn config(mut self, config: TransactionConfig) -> Self {
792            self.config = config;
793            self
794        }
795
796        #[must_use]
797        pub fn priority_fee(mut self, fee: u64) -> Self {
798            self.config.priority_fee = Some(fee);
799            self
800        }
801
802        #[must_use]
803        pub fn compute_unit_limit(mut self, limit: u32) -> Self {
804            self.config.compute_unit_limit = Some(limit);
805            self
806        }
807
808        #[must_use]
809        pub fn loaded_accounts_data_size_limit(mut self, limit: u32) -> Self {
810            self.config.loaded_accounts_data_size_limit = Some(limit);
811            self
812        }
813
814        #[must_use]
815        pub fn heap_size(mut self, size: u32) -> Self {
816            self.config.heap_size = Some(size);
817            self
818        }
819
820        #[must_use]
821        pub fn account(mut self, key: Address) -> Self {
822            self.account_keys.push(key);
823            self
824        }
825
826        #[must_use]
827        pub fn accounts(mut self, keys: Vec<Address>) -> Self {
828            self.account_keys = keys;
829            self
830        }
831
832        #[must_use]
833        pub fn instruction(mut self, instruction: CompiledInstruction) -> Self {
834            self.instructions.push(instruction);
835            self
836        }
837
838        #[must_use]
839        pub fn instructions(mut self, instructions: Vec<CompiledInstruction>) -> Self {
840            self.instructions = instructions;
841            self
842        }
843
844        /// Build the message, validating all constraints.
845        pub fn build(self) -> Result<Message, MessageError> {
846            let lifetime_specifier = self
847                .lifetime_specifier
848                .ok_or(MessageError::MissingLifetimeSpecifier)?;
849
850            let message = Message::new(
851                self.header,
852                self.config,
853                lifetime_specifier,
854                self.account_keys,
855                self.instructions,
856            );
857
858            message.validate()?;
859
860            Ok(message)
861        }
862    }
863
864    fn create_test_message() -> Message {
865        MessageBuilder::new()
866            .required_signatures(1)
867            .readonly_unsigned_accounts(1)
868            .lifetime_specifier(Hash::new_unique())
869            .accounts(vec![
870                Address::new_unique(), // fee payer
871                Address::new_unique(), // program
872                Address::new_unique(), // readonly account
873            ])
874            .compute_unit_limit(200_000)
875            .instruction(CompiledInstruction {
876                program_id_index: 1,
877                accounts: vec![0, 2],
878                data: vec![1, 2, 3, 4],
879            })
880            .build()
881            .unwrap()
882    }
883
884    #[test]
885    fn fee_payer_returns_first_account() {
886        let fee_payer = Address::new_unique();
887        let message = MessageBuilder::new()
888            .required_signatures(1)
889            .lifetime_specifier(Hash::new_unique())
890            .accounts(vec![fee_payer, Address::new_unique()])
891            .build()
892            .unwrap();
893
894        assert_eq!(message.fee_payer(), Some(&fee_payer));
895    }
896
897    #[test]
898    fn fee_payer_returns_none_for_empty_accounts() {
899        // Direct construction to bypass builder validation
900        let message = Message::new(
901            MessageHeader::default(),
902            TransactionConfig::default(),
903            Hash::new_unique(),
904            vec![],
905            vec![],
906        );
907
908        assert_eq!(message.fee_payer(), None);
909    }
910
911    #[test]
912    fn is_signer_checks_signature_requirement() {
913        let message = create_test_message();
914        assert!(message.is_signer(0)); // Fee payer is signer
915        assert!(!message.is_signer(1)); // Program is not signer
916        assert!(!message.is_signer(2)); // Readonly account is not signer
917    }
918
919    #[test]
920    fn is_signer_writable_identifies_writable_signers() {
921        let message = MessageBuilder::new()
922            .required_signatures(3)
923            .readonly_signed_accounts(1) // Last signer is readonly
924            .lifetime_specifier(Hash::new_unique())
925            .accounts(vec![
926                Address::new_unique(), // 0: writable signer
927                Address::new_unique(), // 1: writable signer
928                Address::new_unique(), // 2: readonly signer
929                Address::new_unique(), // 3: non-signer
930            ])
931            .build()
932            .unwrap();
933
934        // Writable signers
935        assert!(message.is_signer_writable(0));
936        assert!(message.is_signer_writable(1));
937        // Readonly signer
938        assert!(!message.is_signer_writable(2));
939        // Non-signers
940        assert!(!message.is_signer_writable(3));
941        assert!(!message.is_signer_writable(100));
942    }
943
944    #[test]
945    fn is_signer_writable_all_writable_when_no_readonly() {
946        let message = MessageBuilder::new()
947            .required_signatures(2)
948            .readonly_signed_accounts(0) // All signers are writable
949            .lifetime_specifier(Hash::new_unique())
950            .accounts(vec![
951                Address::new_unique(),
952                Address::new_unique(),
953                Address::new_unique(),
954            ])
955            .build()
956            .unwrap();
957
958        assert!(message.is_signer_writable(0));
959        assert!(message.is_signer_writable(1));
960        assert!(!message.is_signer_writable(2)); // Not a signer
961    }
962
963    #[test]
964    fn is_key_called_as_program_detects_program_indices() {
965        let message = create_test_message();
966        // program_id_index = 1 in create_test_message
967        assert!(message.is_key_called_as_program(1));
968        assert!(!message.is_key_called_as_program(0));
969        assert!(!message.is_key_called_as_program(2));
970        // Index > u8::MAX can't match any program_id_index
971        assert!(!message.is_key_called_as_program(256));
972        assert!(!message.is_key_called_as_program(10_000));
973    }
974
975    #[test]
976    fn is_upgradeable_loader_present_detects_loader() {
977        let message = create_test_message();
978        assert!(!message.is_upgradeable_loader_present());
979
980        let mut message_with_loader = create_test_message();
981        message_with_loader
982            .account_keys
983            .push(bpf_loader_upgradeable::id());
984        assert!(message_with_loader.is_upgradeable_loader_present());
985    }
986
987    #[test]
988    fn is_writable_index_respects_header_layout() {
989        let message = create_test_message();
990        // Account layout: [writable signer (fee payer), writable unsigned (program), readonly unsigned]
991        assert!(message.is_writable_index(0)); // Fee payer is writable
992        assert!(message.is_writable_index(1)); // Program position is writable unsigned
993        assert!(!message.is_writable_index(2)); // Last account is readonly
994    }
995
996    #[test]
997    fn is_writable_index_handles_mixed_signer_permissions() {
998        let mut message = create_test_message();
999        // 2 signers: first writable, second readonly
1000        message.header.num_required_signatures = 2;
1001        message.header.num_readonly_signed_accounts = 1;
1002        message.header.num_readonly_unsigned_accounts = 1;
1003        message.account_keys = vec![
1004            Address::new_unique(), // writable signer
1005            Address::new_unique(), // readonly signer
1006            Address::new_unique(), // readonly unsigned
1007        ];
1008        message.instructions[0].program_id_index = 2;
1009        message.instructions[0].accounts = vec![0, 1];
1010
1011        assert!(message.sanitize().is_ok());
1012        assert!(message.is_writable_index(0)); // writable signer
1013        assert!(!message.is_writable_index(1)); // readonly signer
1014        assert!(!message.is_writable_index(2)); // readonly unsigned
1015        assert!(!message.is_writable_index(999)); // out of bounds
1016    }
1017
1018    #[test]
1019    #[allow(deprecated)]
1020    fn is_maybe_writable_returns_false_for_readonly_index() {
1021        let message = create_test_message();
1022        // Index 2 is readonly unsigned
1023        assert!(!message.is_writable_index(2));
1024        assert!(!message.is_maybe_writable(2, None));
1025        // Even with empty reserved set
1026        assert!(!message.is_maybe_writable(2, Some(&HashSet::new())));
1027    }
1028
1029    #[test]
1030    #[allow(deprecated)]
1031    fn is_maybe_writable_demotes_reserved_accounts() {
1032        let message = create_test_message();
1033        let reserved = HashSet::from([message.account_keys[0]]);
1034        // Fee payer is writable by index, but reserved → demoted
1035        assert!(message.is_writable_index(0));
1036        assert!(!message.is_maybe_writable(0, Some(&reserved)));
1037    }
1038
1039    #[test]
1040    #[allow(deprecated)]
1041    fn is_maybe_writable_demotes_programs_without_upgradeable_loader() {
1042        let message = create_test_message();
1043        // Index 1 is writable unsigned, called as program, no upgradeable loader
1044        assert!(message.is_writable_index(1));
1045        assert!(message.is_key_called_as_program(1));
1046        assert!(!message.is_upgradeable_loader_present());
1047        assert!(!message.is_maybe_writable(1, None));
1048    }
1049
1050    #[test]
1051    #[allow(deprecated)]
1052    fn is_maybe_writable_preserves_programs_with_upgradeable_loader() {
1053        let mut message = create_test_message();
1054        // Add upgradeable loader to account keys
1055        message.account_keys.push(bpf_loader_upgradeable::id());
1056
1057        assert!(message.sanitize().is_ok());
1058        assert!(message.is_writable_index(1));
1059        assert!(message.is_key_called_as_program(1));
1060        assert!(message.is_upgradeable_loader_present());
1061        // Program not demoted because upgradeable loader is present
1062        assert!(message.is_maybe_writable(1, None));
1063    }
1064
1065    #[test]
1066    fn sanitize_accepts_valid_message() {
1067        let message = create_test_message();
1068        assert!(message.sanitize().is_ok());
1069    }
1070
1071    #[test]
1072    fn sanitize_rejects_zero_signers() {
1073        let mut message = create_test_message();
1074        message.header.num_required_signatures = 0;
1075        assert_eq!(message.sanitize(), Err(SanitizeError::InvalidValue));
1076    }
1077
1078    #[test]
1079    fn sanitize_rejects_over_12_signatures() {
1080        let mut message = create_test_message();
1081        message.header.num_required_signatures = MAX_SIGNATURES + 1;
1082        message.account_keys = (0..MAX_SIGNATURES + 1)
1083            .map(|_| Address::new_unique())
1084            .collect();
1085        assert_eq!(message.sanitize(), Err(SanitizeError::IndexOutOfBounds));
1086    }
1087
1088    #[test]
1089    fn sanitize_rejects_over_64_addresses() {
1090        let mut message = create_test_message();
1091        message.account_keys = (0..65).map(|_| Address::new_unique()).collect();
1092        assert_eq!(message.sanitize(), Err(SanitizeError::IndexOutOfBounds));
1093    }
1094
1095    #[test]
1096    fn sanitize_rejects_over_64_instructions() {
1097        let mut message = create_test_message();
1098        message.instructions = (0..65) // exceeds 64 max
1099            .map(|_| CompiledInstruction {
1100                program_id_index: 1,
1101                accounts: vec![0],
1102                data: vec![],
1103            })
1104            .collect();
1105        assert_eq!(message.sanitize(), Err(SanitizeError::IndexOutOfBounds));
1106    }
1107
1108    #[test]
1109    fn sanitize_rejects_insufficient_accounts_for_header() {
1110        let mut message = create_test_message();
1111        // min_accounts = num_required_signatures + num_readonly_unsigned_accounts
1112        // Set readonly_unsigned high so min_accounts > account_keys.len()
1113        message.header.num_readonly_unsigned_accounts = 10;
1114        assert_eq!(message.sanitize(), Err(SanitizeError::IndexOutOfBounds));
1115    }
1116
1117    #[test]
1118    fn sanitize_rejects_all_signers_readonly() {
1119        let mut message = create_test_message();
1120        message.header.num_readonly_signed_accounts = 1; // All signers readonly
1121        assert_eq!(message.sanitize(), Err(SanitizeError::InvalidValue));
1122    }
1123
1124    #[test]
1125    fn sanitize_rejects_duplicate_addresses() {
1126        let mut message = create_test_message();
1127        let dup = message.account_keys[0];
1128        message.account_keys[1] = dup;
1129        assert_eq!(message.sanitize(), Err(SanitizeError::InvalidValue));
1130    }
1131
1132    #[test]
1133    fn sanitize_rejects_unaligned_heap_size() {
1134        let mut message = create_test_message();
1135        message.config.heap_size = Some(1025); // Not a multiple of 1024
1136        assert_eq!(message.sanitize(), Err(SanitizeError::InvalidValue));
1137    }
1138
1139    #[test]
1140    fn sanitize_rejects_heap_size_below_minimum() {
1141        let mut message = create_test_message();
1142        message.config.heap_size = Some(MIN_HEAP_SIZE - 1);
1143        assert_eq!(message.sanitize(), Err(SanitizeError::InvalidValue));
1144    }
1145
1146    #[test]
1147    fn sanitize_rejects_heap_size_above_maximum() {
1148        let mut message = create_test_message();
1149        message.config.heap_size = Some(MAX_HEAP_SIZE + 1);
1150        assert_eq!(message.sanitize(), Err(SanitizeError::InvalidValue));
1151    }
1152
1153    #[test]
1154    fn sanitize_accepts_minimum_heap_size() {
1155        let mut message = create_test_message();
1156        message.config.heap_size = Some(MIN_HEAP_SIZE);
1157        assert!(message.sanitize().is_ok());
1158    }
1159
1160    #[test]
1161    fn sanitize_accepts_maximum_heap_size() {
1162        let mut message = create_test_message();
1163        message.config.heap_size = Some(MAX_HEAP_SIZE);
1164        assert!(message.sanitize().is_ok());
1165    }
1166
1167    #[test]
1168    fn sanitize_accepts_aligned_heap_size() {
1169        let mut message = create_test_message();
1170        message.config.heap_size = Some(65536); // 64KB, valid
1171        assert!(message.sanitize().is_ok());
1172    }
1173
1174    #[test]
1175    fn sanitize_rejects_invalid_program_id_index() {
1176        let mut message = create_test_message();
1177        message.instructions[0].program_id_index = 99;
1178        assert_eq!(message.sanitize(), Err(SanitizeError::IndexOutOfBounds));
1179    }
1180
1181    #[test]
1182    fn sanitize_rejects_fee_payer_as_program() {
1183        let mut message = create_test_message();
1184        message.instructions[0].program_id_index = 0;
1185        assert_eq!(message.sanitize(), Err(SanitizeError::IndexOutOfBounds));
1186    }
1187
1188    #[test]
1189    fn sanitize_rejects_instruction_with_too_many_accounts() {
1190        let mut message = create_test_message();
1191        message.instructions[0].accounts = vec![0u8; (u8::MAX as usize) + 1];
1192        assert_eq!(message.sanitize(), Err(SanitizeError::InvalidValue));
1193    }
1194
1195    #[test]
1196    fn sanitize_rejects_invalid_instruction_account_index() {
1197        let mut message = create_test_message();
1198        message.instructions[0].accounts = vec![0, 99]; // 99 is out of bounds
1199        assert_eq!(message.sanitize(), Err(SanitizeError::IndexOutOfBounds));
1200    }
1201
1202    #[test]
1203    fn sanitize_accepts_64_addresses() {
1204        let mut message = create_test_message();
1205        message.account_keys = (0..MAX_ADDRESSES).map(|_| Address::new_unique()).collect();
1206        message.header.num_required_signatures = 1;
1207        message.header.num_readonly_signed_accounts = 0;
1208        message.header.num_readonly_unsigned_accounts = 1;
1209        message.instructions[0].program_id_index = 1;
1210        message.instructions[0].accounts = vec![0, 2];
1211        assert!(message.sanitize().is_ok());
1212    }
1213
1214    #[test]
1215    fn sanitize_accepts_64_instructions() {
1216        let mut message = create_test_message();
1217        message.instructions = (0..MAX_INSTRUCTIONS)
1218            .map(|_| CompiledInstruction {
1219                program_id_index: 1,
1220                accounts: vec![0, 2],
1221                data: vec![1, 2, 3],
1222            })
1223            .collect();
1224        assert!(message.sanitize().is_ok());
1225    }
1226
1227    #[test]
1228    fn size_matches_serialized_length() {
1229        let test_cases = [
1230            // Minimal message
1231            MessageBuilder::new()
1232                .required_signatures(1)
1233                .lifetime_specifier(Hash::new_unique())
1234                .accounts(vec![Address::new_unique()])
1235                .build()
1236                .unwrap(),
1237            // With config
1238            MessageBuilder::new()
1239                .required_signatures(1)
1240                .lifetime_specifier(Hash::new_unique())
1241                .accounts(vec![Address::new_unique(), Address::new_unique()])
1242                .priority_fee(1000)
1243                .compute_unit_limit(200_000)
1244                .instruction(CompiledInstruction {
1245                    program_id_index: 1,
1246                    accounts: vec![0],
1247                    data: vec![1, 2, 3, 4],
1248                })
1249                .build()
1250                .unwrap(),
1251            // Multiple instructions with varying data
1252            MessageBuilder::new()
1253                .required_signatures(2)
1254                .readonly_signed_accounts(1)
1255                .readonly_unsigned_accounts(1)
1256                .lifetime_specifier(Hash::new_unique())
1257                .accounts(vec![
1258                    Address::new_unique(),
1259                    Address::new_unique(),
1260                    Address::new_unique(),
1261                    Address::new_unique(),
1262                ])
1263                .heap_size(65536)
1264                .instructions(vec![
1265                    CompiledInstruction {
1266                        program_id_index: 2,
1267                        accounts: vec![0, 1],
1268                        data: vec![],
1269                    },
1270                    CompiledInstruction {
1271                        program_id_index: 3,
1272                        accounts: vec![0, 1, 2],
1273                        data: vec![0xAA; 100],
1274                    },
1275                ])
1276                .build()
1277                .unwrap(),
1278        ];
1279
1280        for message in &test_cases {
1281            assert_eq!(message.size(), wincode::serialize(message).unwrap().len());
1282        }
1283    }
1284
1285    #[test]
1286    fn byte_layout_without_config() {
1287        let fee_payer = Address::new_from_array([1u8; 32]);
1288        let program = Address::new_from_array([2u8; 32]);
1289        let blockhash = Hash::new_from_array([0xAB; 32]);
1290
1291        let message = MessageBuilder::new()
1292            .required_signatures(1)
1293            .lifetime_specifier(blockhash)
1294            .accounts(vec![fee_payer, program])
1295            .instruction(CompiledInstruction {
1296                program_id_index: 1,
1297                accounts: vec![0],
1298                data: vec![0xDE, 0xAD],
1299            })
1300            .build()
1301            .unwrap();
1302
1303        let bytes = wincode::serialize(&message).unwrap();
1304
1305        // Build expected bytes manually per SIMD-0385
1306        //
1307        // num_required_signatures
1308        // num_readonly_signed_accounts
1309        // num_readonly_unsigned_accounts
1310        let mut expected = vec![1, 0, 0];
1311        expected.extend_from_slice(&0u32.to_le_bytes()); // ConfigMask = 0
1312        expected.extend_from_slice(&[0xAB; 32]); // LifetimeSpecifier
1313        expected.push(1); // NumInstructions
1314        expected.push(2); // NumAddresses
1315        expected.extend_from_slice(&[1u8; 32]); // fee_payer
1316        expected.extend_from_slice(&[2u8; 32]); // program
1317                                                // ConfigValues: none
1318        expected.push(1); // program_id_index
1319        expected.push(1); // num_accounts
1320        expected.extend_from_slice(&2u16.to_le_bytes()); // data_len
1321        expected.push(0); // account index 0
1322        expected.extend_from_slice(&[0xDE, 0xAD]); // data
1323        assert_eq!(bytes, expected);
1324    }
1325
1326    #[test]
1327    fn byte_layout_with_config() {
1328        let fee_payer = Address::new_from_array([1u8; 32]);
1329        let program = Address::new_from_array([2u8; 32]);
1330        let blockhash = Hash::new_from_array([0xBB; 32]);
1331
1332        let message = MessageBuilder::new()
1333            .required_signatures(1)
1334            .lifetime_specifier(blockhash)
1335            .accounts(vec![fee_payer, program])
1336            .priority_fee(0x0102030405060708u64)
1337            .compute_unit_limit(0x11223344u32)
1338            .instruction(CompiledInstruction {
1339                program_id_index: 1,
1340                accounts: vec![],
1341                data: vec![],
1342            })
1343            .build()
1344            .unwrap();
1345
1346        let bytes = wincode::serialize(&message).unwrap();
1347
1348        let mut expected = vec![1, 0, 0];
1349        // ConfigMask: priority fee (bits 0,1) + CU limit (bit 2) = 0b111 = 7
1350        expected.extend_from_slice(&7u32.to_le_bytes());
1351        expected.extend_from_slice(&[0xBB; 32]);
1352        expected.push(1);
1353        expected.push(2);
1354        expected.extend_from_slice(&[1u8; 32]);
1355        expected.extend_from_slice(&[2u8; 32]);
1356        // Priority fee as u64 LE
1357        expected.extend_from_slice(&0x0102030405060708u64.to_le_bytes());
1358        // Compute unit limit as u32 LE
1359        expected.extend_from_slice(&0x11223344u32.to_le_bytes());
1360        expected.push(1); // program_id_index
1361        expected.push(0); // num_accounts
1362        expected.extend_from_slice(&0u16.to_le_bytes()); // data_len
1363
1364        assert_eq!(bytes, expected);
1365    }
1366
1367    #[test]
1368    fn roundtrip_preserves_all_config_fields() {
1369        let message = MessageBuilder::new()
1370            .required_signatures(1)
1371            .lifetime_specifier(Hash::new_unique())
1372            .accounts(vec![Address::new_unique(), Address::new_unique()])
1373            .priority_fee(1000)
1374            .compute_unit_limit(200_000)
1375            .loaded_accounts_data_size_limit(1_000_000)
1376            .heap_size(65536)
1377            .instruction(CompiledInstruction {
1378                program_id_index: 1,
1379                accounts: vec![0],
1380                data: vec![],
1381            })
1382            .build()
1383            .unwrap();
1384
1385        let serialized = wincode::serialize(&message).unwrap();
1386        let deserialized = deserialize(&serialized).unwrap();
1387        assert_eq!(message.config, deserialized.config);
1388    }
1389
1390    #[test]
1391    fn deserialize_rejects_unknown_config_mask_bits() {
1392        let message = MessageBuilder::default()
1393            .required_signatures(1)
1394            .lifetime_specifier(Hash::new_unique())
1395            .accounts(vec![Address::new_unique(), Address::new_unique()])
1396            .instruction(CompiledInstruction {
1397                program_id_index: 1,
1398                accounts: vec![0],
1399                data: vec![],
1400            })
1401            .build()
1402            .unwrap();
1403
1404        let mut serialized = wincode::serialize(&message).unwrap();
1405        // Round-trips cleanly with a zero (all-known) mask.
1406        assert!(deserialize(&serialized).is_ok());
1407
1408        // The config mask is a u32 (little-endian) immediately after the 3-byte
1409        // header. Set the first bit past KNOWN_BITS; it must be rejected rather than
1410        // silently dropped, regardless of how many bits KNOWN_BITS covers.
1411        let unknown_bit = 1u32 << TransactionConfigMask::KNOWN_BITS.trailing_ones();
1412        let mask = u32::from_le_bytes(serialized[3..7].try_into().unwrap()) | unknown_bit;
1413        serialized[3..7].copy_from_slice(&mask.to_le_bytes());
1414        assert!(deserialize(&serialized).is_err());
1415    }
1416
1417    #[test]
1418    fn deserialize_rejects_partial_priority_fee_bits() {
1419        let message = MessageBuilder::default()
1420            .required_signatures(1)
1421            .lifetime_specifier(Hash::new_unique())
1422            .accounts(vec![Address::new_unique(), Address::new_unique()])
1423            .instruction(CompiledInstruction {
1424                program_id_index: 1,
1425                accounts: vec![0],
1426                data: vec![],
1427            })
1428            .build()
1429            .unwrap();
1430
1431        let mut serialized = wincode::serialize(&message).unwrap();
1432        // Set only one of the two priority-fee bits (bit 0).
1433        serialized[3] |= 0b1;
1434        assert!(deserialize(&serialized).is_err());
1435    }
1436}