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    pub fn is_instruction_account(&self, key_index: usize) -> bool {
228        if let Ok(key_index) = u8::try_from(key_index) {
229            self.instructions()
230                .iter()
231                .any(|ix| ix.accounts.contains(&key_index))
232        } else {
233            false
234        }
235    }
236
237    /// Returns true if the account at the specified index is invoked as a
238    /// program in this message.
239    pub fn is_invoked(&self, key_index: usize) -> bool {
240        match self {
241            Self::Legacy(message) => message.is_key_called_as_program(key_index),
242            Self::V0(message) => message.is_key_called_as_program(key_index),
243            Self::V1(message) => message.is_key_called_as_program(key_index),
244        }
245    }
246
247    /// Returns true if the account at the specified index is writable by the
248    /// instructions in this message.
249    pub fn is_writable(&self, index: usize) -> bool {
250        match self {
251            Self::Legacy(message) => message.is_writable(index),
252            Self::V0(message) => message.is_writable(index),
253            Self::V1(message) => message.is_writable(index),
254        }
255    }
256
257    /// Returns true if the account at the specified index signed this
258    /// message.
259    pub fn is_signer(&self, index: usize) -> bool {
260        index < usize::from(self.header().num_required_signatures)
261    }
262
263    /// Return the resolved addresses for this message if it has any.
264    fn loaded_lookup_table_addresses(&self) -> Option<&LoadedAddresses> {
265        match &self {
266            SanitizedMessage::V0(message) => Some(&message.loaded_addresses),
267            _ => None,
268        }
269    }
270
271    /// Return the number of readonly accounts loaded by this message.
272    pub fn num_readonly_accounts(&self) -> usize {
273        let loaded_readonly_addresses = self
274            .loaded_lookup_table_addresses()
275            .map(|keys| keys.readonly.len())
276            .unwrap_or_default();
277        loaded_readonly_addresses
278            .saturating_add(usize::from(self.header().num_readonly_signed_accounts))
279            .saturating_add(usize::from(self.header().num_readonly_unsigned_accounts))
280    }
281
282    /// Decompile message instructions without cloning account keys
283    pub fn decompile_instructions(&self) -> Vec<BorrowedInstruction<'_>> {
284        let account_keys = self.account_keys();
285        self.program_instructions_iter()
286            .map(|(program_id, instruction)| {
287                let accounts = instruction
288                    .accounts
289                    .iter()
290                    .map(|account_index| {
291                        let account_index = *account_index as usize;
292                        BorrowedAccountMeta {
293                            is_signer: self.is_signer(account_index),
294                            is_writable: self.is_writable(account_index),
295                            pubkey: account_keys.get(account_index).unwrap(),
296                        }
297                    })
298                    .collect();
299
300                BorrowedInstruction {
301                    accounts,
302                    data: &instruction.data,
303                    program_id,
304                }
305            })
306            .collect()
307    }
308
309    /// Inspect all message keys for the bpf upgradeable loader
310    pub fn is_upgradeable_loader_present(&self) -> bool {
311        match self {
312            Self::Legacy(message) => message.is_upgradeable_loader_present(),
313            Self::V0(message) => message.is_upgradeable_loader_present(),
314            Self::V1(message) => message.is_upgradeable_loader_present(),
315        }
316    }
317
318    /// Get a list of signers for the instruction at the given index
319    pub fn get_ix_signers(&self, ix_index: usize) -> impl Iterator<Item = &Address> {
320        self.instructions()
321            .get(ix_index)
322            .into_iter()
323            .flat_map(|ix| {
324                ix.accounts
325                    .iter()
326                    .copied()
327                    .map(usize::from)
328                    .filter(|index| self.is_signer(*index))
329                    .filter_map(|signer_index| self.account_keys().get(signer_index))
330            })
331    }
332
333    /// If the message uses a durable nonce, return the pubkey of the nonce account
334    pub fn get_durable_nonce(&self) -> Option<&Address> {
335        self.instructions()
336            .get(NONCED_TX_MARKER_IX_INDEX as usize)
337            .filter(
338                |ix| match self.account_keys().get(ix.program_id_index as usize) {
339                    Some(program_id) => solana_sdk_ids::system_program::check_id(program_id),
340                    _ => false,
341                },
342            )
343            .filter(|ix| crate::inline_nonce::is_advance_nonce_instruction_data(&ix.data))
344            .and_then(|ix| {
345                ix.accounts.first().and_then(|idx| {
346                    let idx = *idx as usize;
347                    if !self.is_writable(idx) {
348                        None
349                    } else {
350                        self.account_keys().get(idx)
351                    }
352                })
353            })
354    }
355
356    /// Returns the total number of signatures in the message.
357    /// This includes required transaction signatures as well as any
358    /// pre-compile signatures that are attached in instructions.
359    pub fn num_total_signatures(&self) -> u64 {
360        self.get_signature_details().total_signatures()
361    }
362
363    /// Returns the number of requested write-locks in this message.
364    /// This does not consider if write-locks are demoted.
365    pub fn num_write_locks(&self) -> u64 {
366        self.account_keys()
367            .len()
368            .saturating_sub(self.num_readonly_accounts()) as u64
369    }
370
371    /// return detailed signature counts
372    pub fn get_signature_details(&self) -> TransactionSignatureDetails {
373        let mut transaction_signature_details = TransactionSignatureDetails {
374            num_transaction_signatures: u64::from(self.header().num_required_signatures),
375            ..TransactionSignatureDetails::default()
376        };
377
378        // counting the number of pre-processor operations separately
379        for (program_id, instruction) in self.program_instructions_iter() {
380            if secp256k1_program::check_id(program_id) {
381                if let Some(num_verifies) = instruction.data.first() {
382                    transaction_signature_details.num_secp256k1_instruction_signatures =
383                        transaction_signature_details
384                            .num_secp256k1_instruction_signatures
385                            .saturating_add(u64::from(*num_verifies));
386                }
387            } else if ed25519_program::check_id(program_id) {
388                if let Some(num_verifies) = instruction.data.first() {
389                    transaction_signature_details.num_ed25519_instruction_signatures =
390                        transaction_signature_details
391                            .num_ed25519_instruction_signatures
392                            .saturating_add(u64::from(*num_verifies));
393                }
394            } else if secp256r1_program::check_id(program_id) {
395                if let Some(num_verifies) = instruction.data.first() {
396                    transaction_signature_details.num_secp256r1_instruction_signatures =
397                        transaction_signature_details
398                            .num_secp256r1_instruction_signatures
399                            .saturating_add(u64::from(*num_verifies));
400                }
401            }
402        }
403
404        transaction_signature_details
405    }
406}
407
408/// Transaction signature details including the number of transaction signatures
409/// and precompile signatures.
410#[derive(Clone, Debug, Default)]
411pub struct TransactionSignatureDetails {
412    num_transaction_signatures: u64,
413    num_secp256k1_instruction_signatures: u64,
414    num_ed25519_instruction_signatures: u64,
415    num_secp256r1_instruction_signatures: u64,
416}
417
418impl TransactionSignatureDetails {
419    pub const fn new(
420        num_transaction_signatures: u64,
421        num_secp256k1_instruction_signatures: u64,
422        num_ed25519_instruction_signatures: u64,
423        num_secp256r1_instruction_signatures: u64,
424    ) -> Self {
425        Self {
426            num_transaction_signatures,
427            num_secp256k1_instruction_signatures,
428            num_ed25519_instruction_signatures,
429            num_secp256r1_instruction_signatures,
430        }
431    }
432
433    /// return total number of signature, treating pre-processor operations as signature
434    pub fn total_signatures(&self) -> u64 {
435        self.num_transaction_signatures
436            .saturating_add(self.num_secp256k1_instruction_signatures)
437            .saturating_add(self.num_ed25519_instruction_signatures)
438            .saturating_add(self.num_secp256r1_instruction_signatures)
439    }
440
441    /// return the number of transaction signatures
442    pub fn num_transaction_signatures(&self) -> u64 {
443        self.num_transaction_signatures
444    }
445
446    /// return the number of secp256k1 instruction signatures
447    pub fn num_secp256k1_instruction_signatures(&self) -> u64 {
448        self.num_secp256k1_instruction_signatures
449    }
450
451    /// return the number of ed25519 instruction signatures
452    pub fn num_ed25519_instruction_signatures(&self) -> u64 {
453        self.num_ed25519_instruction_signatures
454    }
455
456    /// return the number of secp256r1 instruction signatures
457    pub fn num_secp256r1_instruction_signatures(&self) -> u64 {
458        self.num_secp256r1_instruction_signatures
459    }
460}
461
462#[cfg(test)]
463mod tests {
464    use {super::*, crate::v0, alloc::vec, std::collections::HashSet};
465
466    #[test]
467    fn test_try_from_legacy_message() {
468        let legacy_message_with_no_signers = legacy::Message {
469            account_keys: vec![Address::new_unique()],
470            ..legacy::Message::default()
471        };
472
473        assert_eq!(
474            SanitizedMessage::try_from_legacy_message(
475                legacy_message_with_no_signers,
476                &HashSet::default(),
477            )
478            .err(),
479            Some(SanitizeMessageError::IndexOutOfBounds),
480        );
481    }
482
483    #[test]
484    fn test_num_readonly_accounts() {
485        let key0 = Address::new_unique();
486        let key1 = Address::new_unique();
487        let key2 = Address::new_unique();
488        let key3 = Address::new_unique();
489        let key4 = Address::new_unique();
490        let key5 = Address::new_unique();
491
492        let legacy_message = SanitizedMessage::try_from_legacy_message(
493            legacy::Message {
494                header: MessageHeader {
495                    num_required_signatures: 2,
496                    num_readonly_signed_accounts: 1,
497                    num_readonly_unsigned_accounts: 1,
498                },
499                account_keys: vec![key0, key1, key2, key3],
500                ..legacy::Message::default()
501            },
502            &HashSet::default(),
503        )
504        .unwrap();
505
506        assert_eq!(legacy_message.num_readonly_accounts(), 2);
507
508        let v0_message = SanitizedMessage::V0(v0::LoadedMessage::new(
509            v0::Message {
510                header: MessageHeader {
511                    num_required_signatures: 2,
512                    num_readonly_signed_accounts: 1,
513                    num_readonly_unsigned_accounts: 1,
514                },
515                account_keys: vec![key0, key1, key2, key3],
516                ..v0::Message::default()
517            },
518            LoadedAddresses {
519                writable: vec![key4],
520                readonly: vec![key5],
521            },
522            &HashSet::default(),
523        ));
524
525        assert_eq!(v0_message.num_readonly_accounts(), 3);
526    }
527
528    #[test]
529    fn test_get_ix_signers() {
530        let signer0 = Address::new_unique();
531        let signer1 = Address::new_unique();
532        let non_signer = Address::new_unique();
533        let loader_key = Address::new_unique();
534        let instructions = vec![
535            CompiledInstruction::new(3, &(), vec![2, 0]),
536            CompiledInstruction::new(3, &(), vec![0, 1]),
537            CompiledInstruction::new(3, &(), vec![0, 0]),
538        ];
539
540        let message = SanitizedMessage::try_from_legacy_message(
541            legacy::Message::new_with_compiled_instructions(
542                2,
543                1,
544                2,
545                vec![signer0, signer1, non_signer, loader_key],
546                Hash::default(),
547                instructions,
548            ),
549            &HashSet::default(),
550        )
551        .unwrap();
552
553        assert_eq!(
554            message.get_ix_signers(0).collect::<HashSet<_>>(),
555            HashSet::from_iter([&signer0])
556        );
557        assert_eq!(
558            message.get_ix_signers(1).collect::<HashSet<_>>(),
559            HashSet::from_iter([&signer0, &signer1])
560        );
561        assert_eq!(
562            message.get_ix_signers(2).collect::<HashSet<_>>(),
563            HashSet::from_iter([&signer0])
564        );
565        assert_eq!(
566            message.get_ix_signers(3).collect::<HashSet<_>>(),
567            HashSet::default()
568        );
569    }
570
571    #[test]
572    #[allow(clippy::get_first)]
573    fn test_is_writable_account_cache() {
574        let key0 = Address::new_unique();
575        let key1 = Address::new_unique();
576        let key2 = Address::new_unique();
577        let key3 = Address::new_unique();
578        let key4 = Address::new_unique();
579        let key5 = Address::new_unique();
580
581        let legacy_message = SanitizedMessage::try_from_legacy_message(
582            legacy::Message {
583                header: MessageHeader {
584                    num_required_signatures: 2,
585                    num_readonly_signed_accounts: 1,
586                    num_readonly_unsigned_accounts: 1,
587                },
588                account_keys: vec![key0, key1, key2, key3],
589                ..legacy::Message::default()
590            },
591            &HashSet::default(),
592        )
593        .unwrap();
594        match legacy_message {
595            SanitizedMessage::Legacy(message) => {
596                assert_eq!(
597                    message.is_writable_account_cache.len(),
598                    message.account_keys().len()
599                );
600                assert!(message.is_writable_account_cache.get(0).unwrap());
601                assert!(!message.is_writable_account_cache.get(1).unwrap());
602                assert!(message.is_writable_account_cache.get(2).unwrap());
603                assert!(!message.is_writable_account_cache.get(3).unwrap());
604            }
605            _ => {
606                panic!("Expect to be SanitizedMessage::LegacyMessage")
607            }
608        }
609
610        let v0_message = SanitizedMessage::V0(v0::LoadedMessage::new(
611            v0::Message {
612                header: MessageHeader {
613                    num_required_signatures: 2,
614                    num_readonly_signed_accounts: 1,
615                    num_readonly_unsigned_accounts: 1,
616                },
617                account_keys: vec![key0, key1, key2, key3],
618                ..v0::Message::default()
619            },
620            LoadedAddresses {
621                writable: vec![key4],
622                readonly: vec![key5],
623            },
624            &HashSet::default(),
625        ));
626        match v0_message {
627            SanitizedMessage::V0(message) => {
628                assert_eq!(
629                    message.is_writable_account_cache.len(),
630                    message.account_keys().len()
631                );
632                assert!(message.is_writable_account_cache.get(0).unwrap());
633                assert!(!message.is_writable_account_cache.get(1).unwrap());
634                assert!(message.is_writable_account_cache.get(2).unwrap());
635                assert!(!message.is_writable_account_cache.get(3).unwrap());
636                assert!(message.is_writable_account_cache.get(4).unwrap());
637                assert!(!message.is_writable_account_cache.get(5).unwrap());
638            }
639            _ => {
640                panic!("Expect to be SanitizedMessage::V0")
641            }
642        }
643    }
644
645    #[test]
646    fn test_get_signature_details() {
647        let key0 = Address::new_unique();
648        let key1 = Address::new_unique();
649        let loader_key = Address::new_unique();
650
651        let loader_instr = CompiledInstruction::new(2, &(), vec![0, 1]);
652        let mock_secp256k1_instr = CompiledInstruction::new(3, &[1u8; 10], vec![]);
653        let mock_ed25519_instr = CompiledInstruction::new(4, &[5u8; 10], vec![]);
654
655        let message = SanitizedMessage::try_from_legacy_message(
656            legacy::Message::new_with_compiled_instructions(
657                2,
658                1,
659                2,
660                vec![
661                    key0,
662                    key1,
663                    loader_key,
664                    secp256k1_program::id(),
665                    ed25519_program::id(),
666                ],
667                Hash::default(),
668                vec![
669                    loader_instr,
670                    mock_secp256k1_instr.clone(),
671                    mock_ed25519_instr,
672                    mock_secp256k1_instr,
673                ],
674            ),
675            &HashSet::new(),
676        )
677        .unwrap();
678
679        let signature_details = message.get_signature_details();
680        // expect 2 required transaction signatures
681        assert_eq!(2, signature_details.num_transaction_signatures);
682        // expect 2 secp256k1 instruction signatures - 1 for each mock_secp2561k1_instr
683        assert_eq!(2, signature_details.num_secp256k1_instruction_signatures);
684        // expect 5 ed25519 instruction signatures from mock_ed25519_instr
685        assert_eq!(5, signature_details.num_ed25519_instruction_signatures);
686    }
687
688    #[test]
689    fn test_static_account_keys() {
690        let keys = vec![
691            Address::new_unique(),
692            Address::new_unique(),
693            Address::new_unique(),
694        ];
695
696        let header = MessageHeader {
697            num_required_signatures: 2,
698            num_readonly_signed_accounts: 1,
699            num_readonly_unsigned_accounts: 1,
700        };
701
702        let legacy_message = SanitizedMessage::try_from_legacy_message(
703            legacy::Message {
704                header,
705                account_keys: keys.clone(),
706                ..legacy::Message::default()
707            },
708            &HashSet::default(),
709        )
710        .unwrap();
711        assert_eq!(legacy_message.static_account_keys(), &keys);
712
713        let v0_message = SanitizedMessage::V0(v0::LoadedMessage::new(
714            v0::Message {
715                header,
716                account_keys: keys.clone(),
717                ..v0::Message::default()
718            },
719            LoadedAddresses {
720                writable: vec![],
721                readonly: vec![],
722            },
723            &HashSet::default(),
724        ));
725        assert_eq!(v0_message.static_account_keys(), &keys);
726
727        let v0_message = SanitizedMessage::V0(v0::LoadedMessage::new(
728            v0::Message {
729                header,
730                account_keys: keys.clone(),
731                ..v0::Message::default()
732            },
733            LoadedAddresses {
734                writable: vec![Address::new_unique()],
735                readonly: vec![Address::new_unique()],
736            },
737            &HashSet::default(),
738        ));
739        assert_eq!(v0_message.static_account_keys(), &keys);
740    }
741}