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