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