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