diff --git a/sdk/program/src/example_mocks.rs b/sdk/program/src/example_mocks.rs new file mode 100644 index 0000000000..082aee3cc4 --- /dev/null +++ b/sdk/program/src/example_mocks.rs @@ -0,0 +1,142 @@ +//! Mock types for use in examples. +//! +//! These represent APIs from crates that themselves depend on this crate, and +//! which are useful for illustrating the examples for APIs in this crate. +//! +//! Directly depending on these crates though would cause problematic circular +//! dependencies, so instead they are mocked out here in a way that allows +//! examples to appear to use crates that this crate must not depend on. +//! +//! Each mod here has the name of a crate, so that examples can be structured to +//! appear to import from that crate. + +#![doc(hidden)] +#![allow(clippy::new_without_default)] + +pub mod solana_client { + pub mod client_error { + use thiserror::Error; + + #[derive(Error, Debug)] + #[error("mock-error")] + pub struct ClientError; + pub type Result = std::result::Result; + } + + pub mod rpc_client { + use super::super::solana_sdk::{ + hash::Hash, signature::Signature, transaction::Transaction, + }; + use super::client_error::Result as ClientResult; + + pub struct RpcClient; + + impl RpcClient { + pub fn new(_url: String) -> Self { + RpcClient + } + + pub fn get_latest_blockhash(&self) -> ClientResult { + Ok(Hash::default()) + } + + pub fn send_and_confirm_transaction( + &self, + _transaction: &Transaction, + ) -> ClientResult { + Ok(Signature::default()) + } + + pub fn get_minimum_balance_for_rent_exemption( + &self, + _data_len: usize, + ) -> ClientResult { + Ok(0) + } + } + } +} + +/// Re-exports and mocks of solana-program modules that mirror those from +/// solana-program. +/// +/// This lets examples in solana-program appear to be written as client +/// programs. +pub mod solana_sdk { + pub use crate::hash; + pub use crate::instruction; + pub use crate::message; + pub use crate::nonce; + pub use crate::pubkey; + pub use crate::system_instruction; + + pub mod signature { + use crate::pubkey::Pubkey; + + #[derive(Default)] + pub struct Signature; + + pub struct Keypair; + + impl Keypair { + pub fn new() -> Keypair { + Keypair + } + + pub fn pubkey(&self) -> Pubkey { + Pubkey::default() + } + } + + impl Signer for Keypair {} + + pub trait Signer {} + } + + pub mod signers { + use super::signature::Signer; + + pub trait Signers {} + + impl Signers for [&T; 1] {} + impl Signers for [&T; 2] {} + } + + pub mod transaction { + use super::signers::Signers; + use crate::hash::Hash; + use crate::instruction::Instruction; + use crate::message::Message; + use crate::pubkey::Pubkey; + + pub struct Transaction { + pub message: Message, + } + + impl Transaction { + pub fn new( + _from_keypairs: &T, + _message: Message, + _recent_blockhash: Hash, + ) -> Transaction { + Transaction { + message: Message::new(&[], None), + } + } + + pub fn new_unsigned(_message: Message) -> Self { + Transaction { + message: Message::new(&[], None), + } + } + + pub fn new_with_payer(_instructions: &[Instruction], _payer: Option<&Pubkey>) -> Self { + Transaction { + message: Message::new(&[], None), + } + } + + pub fn sign(&mut self, _keypairs: &T, _recent_blockhash: Hash) {} + } + } +} diff --git a/sdk/program/src/lib.rs b/sdk/program/src/lib.rs index d4d6ffc780..e87d65af71 100644 --- a/sdk/program/src/lib.rs +++ b/sdk/program/src/lib.rs @@ -19,6 +19,7 @@ pub mod ed25519_program; pub mod entrypoint; pub mod entrypoint_deprecated; pub mod epoch_schedule; +pub mod example_mocks; pub mod feature; pub mod fee_calculator; pub mod hash; diff --git a/sdk/program/src/message/legacy.rs b/sdk/program/src/message/legacy.rs index d5ead3f34c..a85dc7ecba 100644 --- a/sdk/program/src/message/legacy.rs +++ b/sdk/program/src/message/legacy.rs @@ -1,5 +1,15 @@ +//! The original and current Solana message format. +//! +//! This crate defines two versions of `Message` in their own modules: +//! [`legacy`] and [`v0`]. `legacy` is the current version as of Solana 1.10.0. +//! `v0` is a [future message format] that encodes more account keys into a +//! transaction than the legacy format. +//! +//! [`legacy`]: crate::message::legacy +//! [`v0`]: crate::message::v0 +//! [future message format]: https://docs.solana.com/proposals/transactions-v2 + #![allow(clippy::integer_arithmetic)] -//! A library for generating a message from a sequence of instructions use { crate::{ @@ -163,6 +173,20 @@ fn get_program_ids(instructions: &[Instruction]) -> Vec { .collect() } +/// A Solana transaction message (legacy). +/// +/// See the [`message`] module documentation for further description. +/// +/// [`message`]: crate::message +/// +/// Some constructors accept an optional `payer`, the account responsible for +/// paying the cost of executing a transaction. In most cases, callers should +/// specify the payer explicitly in these constructors. In some cases though, +/// the caller is not _required_ to specify the payer, but is still allowed to: +/// in the `Message` structure, the first account is always the fee-payer, so if +/// the caller has knowledge that the first account of the constructed +/// transaction's `Message` is both a signer and the expected fee-payer, then +/// redundantly specifying the fee-payer is not strictly required. // NOTE: Serialization-related changes must be paired with the custom serialization // for versioned messages in the `RemainingLegacyMessage` struct. #[wasm_bindgen] @@ -170,12 +194,12 @@ fn get_program_ids(instructions: &[Instruction]) -> Vec { #[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq, Clone, AbiExample)] #[serde(rename_all = "camelCase")] pub struct Message { - /// The message header, identifying signed and read-only `account_keys` - /// NOTE: Serialization-related changes must be paired with the direct read at sigverify. + /// The message header, identifying signed and read-only `account_keys`. + // NOTE: Serialization-related changes must be paired with the direct read at sigverify. #[wasm_bindgen(skip)] pub header: MessageHeader, - /// All the account keys used by this transaction + /// All the account keys used by this transaction. #[wasm_bindgen(skip)] #[serde(with = "short_vec")] pub account_keys: Vec, @@ -227,30 +251,147 @@ impl Sanitize for Message { } impl Message { - pub fn new_with_compiled_instructions( - num_required_signatures: u8, - num_readonly_signed_accounts: u8, - num_readonly_unsigned_accounts: u8, - account_keys: Vec, - recent_blockhash: Hash, - instructions: Vec, - ) -> Self { - Self { - header: MessageHeader { - num_required_signatures, - num_readonly_signed_accounts, - num_readonly_unsigned_accounts, - }, - account_keys, - recent_blockhash, - instructions, - } - } - + /// Create a new `Message`. + /// + /// # Examples + /// + /// This example uses the [`solana_sdk`], [`solana_client`] and [`anyhow`] crates. + /// + /// [`solana_sdk`]: https://docs.rs/solana-sdk + /// [`solana_client`]: https://docs.rs/solana-client + /// [`anyhow`]: https://docs.rs/anyhow + /// + /// ``` + /// # use solana_program::example_mocks::solana_sdk; + /// # use solana_program::example_mocks::solana_client; + /// use anyhow::Result; + /// use borsh::{BorshSerialize, BorshDeserialize}; + /// use solana_client::rpc_client::RpcClient; + /// use solana_sdk::{ + /// instruction::Instruction, + /// message::Message, + /// pubkey::Pubkey, + /// signature::Keypair, + /// transaction::Transaction, + /// }; + /// + /// // A custom program instruction. This would typically be defined in + /// // another crate so it can be shared between the on-chain program and + /// // the client. + /// #[derive(BorshSerialize, BorshDeserialize)] + /// enum BankInstruction { + /// Initialize, + /// Deposit { lamports: u64 }, + /// Withdraw { lamports: u64 }, + /// } + /// + /// fn send_initialize_tx( + /// client: &RpcClient, + /// program_id: Pubkey, + /// payer: &Keypair + /// ) -> Result<()> { + /// + /// let bank_instruction = BankInstruction::Initialize; + /// + /// let instruction = Instruction::new_with_borsh( + /// program_id, + /// &bank_instruction, + /// vec![], + /// ); + /// + /// let message = Message::new( + /// &[instruction], + /// Some(&payer.pubkey()), + /// ); + /// + /// let blockhash = client.get_latest_blockhash()?; + /// let mut tx = Transaction::new(&[payer], message, blockhash); + /// client.send_and_confirm_transaction(&tx)?; + /// + /// Ok(()) + /// } + /// # + /// # let client = RpcClient::new(String::new()); + /// # let program_id = Pubkey::new_unique(); + /// # let payer = Keypair::new(); + /// # send_initialize_tx(&client, program_id, &payer)?; + /// # + /// # Ok::<(), anyhow::Error>(()) + /// ``` pub fn new(instructions: &[Instruction], payer: Option<&Pubkey>) -> Self { Self::new_with_blockhash(instructions, payer, &Hash::default()) } + /// Create a new message while setting the blockhash. + /// + /// # Examples + /// + /// This example uses the [`solana_sdk`], [`solana_client`] and [`anyhow`] crates. + /// + /// [`solana_sdk`]: https://docs.rs/solana-sdk + /// [`solana_client`]: https://docs.rs/solana-client + /// [`anyhow`]: https://docs.rs/anyhow + /// + /// ``` + /// # use solana_program::example_mocks::solana_sdk; + /// # use solana_program::example_mocks::solana_client; + /// use anyhow::Result; + /// use borsh::{BorshSerialize, BorshDeserialize}; + /// use solana_client::rpc_client::RpcClient; + /// use solana_sdk::{ + /// instruction::Instruction, + /// message::Message, + /// pubkey::Pubkey, + /// signature::Keypair, + /// transaction::Transaction, + /// }; + /// + /// // A custom program instruction. This would typically be defined in + /// // another crate so it can be shared between the on-chain program and + /// // the client. + /// #[derive(BorshSerialize, BorshDeserialize)] + /// enum BankInstruction { + /// Initialize, + /// Deposit { lamports: u64 }, + /// Withdraw { lamports: u64 }, + /// } + /// + /// fn send_initialize_tx( + /// client: &RpcClient, + /// program_id: Pubkey, + /// payer: &Keypair + /// ) -> Result<()> { + /// + /// let bank_instruction = BankInstruction::Initialize; + /// + /// let instruction = Instruction::new_with_borsh( + /// program_id, + /// &bank_instruction, + /// vec![], + /// ); + /// + /// let blockhash = client.get_latest_blockhash()?; + /// + /// let message = Message::new_with_blockhash( + /// &[instruction], + /// Some(&payer.pubkey()), + /// &blockhash, + /// ); + /// + /// let mut tx = Transaction::new_unsigned(message); + /// tx.sign(&[payer], tx.message.recent_blockhash); + /// client.send_and_confirm_transaction(&tx)?; + /// + /// Ok(()) + /// } + /// # + /// # let client = RpcClient::new(String::new()); + /// # let program_id = Pubkey::new_unique(); + /// # let payer = Keypair::new(); + /// # send_initialize_tx(&client, program_id, &payer)?; + /// # + /// # Ok::<(), anyhow::Error>(()) + /// ``` pub fn new_with_blockhash( instructions: &[Instruction], payer: Option<&Pubkey>, @@ -275,6 +416,112 @@ impl Message { ) } + /// Create a new message for a [nonced transaction]. + /// + /// [nonced transaction]: https://docs.solana.com/implemented-proposals/durable-tx-nonces + /// + /// In this type of transaction, the blockhash is replaced with a _durable + /// transaction nonce_, allowing for extended time to pass between the + /// transaction's signing and submission to the blockchain. + /// + /// # Examples + /// + /// This example uses the [`solana_sdk`], [`solana_client`] and [`anyhow`] crates. + /// + /// [`solana_sdk`]: https://docs.rs/solana-sdk + /// [`solana_client`]: https://docs.rs/solana-client + /// [`anyhow`]: https://docs.rs/anyhow + /// + /// ``` + /// # use solana_program::example_mocks::solana_sdk; + /// # use solana_program::example_mocks::solana_client; + /// use anyhow::Result; + /// use borsh::{BorshSerialize, BorshDeserialize}; + /// use solana_client::rpc_client::RpcClient; + /// use solana_sdk::{ + /// hash::Hash, + /// instruction::Instruction, + /// message::Message, + /// nonce, + /// pubkey::Pubkey, + /// signature::Keypair, + /// system_instruction, + /// transaction::Transaction, + /// }; + /// + /// // A custom program instruction. This would typically be defined in + /// // another crate so it can be shared between the on-chain program and + /// // the client. + /// #[derive(BorshSerialize, BorshDeserialize)] + /// enum BankInstruction { + /// Initialize, + /// Deposit { lamports: u64 }, + /// Withdraw { lamports: u64 }, + /// } + /// + /// // Create a nonced transaction for later signing and submission, + /// // returning it and the nonce account's pubkey. + /// fn create_offline_initialize_tx( + /// client: &RpcClient, + /// program_id: Pubkey, + /// payer: &Keypair + /// ) -> Result<(Transaction, Pubkey)> { + /// + /// let bank_instruction = BankInstruction::Initialize; + /// let bank_instruction = Instruction::new_with_borsh( + /// program_id, + /// &bank_instruction, + /// vec![], + /// ); + /// + /// // This will create a nonce account and assign authority to the + /// // payer so they can sign to advance the nonce and withdraw its rent. + /// let nonce_account = make_nonce_account(client, payer)?; + /// + /// let mut message = Message::new_with_nonce( + /// vec![bank_instruction], + /// Some(&payer.pubkey()), + /// &nonce_account, + /// &payer.pubkey() + /// ); + /// + /// // This transaction will need to be signed later, using the blockhash + /// // stored in the nonce account. + /// let tx = Transaction::new_unsigned(message); + /// + /// Ok((tx, nonce_account)) + /// } + /// + /// fn make_nonce_account(client: &RpcClient, payer: &Keypair) + /// -> Result + /// { + /// let nonce_account_address = Keypair::new(); + /// let nonce_account_size = nonce::State::size(); + /// let nonce_rent = client.get_minimum_balance_for_rent_exemption(nonce_account_size)?; + /// + /// // Assigning the nonce authority to the payer so they can sign for the withdrawal, + /// // and we can throw away the nonce address secret key. + /// let create_nonce_instr = system_instruction::create_nonce_account( + /// &payer.pubkey(), + /// &nonce_account_address.pubkey(), + /// &payer.pubkey(), + /// nonce_rent, + /// ); + /// + /// let mut nonce_tx = Transaction::new_with_payer(&create_nonce_instr, Some(&payer.pubkey())); + /// let blockhash = client.get_latest_blockhash()?; + /// nonce_tx.sign(&[&payer, &nonce_account_address], blockhash); + /// client.send_and_confirm_transaction(&nonce_tx)?; + /// + /// Ok(nonce_account_address.pubkey()) + /// } + /// # + /// # let client = RpcClient::new(String::new()); + /// # let program_id = Pubkey::new_unique(); + /// # let payer = Keypair::new(); + /// # create_offline_initialize_tx(&client, program_id, &payer)?; + /// # Ok::<(), anyhow::Error>(()) + /// ``` pub fn new_with_nonce( mut instructions: Vec, payer: Option<&Pubkey>, @@ -287,14 +534,34 @@ impl Message { Self::new(&instructions, payer) } - /// Compute the blake3 hash of this transaction's message + pub fn new_with_compiled_instructions( + num_required_signatures: u8, + num_readonly_signed_accounts: u8, + num_readonly_unsigned_accounts: u8, + account_keys: Vec, + recent_blockhash: Hash, + instructions: Vec, + ) -> Self { + Self { + header: MessageHeader { + num_required_signatures, + num_readonly_signed_accounts, + num_readonly_unsigned_accounts, + }, + account_keys, + recent_blockhash, + instructions, + } + } + + /// Compute the blake3 hash of this transaction's message. #[cfg(not(target_arch = "bpf"))] pub fn hash(&self) -> Hash { let message_bytes = self.serialize(); Self::hash_raw_message(&message_bytes) } - /// Compute the blake3 hash of a raw transaction message + /// Compute the blake3 hash of a raw transaction message. #[cfg(not(target_arch = "bpf"))] pub fn hash_raw_message(message_bytes: &[u8]) -> Hash { use blake3::traits::digest::Digest; @@ -415,7 +682,7 @@ impl Message { self.account_keys[..last_key].iter().collect() } - /// Return true if account_keys has any duplicate keys + /// Returns `true` if `account_keys` has any duplicate keys. pub fn has_duplicates(&self) -> bool { // Note: This is an O(n^2) algorithm, but requires no heap allocations. The benchmark // `bench_has_duplicates` in benches/message_processor.rs shows that this implementation is @@ -429,7 +696,7 @@ impl Message { false } - /// Returns true if any account is the bpf upgradeable loader + /// Returns `true` if any account is the BPF upgradeable loader. pub fn is_upgradeable_loader_present(&self) -> bool { self.account_keys .iter() diff --git a/sdk/program/src/message/mod.rs b/sdk/program/src/message/mod.rs index 910a03c39c..bc1c764ac7 100644 --- a/sdk/program/src/message/mod.rs +++ b/sdk/program/src/message/mod.rs @@ -1,4 +1,41 @@ -//! A library for generating a message from a sequence of instructions +//! Sequences of [`Instruction`]s executed within a single transaction. +//! +//! [`Instruction`]: crate::instruction::Instruction +//! +//! In Solana, programs execute instructions, and clients submit sequences +//! of instructions to the network to be atomically executed as [`Transaction`]s. +//! +//! [`Transaction`]: https://docs.rs/solana-sdk/latest/solana-sdk/transaction/struct.Transaction.html +//! +//! A [`Message`] is the compact internal encoding of a transaction, as +//! transmitted across the network and stored in, and operated on, by the +//! runtime. It contains a flat array of all accounts accessed by all +//! instructions in the message, a [`MessageHeader`] that describes the layout +//! of that account array, a [recent blockhash], and a compact encoding of the +//! message's instructions. +//! +//! [recent blockhash]: https://docs.solana.com/developing/programming-model/transactions#recent-blockhash +//! +//! Clients most often deal with `Instruction`s and `Transaction`s, with +//! `Message`s being created by `Transaction` constructors. +//! +//! To ensure reliable network delivery, serialized messages must fit into the +//! IPv6 MTU size, conservatively assumed to be 1280 bytes. Thus constrained, +//! care must be taken in the amount of data consumed by instructions, and the +//! number of accounts they require to function. +//! +//! This module defines two versions of `Message` in their own modules: +//! [`legacy`] and [`v0`]. `legacy` is reexported here and is the current +//! version as of Solana 1.10.0. `v0` is a [future message format] that encodes +//! more account keys into a transaction than the legacy format. The +//! [`VersionedMessage`] type is a thin wrapper around either message version. +//! +//! [future message format]: https://docs.solana.com/proposals/transactions-v2 +//! +//! Despite living in the `solana-program` crate, there is no way to access the +//! runtime's messages from within a Solana program, and only the legacy message +//! types continue to be exposed to Solana programs, for backwards compatibility +//! reasons. pub mod legacy; @@ -15,23 +52,56 @@ pub use legacy::Message; #[cfg(not(target_arch = "bpf"))] pub use non_bpf_modules::*; -/// The length of a message header in bytes +/// The length of a message header in bytes. pub const MESSAGE_HEADER_LENGTH: usize = 3; +/// Describes the organization of a `Message`'s account keys. +/// +/// Every [`Instruction`] specifies which accounts it may reference, or +/// otherwise requires specific permissions of. Those specifications are: +/// whether the account is read-only, or read-write; and whether the account +/// must have signed the transaction containing the instruction. +/// +/// Whereas individual `Instruction`s contain a list of all accounts they may +/// access, along with their required permissions, a `Message` contains a +/// single shared flat list of _all_ accounts required by _all_ instructions in +/// a transaction. When building a `Message`, this flat list is created and +/// `Instruction`s are converted to [`CompiledInstruction`]s. Those +/// `CompiledInstruction`s then reference by index the accounts they require in +/// the single shared account list. +/// +/// [`Instruction`]: crate::instruction::Instruction +/// [`CompiledInstruction`]: crate::instruction::CompiledInstruction +/// +/// The shared account list is ordered by the permissions required of the accounts: +/// +/// - accounts that are writable and signers +/// - accounts that are read-only and signers +/// - accounts that are writable and not signers +/// - accounts that are read-only and not signers +/// +/// Given this ordering, the fields of `MessageHeader` describe which accounts +/// in a transaction require which permissions. +/// +/// When multiple transactions access the same read-only accounts, the runtime +/// may process them in parallel, in a single [PoH] entry. Transactions that +/// access the same read-write accounts are processed sequentially. +/// +/// [PoH]: https://docs.solana.com/cluster/synchronization #[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq, Clone, AbiExample)] #[serde(rename_all = "camelCase")] pub struct MessageHeader { - /// The number of signatures required for this message to be considered valid. The - /// signatures must match the first `num_required_signatures` of `account_keys`. - /// NOTE: Serialization-related changes must be paired with the direct read at sigverify. + /// The number of signatures required for this message to be considered + /// valid. The signers of those signatures must match the first + /// `num_required_signatures` of [`Message::account_keys`]. + // NOTE: Serialization-related changes must be paired with the direct read at sigverify. pub num_required_signatures: u8, - /// The last num_readonly_signed_accounts of the signed keys are read-only accounts. Programs - /// may process multiple transactions that load read-only accounts within a single PoH entry, - /// but are not permitted to credit or debit lamports or modify account data. Transactions - /// targeting the same read-write account are evaluated sequentially. + /// The last `num_readonly_signed_accounts` of the signed keys are read-only + /// accounts. pub num_readonly_signed_accounts: u8, - /// The last num_readonly_unsigned_accounts of the unsigned keys are read-only accounts. + /// The last `num_readonly_unsigned_accounts` of the unsigned keys are + /// read-only accounts. pub num_readonly_unsigned_accounts: u8, } diff --git a/sdk/program/src/message/sanitized.rs b/sdk/program/src/message/sanitized.rs index 32d4a29fd0..4932f59bea 100644 --- a/sdk/program/src/message/sanitized.rs +++ b/sdk/program/src/message/sanitized.rs @@ -18,8 +18,7 @@ use { thiserror::Error, }; -/// Sanitized message of a transaction which includes a set of atomic -/// instructions to be executed on-chain +/// Sanitized message of a transaction. #[derive(Debug, Clone)] pub enum SanitizedMessage { /// Sanitized legacy message diff --git a/sdk/program/src/message/versions/mod.rs b/sdk/program/src/message/versions/mod.rs index ace0a3bf72..cac0bb54f6 100644 --- a/sdk/program/src/message/versions/mod.rs +++ b/sdk/program/src/message/versions/mod.rs @@ -20,7 +20,7 @@ pub mod v0; /// Bit mask that indicates whether a serialized message is versioned. pub const MESSAGE_VERSION_PREFIX: u8 = 0x80; -/// Message versions supported by the Solana runtime. +/// Either a legacy message or a v0 message. /// /// # Serialization /// diff --git a/sdk/program/src/message/versions/v0/mod.rs b/sdk/program/src/message/versions/v0/mod.rs index 1e98d5c3d6..3a03e1ea71 100644 --- a/sdk/program/src/message/versions/v0/mod.rs +++ b/sdk/program/src/message/versions/v0/mod.rs @@ -1,3 +1,14 @@ +//! A future Solana message format. +//! +//! This crate defines two versions of `Message` in their own modules: +//! [`legacy`] and [`v0`]. `legacy` is the current version as of Solana 1.10.0. +//! `v0` is a [future message format] that encodes more account keys into a +//! transaction than the legacy format. +//! +//! [`legacy`]: crate::message::legacy +//! [`v0`]: crate::message::v0 +//! [future message format]: https://docs.solana.com/proposals/transactions-v2 + use crate::{ hash::Hash, instruction::CompiledInstruction, @@ -26,8 +37,14 @@ pub struct MessageAddressTableLookup { pub readonly_indexes: Vec, } -/// Transaction message format which supports succinct account loading with +/// A Solana transaction message (v0). +/// +/// This message format supports succinct account loading with /// on-chain address lookup tables. +/// +/// See the [`message`] module documentation for further description. +/// +/// [`message`]: crate::message #[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq, Clone, AbiExample)] #[serde(rename_all = "camelCase")] pub struct Message {