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}