From d1c101cde2a258106dbb86239830fc97f64d078b Mon Sep 17 00:00:00 2001 From: Brian Anderson Date: Mon, 6 Dec 2021 11:00:50 -0600 Subject: [PATCH] Rework docs for Pubkey::find_program_address and friends (#21528) * Rework docs for Pubkey::find_program_address and friends * Remove circular dependency * Minor tweaks * Apply suggestions from code review Co-authored-by: Tyera Eulberg * Sort solana-program dev-dependencies Co-authored-by: Tyera Eulberg --- Cargo.lock | 1 + sdk/program/Cargo.toml | 3 +- sdk/program/src/pubkey.rs | 409 ++++++++++++++++++++++++++++---------- 3 files changed, 310 insertions(+), 103 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cae2de3ef0..18cbbe9393 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5328,6 +5328,7 @@ dependencies = [ name = "solana-program" version = "1.10.0" dependencies = [ + "anyhow", "assert_matches", "base64 0.13.0", "bincode", diff --git a/sdk/program/Cargo.toml b/sdk/program/Cargo.toml index 84b6f6a2ff..4812a661b0 100644 --- a/sdk/program/Cargo.toml +++ b/sdk/program/Cargo.toml @@ -47,10 +47,11 @@ itertools = "0.10.1" parking_lot = "0.11" [dev-dependencies] -static_assertions = "1.1.0" +anyhow = "1.0.45" assert_matches = "1.3.0" bincode = "1.3.1" serde_json = "1.0.56" +static_assertions = "1.1.0" [build-dependencies] rustc_version = "0.4" diff --git a/sdk/program/src/pubkey.rs b/sdk/program/src/pubkey.rs index 2222f8a6f2..6e97f179b6 100644 --- a/sdk/program/src/pubkey.rs +++ b/sdk/program/src/pubkey.rs @@ -179,29 +179,319 @@ impl Pubkey { )) } - /// Create a program address + /// Find a valid [program derived address][pda] and its corresponding bump seed. /// - /// Program addresses are account keys that only the program has the - /// authority to sign. The address is of the same form as a Solana - /// `Pubkey`, except they are ensured to not be on the ed25519 curve and - /// thus have no associated private key. When performing cross-program - /// invocations the program can "sign" for the key by calling - /// `invoke_signed` and passing the same seeds used to generate the address. - /// The runtime will check that indeed the program associated with this - /// address is the caller and thus authorized to be the signer. + /// [pda]: https://docs.solana.com/developing/programming-model/calling-between-programs#program-derived-addresses /// - /// Because the program address cannot lie on the ed25519 curve there may be - /// seed and program id combinations that are invalid. In these cases an - /// extra seed (bump seed) can be calculated that results in a point off the - /// curve. Use `find_program_address` to calculate that bump seed. + /// Program derived addresses (PDAs) are account keys that only the program, + /// `program_id`, has the authority to sign. The address is of the same form + /// as a Solana `Pubkey`, except they are ensured to not be on the ed25519 + /// curve and thus have no associated private key. When performing + /// cross-program invocations the program can "sign" for the key by calling + /// [`invoke_signed`] and passing the same seeds used to generate the + /// address, along with the calculated _bump seed_, which this function + /// returns as the second tuple element. The runtime will verify that the + /// program associated with this address is the caller and thus authorized + /// to be the signer. /// - /// Warning: Because of the way the seeds are hashed there is a potential + /// [`invoke_signed`]: crate::program::invoke_signed + /// + /// The `seeds` are application-specific, and must be carefully selected to + /// uniquely derive accounts per application requirements. It is common to + /// use static strings and other pubkeys as seeds. + /// + /// Because the program address must not lie on the ed25519 curve, there may + /// be seed and program id combinations that are invalid. For this reason, + /// an extra seed (the bump seed) is calculated that results in a + /// point off the curve. The bump seed must be passed as an additional seed + /// when calling `invoke_signed`. + /// + /// The processes of finding a valid program address is by trial and error, + /// and even though it is deterministic given a set of inputs it can take a + /// variable amount of time to succeed across different inputs. This means + /// that when called from an on-chain program it may incur a variable amount + /// of the program's compute budget. Programs that are meant to be very + /// performant may not want to use this function because it could take a + /// considerable amount of time. Programs that are already at risk + /// of exceeding their compute budget should call this with care since + /// there is a chance that the program's budget may be occasionally + /// and unpredictably exceeded. + /// + /// As all account addresses accessed by an on-chain Solana program must be + /// explicitly passed to the program, it is typical for the PDAs to be + /// derived in off-chain client programs, avoiding the compute cost of + /// generating the address on-chain. The address may or may not then be + /// verified by re-deriving it on-chain, depending on the requirements of + /// the program. + /// + /// **Warning**: Because of the way the seeds are hashed there is a potential /// for program address collisions for the same program id. The seeds are /// hashed sequentially which means that seeds {"abcdef"}, {"abc", "def"}, /// and {"ab", "cd", "ef"} will all result in the same program address given - /// the same program id. Since the change of collision is local to a given - /// program id the developer of that program must take care to choose seeds - /// that do not collide with themselves. + /// the same program id. Since the chance of collision is local to a given + /// program id, the developer of that program must take care to choose seeds + /// that do not collide with each other. For seed schemes that are susceptible + /// to this type of hash collision, a common remedy is to insert separators + /// between seeds, e.g. transforming {"abc", "def"} into {"abc", "-", "def"}. + /// + /// # Panics + /// + /// Panics in the statistically improbable event that a bump seed could not be + /// found. Use [`try_find_program_address`] to handle this case. + /// + /// [`try_find_program_address`]: Pubkey::try_find_program_address + /// + /// Panics if any of the following are true: + /// + /// - the number of provided seeds is greater than, _or equal to_, [`MAX_SEEDS`], + /// - any individual seed's length is greater than [`MAX_SEED_LEN`]. + /// + /// # Examples + /// + /// This example illustrates a simple case of creating a "vault" account + /// which is derived from the payer account, but owned by an on-chain + /// program. The program derived address is derived in an off-chain client + /// program, which invokes an on-chain Solana program that uses the address + /// to create a new account owned and controlled by the program itself. + /// + /// By convention, the on-chain program will be compiled for use in two + /// different contexts: both on-chain, to interpret a custom program + /// instruction as a Solana transaction; and off-chain, as a library, so + /// that clients can share the instruction data structure, constructors, and + /// other common code. + /// + /// First the on-chain Solana program: + /// + /// ``` + /// # use borsh::{BorshSerialize, BorshDeserialize}; + /// # use solana_program::{ + /// # pubkey::Pubkey, + /// # entrypoint::ProgramResult, + /// # program::invoke_signed, + /// # system_instruction, + /// # account_info::{ + /// # AccountInfo, + /// # next_account_info, + /// # }, + /// # }; + /// // The custom instruction processed by our program. It includes the + /// // PDA's bump seed, which is derived by the client program. This + /// // definition is also imported into the off-chain client program. + /// // The computed address of the PDA will be passed to this program via + /// // the `accounts` vector of the `Instruction` type. + /// #[derive(BorshSerialize, BorshDeserialize, Debug)] + /// pub struct InstructionData { + /// pub vault_bump_seed: u8, + /// pub lamports: u64, + /// } + /// + /// // The size in bytes of a vault account. The client program needs + /// // this information to calculate the quantity of lamports necessary + /// // to pay for the account's rent. + /// pub static VAULT_ACCOUNT_SIZE: u64 = 1024; + /// + /// // The entrypoint of the on-chain program, as provided to the + /// // `entrypoint!` macro. + /// fn process_instruction( + /// program_id: &Pubkey, + /// accounts: &[AccountInfo], + /// instruction_data: &[u8], + /// ) -> ProgramResult { + /// let account_info_iter = &mut accounts.iter(); + /// let payer = next_account_info(account_info_iter)?; + /// // The vault PDA, derived from the payer's address + /// let vault = next_account_info(account_info_iter)?; + /// + /// let mut instruction_data = instruction_data; + /// let instr = InstructionData::deserialize(&mut instruction_data)?; + /// let vault_bump_seed = instr.vault_bump_seed; + /// let lamports = instr.lamports; + /// let vault_size = VAULT_ACCOUNT_SIZE; + /// + /// // Invoke the system program to create an account while virtually + /// // signing with the vault PDA, which is owned by this caller program. + /// invoke_signed( + /// &system_instruction::create_account( + /// &payer.key, + /// &vault.key, + /// lamports, + /// vault_size, + /// &program_id, + /// ), + /// &[ + /// payer.clone(), + /// vault.clone(), + /// ], + /// // A slice of seed slices, each seed slice being the set + /// // of seeds used to generate one of the PDAs required by the + /// // callee program, the final seed being a single-element slice + /// // containing the `u8` bump seed. + /// &[ + /// &[ + /// b"vault", + /// payer.key.as_ref(), + /// &[vault_bump_seed], + /// ], + /// ] + /// )?; + /// + /// Ok(()) + /// } + /// ``` + /// + /// The client program: + /// + /// ```ignore + /// # // NB: This example depends on solana_sdk and solana_client, and adding + /// # // those as dev-dependencies would create an unpublishable circular + /// # // dependency, hence it is ignored. + /// # + /// # use borsh::{BorshSerialize, BorshDeserialize}; + /// # use solana_program::pubkey::Pubkey; + /// # use solana_program::instruction::Instruction; + /// # use solana_program::hash::Hash; + /// # use solana_program::instruction::AccountMeta; + /// # use solana_program::system_program; + /// # use solana_sdk::signature::Keypair; + /// # use solana_sdk::signature::{Signer, Signature}; + /// # use solana_sdk::transaction::Transaction; + /// # use solana_client::rpc_client::RpcClient; + /// # use std::convert::TryFrom; + /// # + /// # #[derive(BorshSerialize, BorshDeserialize, Debug)] + /// # struct InstructionData { + /// # pub vault_bump_seed: u8, + /// # pub lamports: u64, + /// # } + /// # + /// # pub static VAULT_ACCOUNT_SIZE: u64 = 1024; + /// # let program_id = Pubkey::new_unique(); + /// # let payer = Keypair::new(); + /// # let rpc_client = RpcClient::new("no-run".to_string()); + /// # + /// // Derive the PDA from the payer account, a string representing the unique + /// // purpose of the account ("vault"), and the address of our on-chain program. + /// let (vault_pubkey, vault_bump_seed) = Pubkey::find_program_address( + /// &[b"vault", payer.pubkey().as_ref()], + /// &program_id + /// ); + /// + /// // Get the amount of lamports needed to pay for the vault's rent + /// let vault_account_size = usize::try_from(VAULT_ACCOUNT_SIZE)?; + /// let lamports = rpc_client.get_minimum_balance_for_rent_exemption(vault_account_size)?; + /// + /// // The on-chain program's instruction data, imported from that program's crate. + /// let instr_data = InstructionData { + /// vault_bump_seed, + /// lamports, + /// }; + /// + /// // The accounts required by both our on-chain program and the system program's + /// // `create_account` instruction, including the vault's address. + /// let accounts = vec![ + /// AccountMeta::new(payer.pubkey(), true), + /// AccountMeta::new(vault_pubkey, false), + /// AccountMeta::new(system_program::ID, false), + /// ]; + /// + /// // Create the instruction by serializing our instruction data via borsh + /// let instruction = Instruction::new_with_borsh( + /// program_id, + /// &instr_data, + /// accounts, + /// ); + /// + /// let blockhash = rpc_client.get_latest_blockhash()?; + /// + /// let transaction = Transaction::new_signed_with_payer( + /// &[instruction], + /// Some(&payer.pubkey()), + /// &[&payer], + /// blockhash, + /// ); + /// + /// rpc_client.send_and_confirm_transaction(&transaction)?; + /// # Ok::<(), anyhow::Error>(()) + /// ``` + pub fn find_program_address(seeds: &[&[u8]], program_id: &Pubkey) -> (Pubkey, u8) { + Self::try_find_program_address(seeds, program_id) + .unwrap_or_else(|| panic!("Unable to find a viable program address bump seed")) + } + + /// Find a valid [program derived address][pda] and its corresponding bump seed. + /// + /// [pda]: https://docs.solana.com/developing/programming-model/calling-between-programs#program-derived-addresses + /// + /// The only difference between this method and [`find_program_address`] + /// is that this one returns `None` in the statistically improbable event + /// that a bump seed cannot be found; or if any of `find_program_address`'s + /// preconditions are violated. + /// + /// See the documentation for [`find_program_address`] for a full description. + /// + /// [`find_program_address`]: Pubkey::find_program_address + #[allow(clippy::same_item_push)] + pub fn try_find_program_address(seeds: &[&[u8]], program_id: &Pubkey) -> Option<(Pubkey, u8)> { + // Perform the calculation inline, calling this from within a program is + // not supported + #[cfg(not(target_arch = "bpf"))] + { + let mut bump_seed = [std::u8::MAX]; + for _ in 0..std::u8::MAX { + { + let mut seeds_with_bump = seeds.to_vec(); + seeds_with_bump.push(&bump_seed); + match Self::create_program_address(&seeds_with_bump, program_id) { + Ok(address) => return Some((address, bump_seed[0])), + Err(PubkeyError::InvalidSeeds) => (), + _ => break, + } + } + bump_seed[0] -= 1; + } + None + } + // Call via a system call to perform the calculation + #[cfg(target_arch = "bpf")] + { + extern "C" { + fn sol_try_find_program_address( + seeds_addr: *const u8, + seeds_len: u64, + program_id_addr: *const u8, + address_bytes_addr: *const u8, + bump_seed_addr: *const u8, + ) -> u64; + } + let mut bytes = [0; 32]; + let mut bump_seed = std::u8::MAX; + let result = unsafe { + sol_try_find_program_address( + seeds as *const _ as *const u8, + seeds.len() as u64, + program_id as *const _ as *const u8, + &mut bytes as *mut _ as *mut u8, + &mut bump_seed as *mut _ as *mut u8, + ) + }; + match result { + crate::entrypoint::SUCCESS => Some((Pubkey::new(&bytes), bump_seed)), + _ => None, + } + } + } + + /// Create a valid [program derived address][pda] without a bump seed. + /// + /// [pda]: https://docs.solana.com/developing/programming-model/calling-between-programs#program-derived-addresses + /// + /// **Because this function does not create a bump seed, it may unpredictably + /// return an error and should not be used. It exists for backwards + /// compatibility reasons.** + /// + /// See the documentation for [`find_program_address`] for a full description. + /// + /// [`find_program_address`]: Pubkey::find_program_address pub fn create_program_address( seeds: &[&[u8]], program_id: &Pubkey, @@ -259,91 +549,6 @@ impl Pubkey { } } - /// Find a valid program address and its corresponding bump seed which must - /// be passed as an additional seed when calling `invoke_signed`. - /// - /// Panics in the very unlikely event that the additional seed could not be - /// found. - /// - /// The processes of finding a valid program address is by trial and error, - /// and even though it is deterministic given a set of inputs it can take a - /// variable amount of time to succeed across different inputs. This means - /// that when called from an on-chain program it may incur a variable amount - /// of the program's compute budget. Programs that are meant to be very - /// performant may not want to use this function because it could take a - /// considerable amount of time. Also, programs that area already at risk - /// of exceeding their compute budget should also call this with care since - /// there is a chance that the program's budget may be occasionally - /// exceeded. - pub fn find_program_address(seeds: &[&[u8]], program_id: &Pubkey) -> (Pubkey, u8) { - Self::try_find_program_address(seeds, program_id) - .unwrap_or_else(|| panic!("Unable to find a viable program address bump seed")) - } - - /// Find a valid program address and its corresponding bump seed which must - /// be passed as an additional seed when calling `invoke_signed`. - /// - /// The processes of finding a valid program address is by trial and error, - /// and even though it is deterministic given a set of inputs it can take a - /// variable amount of time to succeed across different inputs. This means - /// that when called from an on-chain program it may incur a variable amount - /// of the program's compute budget. Programs that are meant to be very - /// performant may not want to use this function because it could take a - /// considerable amount of time. Also, programs that area already at risk - /// of exceeding their compute budget should also call this with care since - /// there is a chance that the program's budget may be occasionally - /// exceeded. - #[allow(clippy::same_item_push)] - pub fn try_find_program_address(seeds: &[&[u8]], program_id: &Pubkey) -> Option<(Pubkey, u8)> { - // Perform the calculation inline, calling this from within a program is - // not supported - #[cfg(not(target_arch = "bpf"))] - { - let mut bump_seed = [std::u8::MAX]; - for _ in 0..std::u8::MAX { - { - let mut seeds_with_bump = seeds.to_vec(); - seeds_with_bump.push(&bump_seed); - match Self::create_program_address(&seeds_with_bump, program_id) { - Ok(address) => return Some((address, bump_seed[0])), - Err(PubkeyError::InvalidSeeds) => (), - _ => break, - } - } - bump_seed[0] -= 1; - } - None - } - // Call via a system call to perform the calculation - #[cfg(target_arch = "bpf")] - { - extern "C" { - fn sol_try_find_program_address( - seeds_addr: *const u8, - seeds_len: u64, - program_id_addr: *const u8, - address_bytes_addr: *const u8, - bump_seed_addr: *const u8, - ) -> u64; - } - let mut bytes = [0; 32]; - let mut bump_seed = std::u8::MAX; - let result = unsafe { - sol_try_find_program_address( - seeds as *const _ as *const u8, - seeds.len() as u64, - program_id as *const _ as *const u8, - &mut bytes as *mut _ as *mut u8, - &mut bump_seed as *mut _ as *mut u8, - ) - }; - match result { - crate::entrypoint::SUCCESS => Some((Pubkey::new(&bytes), bump_seed)), - _ => None, - } - } - } - pub fn to_bytes(self) -> [u8; 32] { self.0 }