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