clone_solana_message/
sanitized.rs

1#[deprecated(
2    since = "2.1.0",
3    note = "Use clone_solana_transaction_error::SanitizeMessageError instead"
4)]
5pub use clone_solana_transaction_error::SanitizeMessageError;
6use {
7    crate::{
8        compiled_instruction::CompiledInstruction,
9        legacy,
10        v0::{self, LoadedAddresses},
11        AccountKeys, AddressLoader, MessageHeader, SanitizedVersionedMessage, VersionedMessage,
12    },
13    clone_solana_hash::Hash,
14    clone_solana_instruction::{BorrowedAccountMeta, BorrowedInstruction},
15    clone_solana_pubkey::Pubkey,
16    clone_solana_sanitize::Sanitize,
17    clone_solana_sdk_ids::{ed25519_program, secp256k1_program, secp256r1_program},
18    std::{borrow::Cow, collections::HashSet, convert::TryFrom},
19};
20
21// inlined to avoid clone_solana_nonce dep
22#[cfg(feature = "bincode")]
23const NONCED_TX_MARKER_IX_INDEX: u8 = 0;
24#[cfg(test)]
25static_assertions::const_assert_eq!(
26    NONCED_TX_MARKER_IX_INDEX,
27    clone_solana_nonce::NONCED_TX_MARKER_IX_INDEX
28);
29
30#[derive(Debug, Clone, Eq, PartialEq)]
31pub struct LegacyMessage<'a> {
32    /// Legacy message
33    pub message: Cow<'a, legacy::Message>,
34    /// List of boolean with same length as account_keys(), each boolean value indicates if
35    /// corresponding account key is writable or not.
36    pub is_writable_account_cache: Vec<bool>,
37}
38
39impl LegacyMessage<'_> {
40    pub fn new(message: legacy::Message, reserved_account_keys: &HashSet<Pubkey>) -> Self {
41        let is_writable_account_cache = message
42            .account_keys
43            .iter()
44            .enumerate()
45            .map(|(i, _key)| {
46                message.is_writable_index(i)
47                    && !reserved_account_keys.contains(&message.account_keys[i])
48                    && !message.demote_program_id(i)
49            })
50            .collect::<Vec<_>>();
51        Self {
52            message: Cow::Owned(message),
53            is_writable_account_cache,
54        }
55    }
56
57    pub fn has_duplicates(&self) -> bool {
58        self.message.has_duplicates()
59    }
60
61    pub fn is_key_called_as_program(&self, key_index: usize) -> bool {
62        self.message.is_key_called_as_program(key_index)
63    }
64
65    /// Inspect all message keys for the bpf upgradeable loader
66    pub fn is_upgradeable_loader_present(&self) -> bool {
67        self.message.is_upgradeable_loader_present()
68    }
69
70    /// Returns the full list of account keys.
71    pub fn account_keys(&self) -> AccountKeys {
72        AccountKeys::new(&self.message.account_keys, None)
73    }
74
75    pub fn is_writable(&self, index: usize) -> bool {
76        *self.is_writable_account_cache.get(index).unwrap_or(&false)
77    }
78}
79
80/// Sanitized message of a transaction.
81#[derive(Debug, Clone, Eq, PartialEq)]
82pub enum SanitizedMessage {
83    /// Sanitized legacy message
84    Legacy(LegacyMessage<'static>),
85    /// Sanitized version #0 message with dynamically loaded addresses
86    V0(v0::LoadedMessage<'static>),
87}
88
89impl SanitizedMessage {
90    /// Create a sanitized message from a sanitized versioned message.
91    /// If the input message uses address tables, attempt to look up the
92    /// address for each table index.
93    pub fn try_new(
94        sanitized_msg: SanitizedVersionedMessage,
95        address_loader: impl AddressLoader,
96        reserved_account_keys: &HashSet<Pubkey>,
97    ) -> Result<Self, SanitizeMessageError> {
98        Ok(match sanitized_msg.message {
99            VersionedMessage::Legacy(message) => {
100                SanitizedMessage::Legacy(LegacyMessage::new(message, reserved_account_keys))
101            }
102            VersionedMessage::V0(message) => {
103                let loaded_addresses =
104                    address_loader.load_addresses(&message.address_table_lookups)?;
105                SanitizedMessage::V0(v0::LoadedMessage::new(
106                    message,
107                    loaded_addresses,
108                    reserved_account_keys,
109                ))
110            }
111        })
112    }
113
114    /// Create a sanitized legacy message
115    pub fn try_from_legacy_message(
116        message: legacy::Message,
117        reserved_account_keys: &HashSet<Pubkey>,
118    ) -> Result<Self, SanitizeMessageError> {
119        message.sanitize()?;
120        Ok(Self::Legacy(LegacyMessage::new(
121            message,
122            reserved_account_keys,
123        )))
124    }
125
126    /// Return true if this message contains duplicate account keys
127    pub fn has_duplicates(&self) -> bool {
128        match self {
129            SanitizedMessage::Legacy(message) => message.has_duplicates(),
130            SanitizedMessage::V0(message) => message.has_duplicates(),
131        }
132    }
133
134    /// Message header which identifies the number of signer and writable or
135    /// readonly accounts
136    pub fn header(&self) -> &MessageHeader {
137        match self {
138            Self::Legacy(legacy_message) => &legacy_message.message.header,
139            Self::V0(loaded_msg) => &loaded_msg.message.header,
140        }
141    }
142
143    /// Returns a legacy message if this sanitized message wraps one
144    pub fn legacy_message(&self) -> Option<&legacy::Message> {
145        if let Self::Legacy(legacy_message) = &self {
146            Some(&legacy_message.message)
147        } else {
148            None
149        }
150    }
151
152    /// Returns the fee payer for the transaction
153    pub fn fee_payer(&self) -> &Pubkey {
154        self.account_keys()
155            .get(0)
156            .expect("sanitized messages always have a fee payer at index 0")
157    }
158
159    /// The hash of a recent block, used for timing out a transaction
160    pub fn recent_blockhash(&self) -> &Hash {
161        match self {
162            Self::Legacy(legacy_message) => &legacy_message.message.recent_blockhash,
163            Self::V0(loaded_msg) => &loaded_msg.message.recent_blockhash,
164        }
165    }
166
167    /// Program instructions that will be executed in sequence and committed in
168    /// one atomic transaction if all succeed.
169    pub fn instructions(&self) -> &[CompiledInstruction] {
170        match self {
171            Self::Legacy(legacy_message) => &legacy_message.message.instructions,
172            Self::V0(loaded_msg) => &loaded_msg.message.instructions,
173        }
174    }
175
176    /// Program instructions iterator which includes each instruction's program
177    /// id.
178    pub fn program_instructions_iter(
179        &self,
180    ) -> impl Iterator<Item = (&Pubkey, &CompiledInstruction)> + Clone {
181        self.instructions().iter().map(move |ix| {
182            (
183                self.account_keys()
184                    .get(usize::from(ix.program_id_index))
185                    .expect("program id index is sanitized"),
186                ix,
187            )
188        })
189    }
190
191    /// Return the list of statically included account keys.
192    pub fn static_account_keys(&self) -> &[Pubkey] {
193        match self {
194            Self::Legacy(legacy_message) => &legacy_message.message.account_keys,
195            Self::V0(loaded_msg) => &loaded_msg.message.account_keys,
196        }
197    }
198
199    /// Returns the list of account keys that are loaded for this message.
200    pub fn account_keys(&self) -> AccountKeys {
201        match self {
202            Self::Legacy(message) => message.account_keys(),
203            Self::V0(message) => message.account_keys(),
204        }
205    }
206
207    /// Returns the list of account keys used for account lookup tables.
208    pub fn message_address_table_lookups(&self) -> &[v0::MessageAddressTableLookup] {
209        match self {
210            Self::Legacy(_message) => &[],
211            Self::V0(message) => &message.message.address_table_lookups,
212        }
213    }
214
215    /// Returns true if the account at the specified index is an input to some
216    /// program instruction in this message.
217    #[deprecated(since = "2.0.0", note = "Please use `is_instruction_account` instead")]
218    pub fn is_key_passed_to_program(&self, key_index: usize) -> bool {
219        self.is_instruction_account(key_index)
220    }
221
222    /// Returns true if the account at the specified index is an input to some
223    /// program instruction in this message.
224    pub fn is_instruction_account(&self, key_index: usize) -> bool {
225        if let Ok(key_index) = u8::try_from(key_index) {
226            self.instructions()
227                .iter()
228                .any(|ix| ix.accounts.contains(&key_index))
229        } else {
230            false
231        }
232    }
233
234    /// Returns true if the account at the specified index is invoked as a
235    /// program in this message.
236    pub fn is_invoked(&self, key_index: usize) -> bool {
237        match self {
238            Self::Legacy(message) => message.is_key_called_as_program(key_index),
239            Self::V0(message) => message.is_key_called_as_program(key_index),
240        }
241    }
242
243    /// Returns true if the account at the specified index is not invoked as a
244    /// program or, if invoked, is passed to a program.
245    #[deprecated(
246        since = "2.0.0",
247        note = "Please use `is_invoked` and `is_instruction_account` instead"
248    )]
249    pub fn is_non_loader_key(&self, key_index: usize) -> bool {
250        !self.is_invoked(key_index) || self.is_instruction_account(key_index)
251    }
252
253    /// Returns true if the account at the specified index is writable by the
254    /// instructions in this message.
255    pub fn is_writable(&self, index: usize) -> bool {
256        match self {
257            Self::Legacy(message) => message.is_writable(index),
258            Self::V0(message) => message.is_writable(index),
259        }
260    }
261
262    /// Returns true if the account at the specified index signed this
263    /// message.
264    pub fn is_signer(&self, index: usize) -> bool {
265        index < usize::from(self.header().num_required_signatures)
266    }
267
268    /// Return the resolved addresses for this message if it has any.
269    fn loaded_lookup_table_addresses(&self) -> Option<&LoadedAddresses> {
270        match &self {
271            SanitizedMessage::V0(message) => Some(&message.loaded_addresses),
272            _ => None,
273        }
274    }
275
276    /// Return the number of readonly accounts loaded by this message.
277    pub fn num_readonly_accounts(&self) -> usize {
278        let loaded_readonly_addresses = self
279            .loaded_lookup_table_addresses()
280            .map(|keys| keys.readonly.len())
281            .unwrap_or_default();
282        loaded_readonly_addresses
283            .saturating_add(usize::from(self.header().num_readonly_signed_accounts))
284            .saturating_add(usize::from(self.header().num_readonly_unsigned_accounts))
285    }
286
287    /// Decompile message instructions without cloning account keys
288    pub fn decompile_instructions(&self) -> Vec<BorrowedInstruction> {
289        let account_keys = self.account_keys();
290        self.program_instructions_iter()
291            .map(|(program_id, instruction)| {
292                let accounts = instruction
293                    .accounts
294                    .iter()
295                    .map(|account_index| {
296                        let account_index = *account_index as usize;
297                        BorrowedAccountMeta {
298                            is_signer: self.is_signer(account_index),
299                            is_writable: self.is_writable(account_index),
300                            pubkey: account_keys.get(account_index).unwrap(),
301                        }
302                    })
303                    .collect();
304
305                BorrowedInstruction {
306                    accounts,
307                    data: &instruction.data,
308                    program_id,
309                }
310            })
311            .collect()
312    }
313
314    /// Inspect all message keys for the bpf upgradeable loader
315    pub fn is_upgradeable_loader_present(&self) -> bool {
316        match self {
317            Self::Legacy(message) => message.is_upgradeable_loader_present(),
318            Self::V0(message) => message.is_upgradeable_loader_present(),
319        }
320    }
321
322    /// Get a list of signers for the instruction at the given index
323    pub fn get_ix_signers(&self, ix_index: usize) -> impl Iterator<Item = &Pubkey> {
324        self.instructions()
325            .get(ix_index)
326            .into_iter()
327            .flat_map(|ix| {
328                ix.accounts
329                    .iter()
330                    .copied()
331                    .map(usize::from)
332                    .filter(|index| self.is_signer(*index))
333                    .filter_map(|signer_index| self.account_keys().get(signer_index))
334            })
335    }
336
337    #[cfg(feature = "bincode")]
338    /// If the message uses a durable nonce, return the pubkey of the nonce account
339    pub fn get_durable_nonce(&self) -> Option<&Pubkey> {
340        self.instructions()
341            .get(NONCED_TX_MARKER_IX_INDEX as usize)
342            .filter(
343                |ix| match self.account_keys().get(ix.program_id_index as usize) {
344                    Some(program_id) => clone_solana_sdk_ids::system_program::check_id(program_id),
345                    _ => false,
346                },
347            )
348            .filter(|ix| {
349                matches!(
350                    clone_solana_bincode::limited_deserialize(
351                        &ix.data, 4 /* serialized size of AdvanceNonceAccount */
352                    ),
353                    Ok(clone_solana_system_interface::instruction::SystemInstruction::AdvanceNonceAccount)
354                )
355            })
356            .and_then(|ix| {
357                ix.accounts.first().and_then(|idx| {
358                    let idx = *idx as usize;
359                    if !self.is_writable(idx) {
360                        None
361                    } else {
362                        self.account_keys().get(idx)
363                    }
364                })
365            })
366    }
367
368    #[deprecated(
369        since = "2.1.0",
370        note = "Please use `SanitizedMessage::num_total_signatures` instead."
371    )]
372    pub fn num_signatures(&self) -> u64 {
373        self.num_total_signatures()
374    }
375
376    /// Returns the total number of signatures in the message.
377    /// This includes required transaction signatures as well as any
378    /// pre-compile signatures that are attached in instructions.
379    pub fn num_total_signatures(&self) -> u64 {
380        self.get_signature_details().total_signatures()
381    }
382
383    /// Returns the number of requested write-locks in this message.
384    /// This does not consider if write-locks are demoted.
385    pub fn num_write_locks(&self) -> u64 {
386        self.account_keys()
387            .len()
388            .saturating_sub(self.num_readonly_accounts()) as u64
389    }
390
391    /// return detailed signature counts
392    pub fn get_signature_details(&self) -> TransactionSignatureDetails {
393        let mut transaction_signature_details = TransactionSignatureDetails {
394            num_transaction_signatures: u64::from(self.header().num_required_signatures),
395            ..TransactionSignatureDetails::default()
396        };
397
398        // counting the number of pre-processor operations separately
399        for (program_id, instruction) in self.program_instructions_iter() {
400            if secp256k1_program::check_id(program_id) {
401                if let Some(num_verifies) = instruction.data.first() {
402                    transaction_signature_details.num_secp256k1_instruction_signatures =
403                        transaction_signature_details
404                            .num_secp256k1_instruction_signatures
405                            .saturating_add(u64::from(*num_verifies));
406                }
407            } else if ed25519_program::check_id(program_id) {
408                if let Some(num_verifies) = instruction.data.first() {
409                    transaction_signature_details.num_ed25519_instruction_signatures =
410                        transaction_signature_details
411                            .num_ed25519_instruction_signatures
412                            .saturating_add(u64::from(*num_verifies));
413                }
414            } else if secp256r1_program::check_id(program_id) {
415                if let Some(num_verifies) = instruction.data.first() {
416                    transaction_signature_details.num_secp256r1_instruction_signatures =
417                        transaction_signature_details
418                            .num_secp256r1_instruction_signatures
419                            .saturating_add(u64::from(*num_verifies));
420                }
421            }
422        }
423
424        transaction_signature_details
425    }
426}
427
428/// Transaction signature details including the number of transaction signatures
429/// and precompile signatures.
430#[derive(Clone, Debug, Default)]
431pub struct TransactionSignatureDetails {
432    num_transaction_signatures: u64,
433    num_secp256k1_instruction_signatures: u64,
434    num_ed25519_instruction_signatures: u64,
435    num_secp256r1_instruction_signatures: u64,
436}
437
438impl TransactionSignatureDetails {
439    pub const fn new(
440        num_transaction_signatures: u64,
441        num_secp256k1_instruction_signatures: u64,
442        num_ed25519_instruction_signatures: u64,
443        num_secp256r1_instruction_signatures: u64,
444    ) -> Self {
445        Self {
446            num_transaction_signatures,
447            num_secp256k1_instruction_signatures,
448            num_ed25519_instruction_signatures,
449            num_secp256r1_instruction_signatures,
450        }
451    }
452
453    /// return total number of signature, treating pre-processor operations as signature
454    pub fn total_signatures(&self) -> u64 {
455        self.num_transaction_signatures
456            .saturating_add(self.num_secp256k1_instruction_signatures)
457            .saturating_add(self.num_ed25519_instruction_signatures)
458            .saturating_add(self.num_secp256r1_instruction_signatures)
459    }
460
461    /// return the number of transaction signatures
462    pub fn num_transaction_signatures(&self) -> u64 {
463        self.num_transaction_signatures
464    }
465
466    /// return the number of secp256k1 instruction signatures
467    pub fn num_secp256k1_instruction_signatures(&self) -> u64 {
468        self.num_secp256k1_instruction_signatures
469    }
470
471    /// return the number of ed25519 instruction signatures
472    pub fn num_ed25519_instruction_signatures(&self) -> u64 {
473        self.num_ed25519_instruction_signatures
474    }
475
476    /// return the number of secp256r1 instruction signatures
477    pub fn num_secp256r1_instruction_signatures(&self) -> u64 {
478        self.num_secp256r1_instruction_signatures
479    }
480}
481
482#[cfg(test)]
483mod tests {
484    use {super::*, crate::v0, std::collections::HashSet};
485
486    #[test]
487    fn test_try_from_legacy_message() {
488        let legacy_message_with_no_signers = legacy::Message {
489            account_keys: vec![Pubkey::new_unique()],
490            ..legacy::Message::default()
491        };
492
493        assert_eq!(
494            SanitizedMessage::try_from_legacy_message(
495                legacy_message_with_no_signers,
496                &HashSet::default(),
497            )
498            .err(),
499            Some(SanitizeMessageError::IndexOutOfBounds),
500        );
501    }
502
503    #[test]
504    fn test_is_non_loader_key() {
505        #![allow(deprecated)]
506        let key0 = Pubkey::new_unique();
507        let key1 = Pubkey::new_unique();
508        let loader_key = Pubkey::new_unique();
509        let instructions = vec![
510            CompiledInstruction::new(1, &(), vec![0]),
511            CompiledInstruction::new(2, &(), vec![0, 1]),
512        ];
513
514        let message = SanitizedMessage::try_from_legacy_message(
515            legacy::Message::new_with_compiled_instructions(
516                1,
517                0,
518                2,
519                vec![key0, key1, loader_key],
520                Hash::default(),
521                instructions,
522            ),
523            &HashSet::default(),
524        )
525        .unwrap();
526
527        assert!(message.is_non_loader_key(0));
528        assert!(message.is_non_loader_key(1));
529        assert!(!message.is_non_loader_key(2));
530    }
531
532    #[test]
533    fn test_num_readonly_accounts() {
534        let key0 = Pubkey::new_unique();
535        let key1 = Pubkey::new_unique();
536        let key2 = Pubkey::new_unique();
537        let key3 = Pubkey::new_unique();
538        let key4 = Pubkey::new_unique();
539        let key5 = Pubkey::new_unique();
540
541        let legacy_message = SanitizedMessage::try_from_legacy_message(
542            legacy::Message {
543                header: MessageHeader {
544                    num_required_signatures: 2,
545                    num_readonly_signed_accounts: 1,
546                    num_readonly_unsigned_accounts: 1,
547                },
548                account_keys: vec![key0, key1, key2, key3],
549                ..legacy::Message::default()
550            },
551            &HashSet::default(),
552        )
553        .unwrap();
554
555        assert_eq!(legacy_message.num_readonly_accounts(), 2);
556
557        let v0_message = SanitizedMessage::V0(v0::LoadedMessage::new(
558            v0::Message {
559                header: MessageHeader {
560                    num_required_signatures: 2,
561                    num_readonly_signed_accounts: 1,
562                    num_readonly_unsigned_accounts: 1,
563                },
564                account_keys: vec![key0, key1, key2, key3],
565                ..v0::Message::default()
566            },
567            LoadedAddresses {
568                writable: vec![key4],
569                readonly: vec![key5],
570            },
571            &HashSet::default(),
572        ));
573
574        assert_eq!(v0_message.num_readonly_accounts(), 3);
575    }
576
577    #[test]
578    fn test_get_ix_signers() {
579        let signer0 = Pubkey::new_unique();
580        let signer1 = Pubkey::new_unique();
581        let non_signer = Pubkey::new_unique();
582        let loader_key = Pubkey::new_unique();
583        let instructions = vec![
584            CompiledInstruction::new(3, &(), vec![2, 0]),
585            CompiledInstruction::new(3, &(), vec![0, 1]),
586            CompiledInstruction::new(3, &(), vec![0, 0]),
587        ];
588
589        let message = SanitizedMessage::try_from_legacy_message(
590            legacy::Message::new_with_compiled_instructions(
591                2,
592                1,
593                2,
594                vec![signer0, signer1, non_signer, loader_key],
595                Hash::default(),
596                instructions,
597            ),
598            &HashSet::default(),
599        )
600        .unwrap();
601
602        assert_eq!(
603            message.get_ix_signers(0).collect::<HashSet<_>>(),
604            HashSet::from_iter([&signer0])
605        );
606        assert_eq!(
607            message.get_ix_signers(1).collect::<HashSet<_>>(),
608            HashSet::from_iter([&signer0, &signer1])
609        );
610        assert_eq!(
611            message.get_ix_signers(2).collect::<HashSet<_>>(),
612            HashSet::from_iter([&signer0])
613        );
614        assert_eq!(
615            message.get_ix_signers(3).collect::<HashSet<_>>(),
616            HashSet::default()
617        );
618    }
619
620    #[test]
621    #[allow(clippy::get_first)]
622    fn test_is_writable_account_cache() {
623        let key0 = Pubkey::new_unique();
624        let key1 = Pubkey::new_unique();
625        let key2 = Pubkey::new_unique();
626        let key3 = Pubkey::new_unique();
627        let key4 = Pubkey::new_unique();
628        let key5 = Pubkey::new_unique();
629
630        let legacy_message = SanitizedMessage::try_from_legacy_message(
631            legacy::Message {
632                header: MessageHeader {
633                    num_required_signatures: 2,
634                    num_readonly_signed_accounts: 1,
635                    num_readonly_unsigned_accounts: 1,
636                },
637                account_keys: vec![key0, key1, key2, key3],
638                ..legacy::Message::default()
639            },
640            &HashSet::default(),
641        )
642        .unwrap();
643        match legacy_message {
644            SanitizedMessage::Legacy(message) => {
645                assert_eq!(
646                    message.is_writable_account_cache.len(),
647                    message.account_keys().len()
648                );
649                assert!(message.is_writable_account_cache.get(0).unwrap());
650                assert!(!message.is_writable_account_cache.get(1).unwrap());
651                assert!(message.is_writable_account_cache.get(2).unwrap());
652                assert!(!message.is_writable_account_cache.get(3).unwrap());
653            }
654            _ => {
655                panic!("Expect to be SanitizedMessage::LegacyMessage")
656            }
657        }
658
659        let v0_message = SanitizedMessage::V0(v0::LoadedMessage::new(
660            v0::Message {
661                header: MessageHeader {
662                    num_required_signatures: 2,
663                    num_readonly_signed_accounts: 1,
664                    num_readonly_unsigned_accounts: 1,
665                },
666                account_keys: vec![key0, key1, key2, key3],
667                ..v0::Message::default()
668            },
669            LoadedAddresses {
670                writable: vec![key4],
671                readonly: vec![key5],
672            },
673            &HashSet::default(),
674        ));
675        match v0_message {
676            SanitizedMessage::V0(message) => {
677                assert_eq!(
678                    message.is_writable_account_cache.len(),
679                    message.account_keys().len()
680                );
681                assert!(message.is_writable_account_cache.get(0).unwrap());
682                assert!(!message.is_writable_account_cache.get(1).unwrap());
683                assert!(message.is_writable_account_cache.get(2).unwrap());
684                assert!(!message.is_writable_account_cache.get(3).unwrap());
685                assert!(message.is_writable_account_cache.get(4).unwrap());
686                assert!(!message.is_writable_account_cache.get(5).unwrap());
687            }
688            _ => {
689                panic!("Expect to be SanitizedMessage::V0")
690            }
691        }
692    }
693
694    #[test]
695    fn test_get_signature_details() {
696        let key0 = Pubkey::new_unique();
697        let key1 = Pubkey::new_unique();
698        let loader_key = Pubkey::new_unique();
699
700        let loader_instr = CompiledInstruction::new(2, &(), vec![0, 1]);
701        let mock_secp256k1_instr = CompiledInstruction::new(3, &[1u8; 10], vec![]);
702        let mock_ed25519_instr = CompiledInstruction::new(4, &[5u8; 10], vec![]);
703
704        let message = SanitizedMessage::try_from_legacy_message(
705            legacy::Message::new_with_compiled_instructions(
706                2,
707                1,
708                2,
709                vec![
710                    key0,
711                    key1,
712                    loader_key,
713                    secp256k1_program::id(),
714                    ed25519_program::id(),
715                ],
716                Hash::default(),
717                vec![
718                    loader_instr,
719                    mock_secp256k1_instr.clone(),
720                    mock_ed25519_instr,
721                    mock_secp256k1_instr,
722                ],
723            ),
724            &HashSet::new(),
725        )
726        .unwrap();
727
728        let signature_details = message.get_signature_details();
729        // expect 2 required transaction signatures
730        assert_eq!(2, signature_details.num_transaction_signatures);
731        // expect 2 secp256k1 instruction signatures - 1 for each mock_secp2561k1_instr
732        assert_eq!(2, signature_details.num_secp256k1_instruction_signatures);
733        // expect 5 ed25519 instruction signatures from mock_ed25519_instr
734        assert_eq!(5, signature_details.num_ed25519_instruction_signatures);
735    }
736
737    #[test]
738    fn test_static_account_keys() {
739        let keys = vec![
740            Pubkey::new_unique(),
741            Pubkey::new_unique(),
742            Pubkey::new_unique(),
743        ];
744
745        let header = MessageHeader {
746            num_required_signatures: 2,
747            num_readonly_signed_accounts: 1,
748            num_readonly_unsigned_accounts: 1,
749        };
750
751        let legacy_message = SanitizedMessage::try_from_legacy_message(
752            legacy::Message {
753                header,
754                account_keys: keys.clone(),
755                ..legacy::Message::default()
756            },
757            &HashSet::default(),
758        )
759        .unwrap();
760        assert_eq!(legacy_message.static_account_keys(), &keys);
761
762        let v0_message = SanitizedMessage::V0(v0::LoadedMessage::new(
763            v0::Message {
764                header,
765                account_keys: keys.clone(),
766                ..v0::Message::default()
767            },
768            LoadedAddresses {
769                writable: vec![],
770                readonly: vec![],
771            },
772            &HashSet::default(),
773        ));
774        assert_eq!(v0_message.static_account_keys(), &keys);
775
776        let v0_message = SanitizedMessage::V0(v0::LoadedMessage::new(
777            v0::Message {
778                header,
779                account_keys: keys.clone(),
780                ..v0::Message::default()
781            },
782            LoadedAddresses {
783                writable: vec![Pubkey::new_unique()],
784                readonly: vec![Pubkey::new_unique()],
785            },
786            &HashSet::default(),
787        ));
788        assert_eq!(v0_message.static_account_keys(), &keys);
789    }
790}