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