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