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