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