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}