miraland_program/message/versions/v0/
mod.rs

1//! A future Miraland message format.
2//!
3//! This crate defines two versions of `Message` in their own modules:
4//! [`legacy`] and [`v0`]. `legacy` is the current version as of Miraland 1.10.0.
5//! `v0` is a [future message format] that encodes more account keys into a
6//! transaction than the legacy format.
7//!
8//! [`legacy`]: crate::message::legacy
9//! [`v0`]: crate::message::v0
10//! [future message format]: https://docs.solana.com/proposals/versioned-transactions
11
12use crate::{
13    address_lookup_table_account::AddressLookupTableAccount,
14    bpf_loader_upgradeable,
15    hash::Hash,
16    instruction::{CompiledInstruction, Instruction},
17    message::{
18        compiled_keys::{CompileError, CompiledKeys},
19        legacy::is_builtin_key_or_sysvar,
20        AccountKeys, MessageHeader, MESSAGE_VERSION_PREFIX,
21    },
22    pubkey::Pubkey,
23    sanitize::SanitizeError,
24    short_vec,
25};
26pub use loaded::*;
27
28mod loaded;
29
30/// Address table lookups describe an on-chain address lookup table to use
31/// for loading more readonly and writable accounts in a single tx.
32#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq, Clone, AbiExample)]
33#[serde(rename_all = "camelCase")]
34pub struct MessageAddressTableLookup {
35    /// Address lookup table account key
36    pub account_key: Pubkey,
37    /// List of indexes used to load writable account addresses
38    #[serde(with = "short_vec")]
39    pub writable_indexes: Vec<u8>,
40    /// List of indexes used to load readonly account addresses
41    #[serde(with = "short_vec")]
42    pub readonly_indexes: Vec<u8>,
43}
44
45/// A Miraland transaction message (v0).
46///
47/// This message format supports succinct account loading with
48/// on-chain address lookup tables.
49///
50/// See the [`message`] module documentation for further description.
51///
52/// [`message`]: crate::message
53#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq, Clone, AbiExample)]
54#[serde(rename_all = "camelCase")]
55pub struct Message {
56    /// The message header, identifying signed and read-only `account_keys`.
57    /// Header values only describe static `account_keys`, they do not describe
58    /// any additional account keys loaded via address table lookups.
59    pub header: MessageHeader,
60
61    /// List of accounts loaded by this transaction.
62    #[serde(with = "short_vec")]
63    pub account_keys: Vec<Pubkey>,
64
65    /// The blockhash of a recent block.
66    pub recent_blockhash: Hash,
67
68    /// Instructions that invoke a designated program, are executed in sequence,
69    /// and committed in one atomic transaction if all succeed.
70    ///
71    /// # Notes
72    ///
73    /// Program indexes must index into the list of message `account_keys` because
74    /// program id's cannot be dynamically loaded from a lookup table.
75    ///
76    /// Account indexes must index into the list of addresses
77    /// constructed from the concatenation of three key lists:
78    ///   1) message `account_keys`
79    ///   2) ordered list of keys loaded from `writable` lookup table indexes
80    ///   3) ordered list of keys loaded from `readable` lookup table indexes
81    #[serde(with = "short_vec")]
82    pub instructions: Vec<CompiledInstruction>,
83
84    /// List of address table lookups used to load additional accounts
85    /// for this transaction.
86    #[serde(with = "short_vec")]
87    pub address_table_lookups: Vec<MessageAddressTableLookup>,
88}
89
90impl Message {
91    /// Sanitize message fields and compiled instruction indexes
92    pub fn sanitize(&self) -> Result<(), SanitizeError> {
93        let num_static_account_keys = self.account_keys.len();
94        if usize::from(self.header.num_required_signatures)
95            .saturating_add(usize::from(self.header.num_readonly_unsigned_accounts))
96            > num_static_account_keys
97        {
98            return Err(SanitizeError::IndexOutOfBounds);
99        }
100
101        // there should be at least 1 RW fee-payer account.
102        if self.header.num_readonly_signed_accounts >= self.header.num_required_signatures {
103            return Err(SanitizeError::InvalidValue);
104        }
105
106        let num_dynamic_account_keys = {
107            let mut total_lookup_keys: usize = 0;
108            for lookup in &self.address_table_lookups {
109                let num_lookup_indexes = lookup
110                    .writable_indexes
111                    .len()
112                    .saturating_add(lookup.readonly_indexes.len());
113
114                // each lookup table must be used to load at least one account
115                if num_lookup_indexes == 0 {
116                    return Err(SanitizeError::InvalidValue);
117                }
118
119                total_lookup_keys = total_lookup_keys.saturating_add(num_lookup_indexes);
120            }
121            total_lookup_keys
122        };
123
124        // this is redundant with the above sanitization checks which require that:
125        // 1) the header describes at least 1 RW account
126        // 2) the header doesn't describe more account keys than the number of account keys
127        if num_static_account_keys == 0 {
128            return Err(SanitizeError::InvalidValue);
129        }
130
131        // the combined number of static and dynamic account keys must be <= 256
132        // since account indices are encoded as `u8`
133        // Note that this is different from the per-transaction account load cap
134        // as defined in `Bank::get_transaction_account_lock_limit`
135        let total_account_keys = num_static_account_keys.saturating_add(num_dynamic_account_keys);
136        if total_account_keys > 256 {
137            return Err(SanitizeError::IndexOutOfBounds);
138        }
139
140        // `expect` is safe because of earlier check that
141        // `num_static_account_keys` is non-zero
142        let max_account_ix = total_account_keys
143            .checked_sub(1)
144            .expect("message doesn't contain any account keys");
145
146        // reject program ids loaded from lookup tables so that
147        // static analysis on program instructions can be performed
148        // without loading on-chain data from a bank
149        let max_program_id_ix =
150            // `expect` is safe because of earlier check that
151            // `num_static_account_keys` is non-zero
152            num_static_account_keys
153                .checked_sub(1)
154                .expect("message doesn't contain any static account keys");
155
156        for ci in &self.instructions {
157            if usize::from(ci.program_id_index) > max_program_id_ix {
158                return Err(SanitizeError::IndexOutOfBounds);
159            }
160            // A program cannot be a payer.
161            if ci.program_id_index == 0 {
162                return Err(SanitizeError::IndexOutOfBounds);
163            }
164            for ai in &ci.accounts {
165                if usize::from(*ai) > max_account_ix {
166                    return Err(SanitizeError::IndexOutOfBounds);
167                }
168            }
169        }
170
171        Ok(())
172    }
173}
174
175impl Message {
176    /// Create a signable transaction message from a `payer` public key,
177    /// `recent_blockhash`, list of `instructions`, and a list of
178    /// `address_lookup_table_accounts`.
179    ///
180    /// # Examples
181    ///
182    /// This example uses the [`miraland_rpc_client`], [`miraland_sdk`], and [`anyhow`] crates.
183    ///
184    /// [`miraland_rpc_client`]: https://docs.rs/miraland-rpc-client
185    /// [`miraland_sdk`]: https://docs.rs/miraland-sdk
186    /// [`anyhow`]: https://docs.rs/anyhow
187    ///
188    /// ```
189    /// # use miraland_program::example_mocks::{
190    /// #     miraland_rpc_client,
191    /// #     miraland_sdk,
192    /// # };
193    /// # use std::borrow::Cow;
194    /// # use miraland_sdk::account::Account;
195    /// use anyhow::Result;
196    /// use miraland_rpc_client::rpc_client::RpcClient;
197    /// use miraland_program::address_lookup_table::{self, state::{AddressLookupTable, LookupTableMeta}};
198    /// use miraland_sdk::{
199    ///      address_lookup_table_account::AddressLookupTableAccount,
200    ///      instruction::{AccountMeta, Instruction},
201    ///      message::{VersionedMessage, v0},
202    ///      pubkey::Pubkey,
203    ///      signature::{Keypair, Signer},
204    ///      transaction::VersionedTransaction,
205    /// };
206    ///
207    /// fn create_tx_with_address_table_lookup(
208    ///     client: &RpcClient,
209    ///     instruction: Instruction,
210    ///     address_lookup_table_key: Pubkey,
211    ///     payer: &Keypair,
212    /// ) -> Result<VersionedTransaction> {
213    ///     # client.set_get_account_response(address_lookup_table_key, Account {
214    ///     #   lamports: 1,
215    ///     #   data: AddressLookupTable {
216    ///     #     meta: LookupTableMeta::default(),
217    ///     #     addresses: Cow::Owned(instruction.accounts.iter().map(|meta| meta.pubkey).collect()),
218    ///     #   }.serialize_for_tests().unwrap(),
219    ///     #   owner: address_lookup_table::program::id(),
220    ///     #   executable: false,
221    ///     #   rent_epoch: 1,
222    ///     # });
223    ///     let raw_account = client.get_account(&address_lookup_table_key)?;
224    ///     let address_lookup_table = AddressLookupTable::deserialize(&raw_account.data)?;
225    ///     let address_lookup_table_account = AddressLookupTableAccount {
226    ///         key: address_lookup_table_key,
227    ///         addresses: address_lookup_table.addresses.to_vec(),
228    ///     };
229    ///
230    ///     let blockhash = client.get_latest_blockhash()?;
231    ///     let tx = VersionedTransaction::try_new(
232    ///         VersionedMessage::V0(v0::Message::try_compile(
233    ///             &payer.pubkey(),
234    ///             &[instruction],
235    ///             &[address_lookup_table_account],
236    ///             blockhash,
237    ///         )?),
238    ///         &[payer],
239    ///     )?;
240    ///
241    ///     # assert!(tx.message.address_table_lookups().unwrap().len() > 0);
242    ///     Ok(tx)
243    /// }
244    /// #
245    /// # let client = RpcClient::new(String::new());
246    /// # let payer = Keypair::new();
247    /// # let address_lookup_table_key = Pubkey::new_unique();
248    /// # let instruction = Instruction::new_with_bincode(Pubkey::new_unique(), &(), vec![
249    /// #   AccountMeta::new(Pubkey::new_unique(), false),
250    /// # ]);
251    /// # create_tx_with_address_table_lookup(&client, instruction, address_lookup_table_key, &payer)?;
252    /// # Ok::<(), anyhow::Error>(())
253    /// ```
254    pub fn try_compile(
255        payer: &Pubkey,
256        instructions: &[Instruction],
257        address_lookup_table_accounts: &[AddressLookupTableAccount],
258        recent_blockhash: Hash,
259    ) -> Result<Self, CompileError> {
260        let mut compiled_keys = CompiledKeys::compile(instructions, Some(*payer));
261
262        let mut address_table_lookups = Vec::with_capacity(address_lookup_table_accounts.len());
263        let mut loaded_addresses_list = Vec::with_capacity(address_lookup_table_accounts.len());
264        for lookup_table_account in address_lookup_table_accounts {
265            if let Some((lookup, loaded_addresses)) =
266                compiled_keys.try_extract_table_lookup(lookup_table_account)?
267            {
268                address_table_lookups.push(lookup);
269                loaded_addresses_list.push(loaded_addresses);
270            }
271        }
272
273        let (header, static_keys) = compiled_keys.try_into_message_components()?;
274        let dynamic_keys = loaded_addresses_list.into_iter().collect();
275        let account_keys = AccountKeys::new(&static_keys, Some(&dynamic_keys));
276        let instructions = account_keys.try_compile_instructions(instructions)?;
277
278        Ok(Self {
279            header,
280            account_keys: static_keys,
281            recent_blockhash,
282            instructions,
283            address_table_lookups,
284        })
285    }
286
287    /// Serialize this message with a version #0 prefix using bincode encoding.
288    pub fn serialize(&self) -> Vec<u8> {
289        bincode::serialize(&(MESSAGE_VERSION_PREFIX, self)).unwrap()
290    }
291
292    /// Returns true if the account at the specified index is called as a program by an instruction
293    pub fn is_key_called_as_program(&self, key_index: usize) -> bool {
294        if let Ok(key_index) = u8::try_from(key_index) {
295            self.instructions
296                .iter()
297                .any(|ix| ix.program_id_index == key_index)
298        } else {
299            false
300        }
301    }
302
303    /// Returns true if the account at the specified index was requested to be
304    /// writable.  This method should not be used directly.
305    fn is_writable_index(&self, key_index: usize) -> bool {
306        let header = &self.header;
307        let num_account_keys = self.account_keys.len();
308        let num_signed_accounts = usize::from(header.num_required_signatures);
309        if key_index >= num_account_keys {
310            let loaded_addresses_index = key_index.saturating_sub(num_account_keys);
311            let num_writable_dynamic_addresses = self
312                .address_table_lookups
313                .iter()
314                .map(|lookup| lookup.writable_indexes.len())
315                .sum();
316            loaded_addresses_index < num_writable_dynamic_addresses
317        } else if key_index >= num_signed_accounts {
318            let num_unsigned_accounts = num_account_keys.saturating_sub(num_signed_accounts);
319            let num_writable_unsigned_accounts = num_unsigned_accounts
320                .saturating_sub(usize::from(header.num_readonly_unsigned_accounts));
321            let unsigned_account_index = key_index.saturating_sub(num_signed_accounts);
322            unsigned_account_index < num_writable_unsigned_accounts
323        } else {
324            let num_writable_signed_accounts = num_signed_accounts
325                .saturating_sub(usize::from(header.num_readonly_signed_accounts));
326            key_index < num_writable_signed_accounts
327        }
328    }
329
330    /// Returns true if any static account key is the bpf upgradeable loader
331    fn is_upgradeable_loader_in_static_keys(&self) -> bool {
332        self.account_keys
333            .iter()
334            .any(|&key| key == bpf_loader_upgradeable::id())
335    }
336
337    /// Returns true if the account at the specified index was requested as writable.
338    /// Before loading addresses, we can't demote write locks for dynamically loaded
339    /// addresses so this should not be used by the runtime.
340    pub fn is_maybe_writable(&self, key_index: usize) -> bool {
341        self.is_writable_index(key_index)
342            && !{
343                // demote reserved ids
344                self.account_keys
345                    .get(key_index)
346                    .map(is_builtin_key_or_sysvar)
347                    .unwrap_or_default()
348            }
349            && !{
350                // demote program ids
351                self.is_key_called_as_program(key_index)
352                    && !self.is_upgradeable_loader_in_static_keys()
353            }
354    }
355}
356
357#[cfg(test)]
358mod tests {
359    use {
360        super::*,
361        crate::{instruction::AccountMeta, message::VersionedMessage},
362    };
363
364    #[test]
365    fn test_sanitize() {
366        assert!(Message {
367            header: MessageHeader {
368                num_required_signatures: 1,
369                ..MessageHeader::default()
370            },
371            account_keys: vec![Pubkey::new_unique()],
372            ..Message::default()
373        }
374        .sanitize()
375        .is_ok());
376    }
377
378    #[test]
379    fn test_sanitize_with_instruction() {
380        assert!(Message {
381            header: MessageHeader {
382                num_required_signatures: 1,
383                ..MessageHeader::default()
384            },
385            account_keys: vec![Pubkey::new_unique(), Pubkey::new_unique()],
386            instructions: vec![CompiledInstruction {
387                program_id_index: 1,
388                accounts: vec![0],
389                data: vec![]
390            }],
391            ..Message::default()
392        }
393        .sanitize()
394        .is_ok());
395    }
396
397    #[test]
398    fn test_sanitize_with_table_lookup() {
399        assert!(Message {
400            header: MessageHeader {
401                num_required_signatures: 1,
402                ..MessageHeader::default()
403            },
404            account_keys: vec![Pubkey::new_unique()],
405            address_table_lookups: vec![MessageAddressTableLookup {
406                account_key: Pubkey::new_unique(),
407                writable_indexes: vec![1, 2, 3],
408                readonly_indexes: vec![0],
409            }],
410            ..Message::default()
411        }
412        .sanitize()
413        .is_ok());
414    }
415
416    #[test]
417    fn test_sanitize_with_table_lookup_and_ix_with_dynamic_program_id() {
418        let message = Message {
419            header: MessageHeader {
420                num_required_signatures: 1,
421                ..MessageHeader::default()
422            },
423            account_keys: vec![Pubkey::new_unique()],
424            address_table_lookups: vec![MessageAddressTableLookup {
425                account_key: Pubkey::new_unique(),
426                writable_indexes: vec![1, 2, 3],
427                readonly_indexes: vec![0],
428            }],
429            instructions: vec![CompiledInstruction {
430                program_id_index: 4,
431                accounts: vec![0, 1, 2, 3],
432                data: vec![],
433            }],
434            ..Message::default()
435        };
436
437        assert!(message.sanitize().is_err());
438    }
439
440    #[test]
441    fn test_sanitize_with_table_lookup_and_ix_with_static_program_id() {
442        assert!(Message {
443            header: MessageHeader {
444                num_required_signatures: 1,
445                ..MessageHeader::default()
446            },
447            account_keys: vec![Pubkey::new_unique(), Pubkey::new_unique()],
448            address_table_lookups: vec![MessageAddressTableLookup {
449                account_key: Pubkey::new_unique(),
450                writable_indexes: vec![1, 2, 3],
451                readonly_indexes: vec![0],
452            }],
453            instructions: vec![CompiledInstruction {
454                program_id_index: 1,
455                accounts: vec![2, 3, 4, 5],
456                data: vec![]
457            }],
458            ..Message::default()
459        }
460        .sanitize()
461        .is_ok());
462    }
463
464    #[test]
465    fn test_sanitize_without_signer() {
466        assert!(Message {
467            header: MessageHeader::default(),
468            account_keys: vec![Pubkey::new_unique()],
469            ..Message::default()
470        }
471        .sanitize()
472        .is_err());
473    }
474
475    #[test]
476    fn test_sanitize_without_writable_signer() {
477        assert!(Message {
478            header: MessageHeader {
479                num_required_signatures: 1,
480                num_readonly_signed_accounts: 1,
481                ..MessageHeader::default()
482            },
483            account_keys: vec![Pubkey::new_unique()],
484            ..Message::default()
485        }
486        .sanitize()
487        .is_err());
488    }
489
490    #[test]
491    fn test_sanitize_with_empty_table_lookup() {
492        assert!(Message {
493            header: MessageHeader {
494                num_required_signatures: 1,
495                ..MessageHeader::default()
496            },
497            account_keys: vec![Pubkey::new_unique()],
498            address_table_lookups: vec![MessageAddressTableLookup {
499                account_key: Pubkey::new_unique(),
500                writable_indexes: vec![],
501                readonly_indexes: vec![],
502            }],
503            ..Message::default()
504        }
505        .sanitize()
506        .is_err());
507    }
508
509    #[test]
510    fn test_sanitize_with_max_account_keys() {
511        assert!(Message {
512            header: MessageHeader {
513                num_required_signatures: 1,
514                ..MessageHeader::default()
515            },
516            account_keys: (0..=u8::MAX).map(|_| Pubkey::new_unique()).collect(),
517            ..Message::default()
518        }
519        .sanitize()
520        .is_ok());
521    }
522
523    #[test]
524    fn test_sanitize_with_too_many_account_keys() {
525        assert!(Message {
526            header: MessageHeader {
527                num_required_signatures: 1,
528                ..MessageHeader::default()
529            },
530            account_keys: (0..=256).map(|_| Pubkey::new_unique()).collect(),
531            ..Message::default()
532        }
533        .sanitize()
534        .is_err());
535    }
536
537    #[test]
538    fn test_sanitize_with_max_table_loaded_keys() {
539        assert!(Message {
540            header: MessageHeader {
541                num_required_signatures: 1,
542                ..MessageHeader::default()
543            },
544            account_keys: vec![Pubkey::new_unique()],
545            address_table_lookups: vec![MessageAddressTableLookup {
546                account_key: Pubkey::new_unique(),
547                writable_indexes: (0..=254).step_by(2).collect(),
548                readonly_indexes: (1..=254).step_by(2).collect(),
549            }],
550            ..Message::default()
551        }
552        .sanitize()
553        .is_ok());
554    }
555
556    #[test]
557    fn test_sanitize_with_too_many_table_loaded_keys() {
558        assert!(Message {
559            header: MessageHeader {
560                num_required_signatures: 1,
561                ..MessageHeader::default()
562            },
563            account_keys: vec![Pubkey::new_unique()],
564            address_table_lookups: vec![MessageAddressTableLookup {
565                account_key: Pubkey::new_unique(),
566                writable_indexes: (0..=255).step_by(2).collect(),
567                readonly_indexes: (1..=255).step_by(2).collect(),
568            }],
569            ..Message::default()
570        }
571        .sanitize()
572        .is_err());
573    }
574
575    #[test]
576    fn test_sanitize_with_invalid_ix_program_id() {
577        let message = Message {
578            header: MessageHeader {
579                num_required_signatures: 1,
580                ..MessageHeader::default()
581            },
582            account_keys: vec![Pubkey::new_unique()],
583            address_table_lookups: vec![MessageAddressTableLookup {
584                account_key: Pubkey::new_unique(),
585                writable_indexes: vec![0],
586                readonly_indexes: vec![],
587            }],
588            instructions: vec![CompiledInstruction {
589                program_id_index: 2,
590                accounts: vec![],
591                data: vec![],
592            }],
593            ..Message::default()
594        };
595
596        assert!(message.sanitize().is_err());
597    }
598
599    #[test]
600    fn test_sanitize_with_invalid_ix_account() {
601        assert!(Message {
602            header: MessageHeader {
603                num_required_signatures: 1,
604                ..MessageHeader::default()
605            },
606            account_keys: vec![Pubkey::new_unique(), Pubkey::new_unique()],
607            address_table_lookups: vec![MessageAddressTableLookup {
608                account_key: Pubkey::new_unique(),
609                writable_indexes: vec![],
610                readonly_indexes: vec![0],
611            }],
612            instructions: vec![CompiledInstruction {
613                program_id_index: 1,
614                accounts: vec![3],
615                data: vec![]
616            }],
617            ..Message::default()
618        }
619        .sanitize()
620        .is_err());
621    }
622
623    #[test]
624    fn test_serialize() {
625        let message = Message::default();
626        let versioned_msg = VersionedMessage::V0(message.clone());
627        assert_eq!(message.serialize(), versioned_msg.serialize());
628    }
629
630    #[test]
631    fn test_try_compile() {
632        let mut keys = vec![];
633        keys.resize_with(7, Pubkey::new_unique);
634
635        let payer = keys[0];
636        let program_id = keys[6];
637        let instructions = vec![Instruction {
638            program_id,
639            accounts: vec![
640                AccountMeta::new(keys[1], true),
641                AccountMeta::new_readonly(keys[2], true),
642                AccountMeta::new(keys[3], false),
643                AccountMeta::new(keys[4], false), // loaded from lut
644                AccountMeta::new_readonly(keys[5], false), // loaded from lut
645            ],
646            data: vec![],
647        }];
648        let address_lookup_table_accounts = vec![
649            AddressLookupTableAccount {
650                key: Pubkey::new_unique(),
651                addresses: vec![keys[4], keys[5], keys[6]],
652            },
653            AddressLookupTableAccount {
654                key: Pubkey::new_unique(),
655                addresses: vec![],
656            },
657        ];
658
659        let recent_blockhash = Hash::new_unique();
660        assert_eq!(
661            Message::try_compile(
662                &payer,
663                &instructions,
664                &address_lookup_table_accounts,
665                recent_blockhash
666            ),
667            Ok(Message {
668                header: MessageHeader {
669                    num_required_signatures: 3,
670                    num_readonly_signed_accounts: 1,
671                    num_readonly_unsigned_accounts: 1
672                },
673                recent_blockhash,
674                account_keys: vec![keys[0], keys[1], keys[2], keys[3], program_id],
675                instructions: vec![CompiledInstruction {
676                    program_id_index: 4,
677                    accounts: vec![1, 2, 3, 5, 6],
678                    data: vec![],
679                },],
680                address_table_lookups: vec![MessageAddressTableLookup {
681                    account_key: address_lookup_table_accounts[0].key,
682                    writable_indexes: vec![0],
683                    readonly_indexes: vec![1],
684                }],
685            })
686        );
687    }
688}