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