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