miraland_program/message/
sanitized.rs

1use {
2    crate::{
3        ed25519_program,
4        hash::Hash,
5        instruction::CompiledInstruction,
6        message::{
7            legacy,
8            v0::{self, LoadedAddresses},
9            AccountKeys, AddressLoader, AddressLoaderError, MessageHeader,
10            SanitizedVersionedMessage, VersionedMessage,
11        },
12        nonce::NONCED_TX_MARKER_IX_INDEX,
13        program_utils::limited_deserialize,
14        pubkey::Pubkey,
15        sanitize::{Sanitize, SanitizeError},
16        secp256k1_program,
17        miraland_program::{system_instruction::SystemInstruction, system_program},
18        sysvar::instructions::{BorrowedAccountMeta, BorrowedInstruction},
19    },
20    std::{borrow::Cow, convert::TryFrom},
21    thiserror::Error,
22};
23
24#[derive(Debug, Clone, Eq, PartialEq)]
25pub struct LegacyMessage<'a> {
26    /// Legacy message
27    pub message: Cow<'a, legacy::Message>,
28    /// List of boolean with same length as account_keys(), each boolean value indicates if
29    /// corresponding account key is writable or not.
30    pub is_writable_account_cache: Vec<bool>,
31}
32
33impl<'a> LegacyMessage<'a> {
34    pub fn new(message: legacy::Message) -> Self {
35        let is_writable_account_cache = message
36            .account_keys
37            .iter()
38            .enumerate()
39            .map(|(i, _key)| message.is_writable(i))
40            .collect::<Vec<_>>();
41        Self {
42            message: Cow::Owned(message),
43            is_writable_account_cache,
44        }
45    }
46
47    pub fn has_duplicates(&self) -> bool {
48        self.message.has_duplicates()
49    }
50
51    pub fn is_key_called_as_program(&self, key_index: usize) -> bool {
52        self.message.is_key_called_as_program(key_index)
53    }
54
55    /// Inspect all message keys for the bpf upgradeable loader
56    pub fn is_upgradeable_loader_present(&self) -> bool {
57        self.message.is_upgradeable_loader_present()
58    }
59
60    /// Returns the full list of account keys.
61    pub fn account_keys(&self) -> AccountKeys {
62        AccountKeys::new(&self.message.account_keys, None)
63    }
64
65    pub fn is_writable(&self, index: usize) -> bool {
66        *self.is_writable_account_cache.get(index).unwrap_or(&false)
67    }
68}
69
70/// Sanitized message of a transaction.
71#[derive(Debug, Clone, Eq, PartialEq)]
72pub enum SanitizedMessage {
73    /// Sanitized legacy message
74    Legacy(LegacyMessage<'static>),
75    /// Sanitized version #0 message with dynamically loaded addresses
76    V0(v0::LoadedMessage<'static>),
77}
78
79#[derive(PartialEq, Debug, Error, Eq, Clone)]
80pub enum SanitizeMessageError {
81    #[error("index out of bounds")]
82    IndexOutOfBounds,
83    #[error("value out of bounds")]
84    ValueOutOfBounds,
85    #[error("invalid value")]
86    InvalidValue,
87    #[error("{0}")]
88    AddressLoaderError(#[from] AddressLoaderError),
89}
90
91impl From<SanitizeError> for SanitizeMessageError {
92    fn from(err: SanitizeError) -> Self {
93        match err {
94            SanitizeError::IndexOutOfBounds => Self::IndexOutOfBounds,
95            SanitizeError::ValueOutOfBounds => Self::ValueOutOfBounds,
96            SanitizeError::InvalidValue => Self::InvalidValue,
97        }
98    }
99}
100
101impl TryFrom<legacy::Message> for SanitizedMessage {
102    type Error = SanitizeMessageError;
103    fn try_from(message: legacy::Message) -> Result<Self, Self::Error> {
104        message.sanitize()?;
105        Ok(Self::Legacy(LegacyMessage::new(message)))
106    }
107}
108
109impl SanitizedMessage {
110    /// Create a sanitized message from a sanitized versioned message.
111    /// If the input message uses address tables, attempt to look up the
112    /// address for each table index.
113    pub fn try_new(
114        sanitized_msg: SanitizedVersionedMessage,
115        address_loader: impl AddressLoader,
116    ) -> Result<Self, SanitizeMessageError> {
117        Ok(match sanitized_msg.message {
118            VersionedMessage::Legacy(message) => {
119                SanitizedMessage::Legacy(LegacyMessage::new(message))
120            }
121            VersionedMessage::V0(message) => {
122                let loaded_addresses =
123                    address_loader.load_addresses(&message.address_table_lookups)?;
124                SanitizedMessage::V0(v0::LoadedMessage::new(message, loaded_addresses))
125            }
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        }
135    }
136
137    /// Message header which identifies the number of signer and writable or
138    /// readonly accounts
139    pub fn header(&self) -> &MessageHeader {
140        match self {
141            Self::Legacy(legacy_message) => &legacy_message.message.header,
142            Self::V0(loaded_msg) => &loaded_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) -> &Pubkey {
157        self.account_keys()
158            .get(0)
159            .expect("sanitized message always has non-program 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        }
168    }
169
170    /// Program instructions that will be executed in sequence and committed in
171    /// one atomic transaction if all succeed.
172    pub fn instructions(&self) -> &[CompiledInstruction] {
173        match self {
174            Self::Legacy(legacy_message) => &legacy_message.message.instructions,
175            Self::V0(loaded_msg) => &loaded_msg.message.instructions,
176        }
177    }
178
179    /// Program instructions iterator which includes each instruction's program
180    /// id.
181    pub fn program_instructions_iter(
182        &self,
183    ) -> impl Iterator<Item = (&Pubkey, &CompiledInstruction)> {
184        self.instructions().iter().map(move |ix| {
185            (
186                self.account_keys()
187                    .get(usize::from(ix.program_id_index))
188                    .expect("program id index is sanitized"),
189                ix,
190            )
191        })
192    }
193
194    /// Returns the list of account keys that are loaded for this message.
195    pub fn account_keys(&self) -> AccountKeys {
196        match self {
197            Self::Legacy(message) => message.account_keys(),
198            Self::V0(message) => message.account_keys(),
199        }
200    }
201
202    /// Returns the list of account keys used for account lookup tables.
203    pub fn message_address_table_lookups(&self) -> &[v0::MessageAddressTableLookup] {
204        match self {
205            Self::Legacy(_message) => &[],
206            Self::V0(message) => &message.message.address_table_lookups,
207        }
208    }
209
210    /// Returns true if the account at the specified index is an input to some
211    /// program instruction in this message.
212    fn is_key_passed_to_program(&self, key_index: usize) -> bool {
213        if let Ok(key_index) = u8::try_from(key_index) {
214            self.instructions()
215                .iter()
216                .any(|ix| ix.accounts.contains(&key_index))
217        } else {
218            false
219        }
220    }
221
222    /// Returns true if the account at the specified index is invoked as a
223    /// program in this message.
224    pub fn is_invoked(&self, key_index: usize) -> bool {
225        match self {
226            Self::Legacy(message) => message.is_key_called_as_program(key_index),
227            Self::V0(message) => message.is_key_called_as_program(key_index),
228        }
229    }
230
231    /// Returns true if the account at the specified index is not invoked as a
232    /// program or, if invoked, is passed to a program.
233    pub fn is_non_loader_key(&self, key_index: usize) -> bool {
234        !self.is_invoked(key_index) || self.is_key_passed_to_program(key_index)
235    }
236
237    /// Returns true if the account at the specified index is writable by the
238    /// instructions in this message.
239    pub fn is_writable(&self, index: usize) -> bool {
240        match self {
241            Self::Legacy(message) => message.is_writable(index),
242            Self::V0(message) => message.is_writable(index),
243        }
244    }
245
246    /// Returns true if the account at the specified index signed this
247    /// message.
248    pub fn is_signer(&self, index: usize) -> bool {
249        index < usize::from(self.header().num_required_signatures)
250    }
251
252    /// Return the resolved addresses for this message if it has any.
253    fn loaded_lookup_table_addresses(&self) -> Option<&LoadedAddresses> {
254        match &self {
255            SanitizedMessage::V0(message) => Some(&message.loaded_addresses),
256            _ => None,
257        }
258    }
259
260    /// Return the number of readonly accounts loaded by this message.
261    pub fn num_readonly_accounts(&self) -> usize {
262        let loaded_readonly_addresses = self
263            .loaded_lookup_table_addresses()
264            .map(|keys| keys.readonly.len())
265            .unwrap_or_default();
266        loaded_readonly_addresses
267            .saturating_add(usize::from(self.header().num_readonly_signed_accounts))
268            .saturating_add(usize::from(self.header().num_readonly_unsigned_accounts))
269    }
270
271    /// Decompile message instructions without cloning account keys
272    pub fn decompile_instructions(&self) -> Vec<BorrowedInstruction> {
273        let account_keys = self.account_keys();
274        self.program_instructions_iter()
275            .map(|(program_id, instruction)| {
276                let accounts = instruction
277                    .accounts
278                    .iter()
279                    .map(|account_index| {
280                        let account_index = *account_index as usize;
281                        BorrowedAccountMeta {
282                            is_signer: self.is_signer(account_index),
283                            is_writable: self.is_writable(account_index),
284                            pubkey: account_keys.get(account_index).unwrap(),
285                        }
286                    })
287                    .collect();
288
289                BorrowedInstruction {
290                    accounts,
291                    data: &instruction.data,
292                    program_id,
293                }
294            })
295            .collect()
296    }
297
298    /// Inspect all message keys for the bpf upgradeable loader
299    pub fn is_upgradeable_loader_present(&self) -> bool {
300        match self {
301            Self::Legacy(message) => message.is_upgradeable_loader_present(),
302            Self::V0(message) => message.is_upgradeable_loader_present(),
303        }
304    }
305
306    /// Get a list of signers for the instruction at the given index
307    pub fn get_ix_signers(&self, ix_index: usize) -> impl Iterator<Item = &Pubkey> {
308        self.instructions()
309            .get(ix_index)
310            .into_iter()
311            .flat_map(|ix| {
312                ix.accounts
313                    .iter()
314                    .copied()
315                    .map(usize::from)
316                    .filter(|index| self.is_signer(*index))
317                    .filter_map(|signer_index| self.account_keys().get(signer_index))
318            })
319    }
320
321    /// If the message uses a durable nonce, return the pubkey of the nonce account
322    pub fn get_durable_nonce(&self) -> Option<&Pubkey> {
323        self.instructions()
324            .get(NONCED_TX_MARKER_IX_INDEX as usize)
325            .filter(
326                |ix| match self.account_keys().get(ix.program_id_index as usize) {
327                    Some(program_id) => system_program::check_id(program_id),
328                    _ => false,
329                },
330            )
331            .filter(|ix| {
332                matches!(
333                    limited_deserialize(&ix.data, 4 /* serialized size of AdvanceNonceAccount */),
334                    Ok(SystemInstruction::AdvanceNonceAccount)
335                )
336            })
337            .and_then(|ix| {
338                ix.accounts.first().and_then(|idx| {
339                    let idx = *idx as usize;
340                    if !self.is_writable(idx) {
341                        None
342                    } else {
343                        self.account_keys().get(idx)
344                    }
345                })
346            })
347    }
348
349    pub fn num_signatures(&self) -> u64 {
350        let mut num_signatures = u64::from(self.header().num_required_signatures);
351        // This next part is really calculating the number of pre-processor
352        // operations being done and treating them like a signature
353        for (program_id, instruction) in self.program_instructions_iter() {
354            if secp256k1_program::check_id(program_id) || ed25519_program::check_id(program_id) {
355                if let Some(num_verifies) = instruction.data.first() {
356                    num_signatures = num_signatures.saturating_add(u64::from(*num_verifies));
357                }
358            }
359        }
360        num_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
372#[cfg(test)]
373mod tests {
374    use {super::*, crate::message::v0, std::collections::HashSet};
375
376    #[test]
377    fn test_try_from_message() {
378        let legacy_message_with_no_signers = legacy::Message {
379            account_keys: vec![Pubkey::new_unique()],
380            ..legacy::Message::default()
381        };
382
383        assert_eq!(
384            SanitizedMessage::try_from(legacy_message_with_no_signers).err(),
385            Some(SanitizeMessageError::IndexOutOfBounds),
386        );
387    }
388
389    #[test]
390    fn test_is_non_loader_key() {
391        let key0 = Pubkey::new_unique();
392        let key1 = Pubkey::new_unique();
393        let loader_key = Pubkey::new_unique();
394        let instructions = vec![
395            CompiledInstruction::new(1, &(), vec![0]),
396            CompiledInstruction::new(2, &(), vec![0, 1]),
397        ];
398
399        let message = SanitizedMessage::try_from(legacy::Message::new_with_compiled_instructions(
400            1,
401            0,
402            2,
403            vec![key0, key1, loader_key],
404            Hash::default(),
405            instructions,
406        ))
407        .unwrap();
408
409        assert!(message.is_non_loader_key(0));
410        assert!(message.is_non_loader_key(1));
411        assert!(!message.is_non_loader_key(2));
412    }
413
414    #[test]
415    fn test_num_readonly_accounts() {
416        let key0 = Pubkey::new_unique();
417        let key1 = Pubkey::new_unique();
418        let key2 = Pubkey::new_unique();
419        let key3 = Pubkey::new_unique();
420        let key4 = Pubkey::new_unique();
421        let key5 = Pubkey::new_unique();
422
423        let legacy_message = SanitizedMessage::try_from(legacy::Message {
424            header: MessageHeader {
425                num_required_signatures: 2,
426                num_readonly_signed_accounts: 1,
427                num_readonly_unsigned_accounts: 1,
428            },
429            account_keys: vec![key0, key1, key2, key3],
430            ..legacy::Message::default()
431        })
432        .unwrap();
433
434        assert_eq!(legacy_message.num_readonly_accounts(), 2);
435
436        let v0_message = SanitizedMessage::V0(v0::LoadedMessage::new(
437            v0::Message {
438                header: MessageHeader {
439                    num_required_signatures: 2,
440                    num_readonly_signed_accounts: 1,
441                    num_readonly_unsigned_accounts: 1,
442                },
443                account_keys: vec![key0, key1, key2, key3],
444                ..v0::Message::default()
445            },
446            LoadedAddresses {
447                writable: vec![key4],
448                readonly: vec![key5],
449            },
450        ));
451
452        assert_eq!(v0_message.num_readonly_accounts(), 3);
453    }
454
455    #[test]
456    fn test_get_ix_signers() {
457        let signer0 = Pubkey::new_unique();
458        let signer1 = Pubkey::new_unique();
459        let non_signer = Pubkey::new_unique();
460        let loader_key = Pubkey::new_unique();
461        let instructions = vec![
462            CompiledInstruction::new(3, &(), vec![2, 0]),
463            CompiledInstruction::new(3, &(), vec![0, 1]),
464            CompiledInstruction::new(3, &(), vec![0, 0]),
465        ];
466
467        let message = SanitizedMessage::try_from(legacy::Message::new_with_compiled_instructions(
468            2,
469            1,
470            2,
471            vec![signer0, signer1, non_signer, loader_key],
472            Hash::default(),
473            instructions,
474        ))
475        .unwrap();
476
477        assert_eq!(
478            message.get_ix_signers(0).collect::<HashSet<_>>(),
479            HashSet::from_iter([&signer0])
480        );
481        assert_eq!(
482            message.get_ix_signers(1).collect::<HashSet<_>>(),
483            HashSet::from_iter([&signer0, &signer1])
484        );
485        assert_eq!(
486            message.get_ix_signers(2).collect::<HashSet<_>>(),
487            HashSet::from_iter([&signer0])
488        );
489        assert_eq!(
490            message.get_ix_signers(3).collect::<HashSet<_>>(),
491            HashSet::default()
492        );
493    }
494
495    #[test]
496    #[allow(clippy::get_first)]
497    fn test_is_writable_account_cache() {
498        let key0 = Pubkey::new_unique();
499        let key1 = Pubkey::new_unique();
500        let key2 = Pubkey::new_unique();
501        let key3 = Pubkey::new_unique();
502        let key4 = Pubkey::new_unique();
503        let key5 = Pubkey::new_unique();
504
505        let legacy_message = SanitizedMessage::try_from(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        .unwrap();
515        match legacy_message {
516            SanitizedMessage::Legacy(message) => {
517                assert_eq!(
518                    message.is_writable_account_cache.len(),
519                    message.account_keys().len()
520                );
521                assert!(message.is_writable_account_cache.get(0).unwrap());
522                assert!(!message.is_writable_account_cache.get(1).unwrap());
523                assert!(message.is_writable_account_cache.get(2).unwrap());
524                assert!(!message.is_writable_account_cache.get(3).unwrap());
525            }
526            _ => {
527                panic!("Expect to be SanitizedMessage::LegacyMessage")
528            }
529        }
530
531        let v0_message = SanitizedMessage::V0(v0::LoadedMessage::new(
532            v0::Message {
533                header: MessageHeader {
534                    num_required_signatures: 2,
535                    num_readonly_signed_accounts: 1,
536                    num_readonly_unsigned_accounts: 1,
537                },
538                account_keys: vec![key0, key1, key2, key3],
539                ..v0::Message::default()
540            },
541            LoadedAddresses {
542                writable: vec![key4],
543                readonly: vec![key5],
544            },
545        ));
546        match v0_message {
547            SanitizedMessage::V0(message) => {
548                assert_eq!(
549                    message.is_writable_account_cache.len(),
550                    message.account_keys().len()
551                );
552                assert!(message.is_writable_account_cache.get(0).unwrap());
553                assert!(!message.is_writable_account_cache.get(1).unwrap());
554                assert!(message.is_writable_account_cache.get(2).unwrap());
555                assert!(!message.is_writable_account_cache.get(3).unwrap());
556                assert!(message.is_writable_account_cache.get(4).unwrap());
557                assert!(!message.is_writable_account_cache.get(5).unwrap());
558            }
559            _ => {
560                panic!("Expect to be SanitizedMessage::V0")
561            }
562        }
563    }
564}