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