Skip to main content

solana_message/
legacy.rs

1//! The original and current Solana message format.
2//!
3//! This crate defines two versions of `Message` in their own modules:
4//! [`legacy`] and [`v0`]. `legacy` is the current version as of Solana 1.10.0.
5//! `v0` is a [future message format] that encodes more account keys into a
6//! transaction than the legacy format.
7//!
8//! [`legacy`]: crate::legacy
9//! [`v0`]: crate::v0
10//! [future message format]: https://docs.solanalabs.com/proposals/versioned-transactions
11
12#![allow(clippy::arithmetic_side_effects)]
13
14#[cfg(feature = "serde")]
15use serde_derive::{Deserialize, Serialize};
16#[cfg(feature = "frozen-abi")]
17use solana_frozen_abi_macro::{frozen_abi, AbiExample, StableAbi, StableAbiSample};
18use {
19    crate::{
20        compiled_instruction::CompiledInstruction, compiled_keys::CompiledKeys,
21        inline_nonce::advance_nonce_account_instruction, AddressSet, MessageHeader,
22    },
23    alloc::vec::Vec,
24    core::convert::TryFrom,
25    solana_address::Address,
26    solana_hash::Hash,
27    solana_instruction::Instruction,
28    solana_sanitize::{Sanitize, SanitizeError},
29};
30#[cfg(feature = "wincode")]
31use {
32    core::mem::MaybeUninit,
33    solana_short_vec::ShortU16,
34    wincode::{
35        config::Config, containers, io::Reader, ReadResult, SchemaRead, SchemaReadContext,
36        SchemaWrite,
37    },
38};
39
40fn position(keys: &[Address], key: &Address) -> u8 {
41    keys.iter().position(|k| k == key).unwrap() as u8
42}
43
44fn compile_instruction(ix: &Instruction, keys: &[Address]) -> CompiledInstruction {
45    let accounts: Vec<_> = ix
46        .accounts
47        .iter()
48        .map(|account_meta| position(keys, &account_meta.pubkey))
49        .collect();
50
51    CompiledInstruction {
52        program_id_index: position(keys, &ix.program_id),
53        data: ix.data.clone(),
54        accounts,
55    }
56}
57
58fn compile_instructions(ixs: &[Instruction], keys: &[Address]) -> Vec<CompiledInstruction> {
59    ixs.iter().map(|ix| compile_instruction(ix, keys)).collect()
60}
61
62/// Samples a `MessageHeader` whose `num_required_signatures` cannot be mistaken
63/// for a version prefix.
64///
65/// The legacy message format has no version prefix, so its first serialized byte
66/// (the header's `num_required_signatures`) must stay below
67/// `MESSAGE_VERSION_PREFIX`, otherwise it would decode as a versioned message.
68/// Masking the prefix bit keeps a sampled legacy message self-consistent across
69/// a serialize/deserialize roundtrip.
70#[cfg(feature = "frozen-abi")]
71fn sample_legacy_header(
72    rng: &mut (impl solana_frozen_abi::rand::RngCore + ?Sized),
73) -> MessageHeader {
74    use solana_frozen_abi::stable_abi::StableAbi;
75
76    let mut header = MessageHeader::random(rng);
77    header.num_required_signatures &= !crate::MESSAGE_VERSION_PREFIX;
78    header
79}
80
81/// A Solana transaction message (legacy).
82///
83/// See the crate documentation for further description.
84///
85/// Some constructors accept an optional `payer`, the account responsible for
86/// paying the cost of executing a transaction. In most cases, callers should
87/// specify the payer explicitly in these constructors. In some cases though,
88/// the caller is not _required_ to specify the payer, but is still allowed to:
89/// in the `Message` structure, the first account is always the fee-payer, so if
90/// the caller has knowledge that the first account of the constructed
91/// transaction's `Message` is both a signer and the expected fee-payer, then
92/// redundantly specifying the fee-payer is not strictly required.
93// NOTE: Serialization-related changes must be paired with the custom serialization
94// for versioned messages in the `RemainingLegacyMessage` struct.
95#[cfg_attr(
96    feature = "frozen-abi",
97    frozen_abi(digest = "GXpvLNiMCnjnZpQEDKpc2NBpsqmRnAX7ZTCy9JmvG8Dg"),
98    derive(AbiExample, StableAbi, StableAbiSample)
99)]
100#[cfg_attr(
101    feature = "serde",
102    derive(Deserialize, Serialize),
103    serde(rename_all = "camelCase")
104)]
105#[cfg_attr(feature = "wincode", derive(SchemaWrite, SchemaRead))]
106#[derive(Default, Debug, PartialEq, Eq, Clone)]
107pub struct Message {
108    /// The message header, identifying signed and read-only `account_keys`.
109    // NOTE: Serialization-related changes must be paired with the direct read at sigverify.
110    #[cfg_attr(
111        feature = "frozen-abi",
112        stable_abi_sample(with = "sample_legacy_header(rng)")
113    )]
114    pub header: MessageHeader,
115
116    /// All the account keys used by this transaction.
117    #[cfg_attr(feature = "serde", serde(with = "solana_short_vec"))]
118    #[cfg_attr(feature = "wincode", wincode(with = "containers::Vec<_, ShortU16>"))]
119    pub account_keys: Vec<Address>,
120
121    /// The id of a recent ledger entry.
122    pub recent_blockhash: Hash,
123
124    /// Programs that will be executed in sequence and committed in one atomic transaction if all
125    /// succeed.
126    #[cfg_attr(feature = "serde", serde(with = "solana_short_vec"))]
127    #[cfg_attr(feature = "wincode", wincode(with = "containers::Vec<_, ShortU16>"))]
128    pub instructions: Vec<CompiledInstruction>,
129}
130
131#[cfg(feature = "wincode")]
132unsafe impl<'de, C: Config> SchemaReadContext<'de, C, u8> for Message {
133    type Dst = Self;
134
135    fn read_with_context(
136        num_required_signatures: u8,
137        mut reader: impl Reader<'de>,
138        dst: &mut MaybeUninit<Self::Dst>,
139    ) -> ReadResult<()> {
140        let header = {
141            let mut reader = unsafe { reader.as_trusted_for(2) }?;
142            MessageHeader {
143                num_required_signatures,
144                num_readonly_signed_accounts: reader.take_byte()?,
145                num_readonly_unsigned_accounts: reader.take_byte()?,
146            }
147        };
148        let account_keys =
149            <containers::Vec<Address, ShortU16> as SchemaRead<C>>::get(reader.by_ref())?;
150        let recent_blockhash = <Hash as SchemaRead<C>>::get(reader.by_ref())?;
151        let instructions =
152            <containers::Vec<CompiledInstruction, ShortU16> as SchemaRead<C>>::get(reader)?;
153        dst.write(Message {
154            header,
155            account_keys,
156            recent_blockhash,
157            instructions,
158        });
159        Ok(())
160    }
161}
162
163impl Sanitize for Message {
164    fn sanitize(&self) -> Result<(), SanitizeError> {
165        // signing area and read-only non-signing area should not overlap
166        if self.header.num_required_signatures as usize
167            + self.header.num_readonly_unsigned_accounts as usize
168            > self.account_keys.len()
169        {
170            return Err(SanitizeError::IndexOutOfBounds);
171        }
172
173        // there should be at least 1 RW fee-payer account.
174        if self.header.num_readonly_signed_accounts >= self.header.num_required_signatures {
175            return Err(SanitizeError::IndexOutOfBounds);
176        }
177
178        for ci in &self.instructions {
179            if ci.program_id_index as usize >= self.account_keys.len() {
180                return Err(SanitizeError::IndexOutOfBounds);
181            }
182            // A program cannot be a payer.
183            if ci.program_id_index == 0 {
184                return Err(SanitizeError::IndexOutOfBounds);
185            }
186            for ai in &ci.accounts {
187                if *ai as usize >= self.account_keys.len() {
188                    return Err(SanitizeError::IndexOutOfBounds);
189                }
190            }
191        }
192        self.account_keys.sanitize()?;
193        self.recent_blockhash.sanitize()?;
194        self.instructions.sanitize()?;
195        Ok(())
196    }
197}
198
199impl Message {
200    /// Create a new `Message`.
201    ///
202    /// # Examples
203    ///
204    /// This example uses the [`solana_sdk`], [`solana_rpc_client`] and [`anyhow`] crates.
205    ///
206    /// [`solana_sdk`]: https://docs.rs/solana-sdk
207    /// [`solana_rpc_client`]: https://docs.rs/solana-rpc-client
208    /// [`anyhow`]: https://docs.rs/anyhow
209    ///
210    /// ```
211    /// # use solana_example_mocks::{solana_keypair, solana_signer, solana_transaction};
212    /// # use solana_example_mocks::solana_rpc_client;
213    /// use anyhow::Result;
214    /// use borsh::{BorshSerialize, BorshDeserialize};
215    /// use solana_instruction::Instruction;
216    /// use solana_keypair::Keypair;
217    /// use solana_message::Message;
218    /// use solana_address::Address;
219    /// use solana_rpc_client::rpc_client::RpcClient;
220    /// use solana_signer::Signer;
221    /// use solana_transaction::Transaction;
222    ///
223    /// // A custom program instruction. This would typically be defined in
224    /// // another crate so it can be shared between the on-chain program and
225    /// // the client.
226    /// #[derive(BorshSerialize, BorshDeserialize)]
227    /// # #[borsh(crate = "borsh")]
228    /// enum BankInstruction {
229    ///     Initialize,
230    ///     Deposit { lamports: u64 },
231    ///     Withdraw { lamports: u64 },
232    /// }
233    ///
234    /// fn send_initialize_tx(
235    ///     client: &RpcClient,
236    ///     program_id: Address,
237    ///     payer: &Keypair
238    /// ) -> Result<()> {
239    ///
240    ///     let bank_instruction = BankInstruction::Initialize;
241    ///
242    ///     let instruction = Instruction::new_with_borsh(
243    ///         program_id,
244    ///         &bank_instruction,
245    ///         vec![],
246    ///     );
247    ///
248    ///     let message = Message::new(
249    ///         &[instruction],
250    ///         Some(&payer.pubkey()),
251    ///     );
252    ///
253    ///     let blockhash = client.get_latest_blockhash()?;
254    ///     let mut tx = Transaction::new(&[payer], message, blockhash);
255    ///     client.send_and_confirm_transaction(&tx)?;
256    ///
257    ///     Ok(())
258    /// }
259    /// #
260    /// # let client = RpcClient::new(String::new());
261    /// # let program_id = Address::new_unique();
262    /// # let payer = Keypair::new();
263    /// # send_initialize_tx(&client, program_id, &payer)?;
264    /// #
265    /// # Ok::<(), anyhow::Error>(())
266    /// ```
267    pub fn new(instructions: &[Instruction], payer: Option<&Address>) -> Self {
268        Self::new_with_blockhash(instructions, payer, &Hash::default())
269    }
270
271    /// Create a new message while setting the blockhash.
272    ///
273    /// # Examples
274    ///
275    /// This example uses the [`solana_sdk`], [`solana_rpc_client`] and [`anyhow`] crates.
276    ///
277    /// [`solana_sdk`]: https://docs.rs/solana-sdk
278    /// [`solana_rpc_client`]: https://docs.rs/solana-rpc-client
279    /// [`anyhow`]: https://docs.rs/anyhow
280    ///
281    /// ```
282    /// # use solana_example_mocks::{solana_keypair, solana_signer, solana_transaction};
283    /// # use solana_example_mocks::solana_rpc_client;
284    /// use anyhow::Result;
285    /// use borsh::{BorshSerialize, BorshDeserialize};
286    /// use solana_instruction::Instruction;
287    /// use solana_keypair::Keypair;
288    /// use solana_message::Message;
289    /// use solana_address::Address;
290    /// use solana_rpc_client::rpc_client::RpcClient;
291    /// use solana_signer::Signer;
292    /// use solana_transaction::Transaction;
293    ///
294    /// // A custom program instruction. This would typically be defined in
295    /// // another crate so it can be shared between the on-chain program and
296    /// // the client.
297    /// #[derive(BorshSerialize, BorshDeserialize)]
298    /// # #[borsh(crate = "borsh")]
299    /// enum BankInstruction {
300    ///     Initialize,
301    ///     Deposit { lamports: u64 },
302    ///     Withdraw { lamports: u64 },
303    /// }
304    ///
305    /// fn send_initialize_tx(
306    ///     client: &RpcClient,
307    ///     program_id: Address,
308    ///     payer: &Keypair
309    /// ) -> Result<()> {
310    ///
311    ///     let bank_instruction = BankInstruction::Initialize;
312    ///
313    ///     let instruction = Instruction::new_with_borsh(
314    ///         program_id,
315    ///         &bank_instruction,
316    ///         vec![],
317    ///     );
318    ///
319    ///     let blockhash = client.get_latest_blockhash()?;
320    ///
321    ///     let message = Message::new_with_blockhash(
322    ///         &[instruction],
323    ///         Some(&payer.pubkey()),
324    ///         &blockhash,
325    ///     );
326    ///
327    ///     let mut tx = Transaction::new_unsigned(message);
328    ///     tx.sign(&[payer], blockhash);
329    ///     client.send_and_confirm_transaction(&tx)?;
330    ///
331    ///     Ok(())
332    /// }
333    /// #
334    /// # let client = RpcClient::new(String::new());
335    /// # let program_id = Address::new_unique();
336    /// # let payer = Keypair::new();
337    /// # send_initialize_tx(&client, program_id, &payer)?;
338    /// #
339    /// # Ok::<(), anyhow::Error>(())
340    /// ```
341    pub fn new_with_blockhash(
342        instructions: &[Instruction],
343        payer: Option<&Address>,
344        blockhash: &Hash,
345    ) -> Self {
346        let compiled_keys = CompiledKeys::compile(instructions, payer.cloned());
347        let (header, account_keys) = compiled_keys
348            .try_into_message_components()
349            .expect("overflow when compiling message keys");
350        let instructions = compile_instructions(instructions, &account_keys);
351        Self::new_with_compiled_instructions(
352            header.num_required_signatures,
353            header.num_readonly_signed_accounts,
354            header.num_readonly_unsigned_accounts,
355            account_keys,
356            Hash::new_from_array(blockhash.to_bytes()),
357            instructions,
358        )
359    }
360
361    /// Create a new message for a [nonced transaction].
362    ///
363    /// [nonced transaction]: https://docs.solanalabs.com/implemented-proposals/durable-tx-nonces
364    ///
365    /// In this type of transaction, the blockhash is replaced with a _durable
366    /// transaction nonce_, allowing for extended time to pass between the
367    /// transaction's signing and submission to the blockchain.
368    ///
369    /// # Examples
370    ///
371    /// This example uses the [`solana_sdk`], [`solana_rpc_client`] and [`anyhow`] crates.
372    ///
373    /// [`solana_sdk`]: https://docs.rs/solana-sdk
374    /// [`solana_rpc_client`]: https://docs.rs/solana-client
375    /// [`anyhow`]: https://docs.rs/anyhow
376    ///
377    /// ```
378    /// # use solana_example_mocks::{solana_keypair, solana_signer, solana_transaction};
379    /// # use solana_example_mocks::solana_rpc_client;
380    /// use anyhow::Result;
381    /// use borsh::{BorshSerialize, BorshDeserialize};
382    /// use solana_hash::Hash;
383    /// use solana_instruction::Instruction;
384    /// use solana_keypair::Keypair;
385    /// use solana_message::Message;
386    /// use solana_address::Address;
387    /// use solana_rpc_client::rpc_client::RpcClient;
388    /// use solana_signer::Signer;
389    /// use solana_transaction::Transaction;
390    /// use solana_system_interface::instruction::create_nonce_account;
391    ///
392    /// // A custom program instruction. This would typically be defined in
393    /// // another crate so it can be shared between the on-chain program and
394    /// // the client.
395    /// #[derive(BorshSerialize, BorshDeserialize)]
396    /// # #[borsh(crate = "borsh")]
397    /// enum BankInstruction {
398    ///     Initialize,
399    ///     Deposit { lamports: u64 },
400    ///     Withdraw { lamports: u64 },
401    /// }
402    ///
403    /// // Create a nonced transaction for later signing and submission,
404    /// // returning it and the nonce account's pubkey.
405    /// fn create_offline_initialize_tx(
406    ///     client: &RpcClient,
407    ///     program_id: Address,
408    ///     payer: &Keypair
409    /// ) -> Result<(Transaction, Address)> {
410    ///
411    ///     let bank_instruction = BankInstruction::Initialize;
412    ///     let bank_instruction = Instruction::new_with_borsh(
413    ///         program_id,
414    ///         &bank_instruction,
415    ///         vec![],
416    ///     );
417    ///
418    ///     // This will create a nonce account and assign authority to the
419    ///     // payer so they can sign to advance the nonce and withdraw its rent.
420    ///     let nonce_account = make_nonce_account(client, payer)?;
421    ///
422    ///     let mut message = Message::new_with_nonce(
423    ///         vec![bank_instruction],
424    ///         Some(&payer.pubkey()),
425    ///         &nonce_account,
426    ///         &payer.pubkey()
427    ///     );
428    ///
429    ///     // This transaction will need to be signed later, using the blockhash
430    ///     // stored in the nonce account.
431    ///     let tx = Transaction::new_unsigned(message);
432    ///
433    ///     Ok((tx, nonce_account))
434    /// }
435    ///
436    /// fn make_nonce_account(client: &RpcClient, payer: &Keypair)
437    ///     -> Result<Address>
438    /// {
439    ///     let nonce_account_address = Keypair::new();
440    ///     let nonce_account_size = solana_nonce::state::State::size();
441    ///     let nonce_rent = client.get_minimum_balance_for_rent_exemption(nonce_account_size)?;
442    ///
443    ///     // Assigning the nonce authority to the payer so they can sign for the withdrawal,
444    ///     // and we can throw away the nonce address secret key.
445    ///     let create_nonce_instr = create_nonce_account(
446    ///         &payer.pubkey(),
447    ///         &nonce_account_address.pubkey(),
448    ///         &payer.pubkey(),
449    ///         nonce_rent,
450    ///     );
451    ///
452    ///     let mut nonce_tx = Transaction::new_with_payer(&create_nonce_instr, Some(&payer.pubkey()));
453    ///     let blockhash = client.get_latest_blockhash()?;
454    ///     nonce_tx.sign(&[&payer, &nonce_account_address], blockhash);
455    ///     client.send_and_confirm_transaction(&nonce_tx)?;
456    ///
457    ///     Ok(nonce_account_address.pubkey())
458    /// }
459    /// #
460    /// # let client = RpcClient::new(String::new());
461    /// # let program_id = Address::new_unique();
462    /// # let payer = Keypair::new();
463    /// # create_offline_initialize_tx(&client, program_id, &payer)?;
464    /// # Ok::<(), anyhow::Error>(())
465    /// ```
466    pub fn new_with_nonce(
467        mut instructions: Vec<Instruction>,
468        payer: Option<&Address>,
469        nonce_account_pubkey: &Address,
470        nonce_authority_pubkey: &Address,
471    ) -> Self {
472        let nonce_ix =
473            advance_nonce_account_instruction(nonce_account_pubkey, nonce_authority_pubkey);
474        instructions.insert(0, nonce_ix);
475        Self::new(&instructions, payer)
476    }
477
478    pub fn new_with_compiled_instructions(
479        num_required_signatures: u8,
480        num_readonly_signed_accounts: u8,
481        num_readonly_unsigned_accounts: u8,
482        account_keys: Vec<Address>,
483        recent_blockhash: Hash,
484        instructions: Vec<CompiledInstruction>,
485    ) -> Self {
486        Self {
487            header: MessageHeader {
488                num_required_signatures,
489                num_readonly_signed_accounts,
490                num_readonly_unsigned_accounts,
491            },
492            account_keys,
493            recent_blockhash,
494            instructions,
495        }
496    }
497
498    /// Compute the blake3 hash of this transaction's message.
499    #[cfg(all(feature = "wincode", feature = "blake3"))]
500    pub fn hash(&self) -> Hash {
501        let message_bytes = self.serialize();
502        Self::hash_raw_message(&message_bytes)
503    }
504
505    /// Compute the blake3 hash of a raw transaction message.
506    #[cfg(feature = "blake3")]
507    pub fn hash_raw_message(message_bytes: &[u8]) -> Hash {
508        use {blake3::traits::digest::Digest, solana_hash::HASH_BYTES};
509        let mut hasher = blake3::Hasher::new();
510        hasher.update(b"solana-tx-message-v1");
511        hasher.update(message_bytes);
512        let hash_bytes: [u8; HASH_BYTES] = hasher.finalize().into();
513        hash_bytes.into()
514    }
515
516    pub fn compile_instruction(&self, ix: &Instruction) -> CompiledInstruction {
517        compile_instruction(ix, &self.account_keys)
518    }
519
520    #[cfg(feature = "wincode")]
521    pub fn serialize(&self) -> Vec<u8> {
522        wincode::serialize(self).unwrap()
523    }
524
525    pub fn program_id(&self, instruction_index: usize) -> Option<&Address> {
526        Some(
527            &self.account_keys[self.instructions.get(instruction_index)?.program_id_index as usize],
528        )
529    }
530
531    pub fn program_index(&self, instruction_index: usize) -> Option<usize> {
532        Some(self.instructions.get(instruction_index)?.program_id_index as usize)
533    }
534
535    pub fn program_ids(&self) -> Vec<&Address> {
536        self.instructions
537            .iter()
538            .map(|ix| &self.account_keys[ix.program_id_index as usize])
539            .collect()
540    }
541
542    /// Returns true if the account at the specified index is an account input
543    /// to some program instruction in this message.
544    pub fn is_instruction_account(&self, key_index: usize) -> bool {
545        if let Ok(key_index) = u8::try_from(key_index) {
546            self.instructions
547                .iter()
548                .any(|ix| ix.accounts.contains(&key_index))
549        } else {
550            false
551        }
552    }
553
554    pub fn is_key_called_as_program(&self, key_index: usize) -> bool {
555        super::is_key_called_as_program(&self.instructions, key_index)
556    }
557
558    pub fn program_position(&self, index: usize) -> Option<usize> {
559        let program_ids = self.program_ids();
560        program_ids
561            .iter()
562            .position(|&&pubkey| pubkey == self.account_keys[index])
563    }
564
565    pub fn maybe_executable(&self, i: usize) -> bool {
566        self.program_position(i).is_some()
567    }
568
569    pub fn demote_program_id(&self, i: usize) -> bool {
570        super::is_program_id_write_demoted(i, &self.account_keys, &self.instructions)
571    }
572
573    /// Returns true if the account at the specified index was requested to be
574    /// writable. This method should not be used directly.
575    #[cfg(feature = "std")]
576    pub(super) fn is_writable_index(&self, i: usize) -> bool {
577        super::is_writable_index(i, self.header, &self.account_keys)
578    }
579
580    /// Returns true if the account at the specified index is writable by the
581    /// instructions in this message.
582    ///
583    /// # Important
584    ///
585    /// The `reserved_addresses` param is optional to allow clients to approximate
586    /// writability without requiring fetching the latest set of protocol-reserved
587    /// addresses. If this method is called by the runtime, the latest set of
588    /// reserved addresses must be passed.
589    pub fn is_maybe_writable_with_reserved_addresses(
590        &self,
591        i: usize,
592        reserved_addresses: Option<&impl AddressSet>,
593    ) -> bool {
594        super::is_maybe_writable(
595            i,
596            self.header,
597            &self.account_keys,
598            &self.instructions,
599            reserved_addresses,
600        )
601    }
602
603    pub fn is_signer(&self, i: usize) -> bool {
604        i < self.header.num_required_signatures as usize
605    }
606
607    pub fn signer_keys(&self) -> Vec<&Address> {
608        // Clamp in case we're working on un-`sanitize()`ed input
609        let last_key = self
610            .account_keys
611            .len()
612            .min(self.header.num_required_signatures as usize);
613        self.account_keys[..last_key].iter().collect()
614    }
615
616    /// Returns `true` if `account_keys` has any duplicate keys.
617    pub fn has_duplicates(&self) -> bool {
618        // Note: This is an O(n^2) algorithm, but requires no heap allocations. The benchmark
619        // `bench_has_duplicates` in benches/message_processor.rs shows that this implementation is
620        // ~50 times faster than using HashSet for very short slices.
621        for i in 1..self.account_keys.len() {
622            #[allow(clippy::arithmetic_side_effects)]
623            if self.account_keys[i..].contains(&self.account_keys[i - 1]) {
624                return true;
625            }
626        }
627        false
628    }
629
630    /// Returns `true` if any account is the BPF upgradeable loader.
631    pub fn is_upgradeable_loader_present(&self) -> bool {
632        super::is_upgradeable_loader_present(&self.account_keys)
633    }
634}
635
636#[cfg(test)]
637mod tests {
638    use {
639        super::*, crate::MESSAGE_HEADER_LENGTH, alloc::vec, core::str::FromStr,
640        solana_instruction::AccountMeta,
641    };
642
643    #[test]
644    // Ensure there's a way to calculate the number of required signatures.
645    fn test_message_signed_keys_len() {
646        let program_id = Address::default();
647        let id0 = Address::default();
648        let ix = Instruction::new_with_bincode(program_id, &0, vec![AccountMeta::new(id0, false)]);
649        let message = Message::new(&[ix], None);
650        assert_eq!(message.header.num_required_signatures, 0);
651
652        let ix = Instruction::new_with_bincode(program_id, &0, vec![AccountMeta::new(id0, true)]);
653        let message = Message::new(&[ix], Some(&id0));
654        assert_eq!(message.header.num_required_signatures, 1);
655    }
656
657    #[test]
658    fn test_message_kitchen_sink() {
659        let program_id0 = Address::new_unique();
660        let program_id1 = Address::new_unique();
661        let id0 = Address::default();
662        let id1 = Address::new_unique();
663        let message = Message::new(
664            &[
665                Instruction::new_with_bincode(program_id0, &0, vec![AccountMeta::new(id0, false)]),
666                Instruction::new_with_bincode(program_id1, &0, vec![AccountMeta::new(id1, true)]),
667                Instruction::new_with_bincode(program_id0, &0, vec![AccountMeta::new(id1, false)]),
668            ],
669            Some(&id1),
670        );
671        assert_eq!(
672            message.instructions[0],
673            CompiledInstruction::new(2, &0, vec![1])
674        );
675        assert_eq!(
676            message.instructions[1],
677            CompiledInstruction::new(3, &0, vec![0])
678        );
679        assert_eq!(
680            message.instructions[2],
681            CompiledInstruction::new(2, &0, vec![0])
682        );
683    }
684
685    #[test]
686    fn test_message_payer_first() {
687        let program_id = Address::default();
688        let payer = Address::new_unique();
689        let id0 = Address::default();
690
691        let ix = Instruction::new_with_bincode(program_id, &0, vec![AccountMeta::new(id0, false)]);
692        let message = Message::new(&[ix], Some(&payer));
693        assert_eq!(message.header.num_required_signatures, 1);
694
695        let ix = Instruction::new_with_bincode(program_id, &0, vec![AccountMeta::new(id0, true)]);
696        let message = Message::new(&[ix], Some(&payer));
697        assert_eq!(message.header.num_required_signatures, 2);
698
699        let ix = Instruction::new_with_bincode(
700            program_id,
701            &0,
702            vec![AccountMeta::new(payer, true), AccountMeta::new(id0, true)],
703        );
704        let message = Message::new(&[ix], Some(&payer));
705        assert_eq!(message.header.num_required_signatures, 2);
706    }
707
708    #[test]
709    fn test_program_position() {
710        let program_id0 = Address::default();
711        let program_id1 = Address::new_unique();
712        let id = Address::new_unique();
713        let message = Message::new(
714            &[
715                Instruction::new_with_bincode(program_id0, &0, vec![AccountMeta::new(id, false)]),
716                Instruction::new_with_bincode(program_id1, &0, vec![AccountMeta::new(id, true)]),
717            ],
718            Some(&id),
719        );
720        assert_eq!(message.program_position(0), None);
721        assert_eq!(message.program_position(1), Some(0));
722        assert_eq!(message.program_position(2), Some(1));
723    }
724
725    #[test]
726    fn test_program_ids() {
727        let key0 = Address::new_unique();
728        let key1 = Address::new_unique();
729        let loader2 = Address::new_unique();
730        let instructions = vec![CompiledInstruction::new(2, &(), vec![0, 1])];
731        let message = Message::new_with_compiled_instructions(
732            1,
733            0,
734            2,
735            vec![key0, key1, loader2],
736            Hash::default(),
737            instructions,
738        );
739        assert_eq!(message.program_ids(), vec![&loader2]);
740    }
741
742    #[test]
743    fn test_is_instruction_account() {
744        let key0 = Address::new_unique();
745        let key1 = Address::new_unique();
746        let loader2 = Address::new_unique();
747        let instructions = vec![CompiledInstruction::new(2, &(), vec![0, 1])];
748        let message = Message::new_with_compiled_instructions(
749            1,
750            0,
751            2,
752            vec![key0, key1, loader2],
753            Hash::default(),
754            instructions,
755        );
756
757        assert!(message.is_instruction_account(0));
758        assert!(message.is_instruction_account(1));
759        assert!(!message.is_instruction_account(2));
760    }
761
762    #[test]
763    fn test_message_header_len_constant() {
764        assert_eq!(
765            bincode::serialized_size(&MessageHeader::default()).unwrap() as usize,
766            MESSAGE_HEADER_LENGTH
767        );
768    }
769
770    #[test]
771    fn test_message_hash() {
772        // when this test fails, it's most likely due to a new serialized format of a message.
773        // in this case, the domain prefix `solana-tx-message-v1` should be updated.
774        let program_id0 = Address::from_str("4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM").unwrap();
775        let program_id1 = Address::from_str("8opHzTAnfzRpPEx21XtnrVTX28YQuCpAjcn1PczScKh").unwrap();
776        let id0 = Address::from_str("CiDwVBFgWV9E5MvXWoLgnEgn2hK7rJikbvfWavzAQz3").unwrap();
777        let id1 = Address::from_str("GcdayuLaLyrdmUu324nahyv33G5poQdLUEZ1nEytDeP").unwrap();
778        let id2 = Address::from_str("LX3EUdRUBUa3TbsYXLEUdj9J3prXkWXvLYSWyYyc2Jj").unwrap();
779        let id3 = Address::from_str("QRSsyMWN1yHT9ir42bgNZUNZ4PdEhcSWCrL2AryKpy5").unwrap();
780        let instructions = vec![
781            Instruction::new_with_bincode(program_id0, &0, vec![AccountMeta::new(id0, false)]),
782            Instruction::new_with_bincode(program_id0, &0, vec![AccountMeta::new(id1, true)]),
783            Instruction::new_with_bincode(
784                program_id1,
785                &0,
786                vec![AccountMeta::new_readonly(id2, false)],
787            ),
788            Instruction::new_with_bincode(
789                program_id1,
790                &0,
791                vec![AccountMeta::new_readonly(id3, true)],
792            ),
793        ];
794
795        let message = Message::new(&instructions, Some(&id1));
796        assert_eq!(
797            message.hash(),
798            Hash::from_str("7VWCF4quo2CcWQFNUayZiorxpiR5ix8YzLebrXKf3fMF").unwrap()
799        )
800    }
801
802    #[test]
803    fn test_is_writable_index_saturating_behavior() {
804        // Directly matching issue #150 PoC 1:
805        // num_readonly_signed_accounts > num_required_signatures
806        // This now results in the first part of the OR condition in is_writable_index effectively becoming `i < 0`.
807        let key0 = Address::new_unique();
808        let message1 = Message {
809            header: MessageHeader {
810                num_required_signatures: 1,
811                num_readonly_signed_accounts: 2, // 2 > 1
812                num_readonly_unsigned_accounts: 0,
813            },
814            account_keys: vec![key0],
815            recent_blockhash: Hash::default(),
816            instructions: vec![],
817        };
818        assert!(!message1.is_writable_index(0));
819
820        // Matching issue #150 PoC 2 - num_readonly_unsigned_accounts > account_keys.len()
821        let key_for_poc2 = Address::new_unique();
822        let message2 = Message {
823            header: MessageHeader {
824                num_required_signatures: 0,
825                num_readonly_signed_accounts: 0,
826                num_readonly_unsigned_accounts: 2, // 2 > account_keys.len() (1)
827            },
828            account_keys: vec![key_for_poc2],
829            recent_blockhash: Hash::default(),
830            instructions: vec![],
831        };
832        assert!(!message2.is_writable_index(0));
833
834        // Scenario 3: num_readonly_unsigned_accounts > account_keys.len() with writable signed account
835        // This should result in the first condition being true for the signed account
836        let message3 = Message {
837            header: MessageHeader {
838                num_required_signatures: 1, // Writable range starts before index 1
839                num_readonly_signed_accounts: 0,
840                num_readonly_unsigned_accounts: 2, // 2 > account_keys.len() (1)
841            },
842            account_keys: vec![key0],
843            recent_blockhash: Hash::default(),
844            instructions: vec![],
845        };
846        assert!(message3.is_writable_index(0));
847
848        // Scenario 4: Both conditions, and testing an index that would rely on the second part of OR
849        let key1 = Address::new_unique();
850        let message4 = Message {
851            header: MessageHeader {
852                num_required_signatures: 1, // Writable range starts before index 1 for signed accounts
853                num_readonly_signed_accounts: 0,
854                num_readonly_unsigned_accounts: 3, // 3 > account_keys.len() (2)
855            },
856            account_keys: vec![key0, key1],
857            recent_blockhash: Hash::default(),
858            instructions: vec![],
859        };
860        assert!(message4.is_writable_index(0));
861        assert!(!message4.is_writable_index(1));
862
863        // Scenario 5: num_required_signatures is 0 due to saturating_sub
864        // and num_readonly_unsigned_accounts makes the second range empty
865        let message5 = Message {
866            header: MessageHeader {
867                num_required_signatures: 1,
868                num_readonly_signed_accounts: 2, // 1.saturating_sub(2) = 0
869                num_readonly_unsigned_accounts: 3, // account_keys.len().saturating_sub(3) potentially 0
870            },
871            account_keys: vec![key0, key1], // len is 2
872            recent_blockhash: Hash::default(),
873            instructions: vec![],
874        };
875        assert!(!message5.is_writable_index(0));
876        assert!(!message5.is_writable_index(1));
877    }
878}