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