Skip to main content

solana_message/
sanitized.rs

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