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