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