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