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