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