lang: Move program check to try_from (#660)

This commit is contained in:
Armani Ferrante 2021-09-01 14:53:39 -07:00 committed by GitHub
parent afa218f797
commit f4f60d7fab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 98 additions and 170 deletions

View File

@ -28,6 +28,7 @@ incremented for features.
* lang: `bump` must be provided when using the `seeds` constraint. This has been added as an extra safety constraint to ensure that whenever a PDA is initialized via a constraint the bump used is the one created by `Pubkey::find_program_address` ([#641](https://github.com/project-serum/anchor/pull/641)).
* lang: `try_from_init` has been removed from `Loader`, `ProgramAccount`, and `CpiAccount` and replaced with `try_from_unchecked` ([#641](https://github.com/project-serum/anchor/pull/641)).
* lang: Remove `AccountsInit` trait ([#641](https://github.com/project-serum/anchor/pull/641)).
* lang: `try_from` methods for `ProgramAccount`, `Loader`, and `ProgramState` now take in an additional `program_id: &Pubkey` parameter ([#660](https://github.com/project-serum/anchor/pull/660)).
## [0.13.2] - 2021-08-11

View File

@ -64,7 +64,6 @@ module.exports = {
"/tutorials/tutorial-2",
"/tutorials/tutorial-3",
"/tutorials/tutorial-4",
"/tutorials/tutorial-5",
],
},
{

View File

@ -1,84 +1,61 @@
# State structs
# Errors
Up until now, we've treated programs on Solana as stateless, using accounts to persist
state between instruction invocations. In this tutorial, we'll give Solana programs the
illusion of state by introducing state structs, which define program account
singletons that can be operated over like any other account.
## Clone the Repo
To get started, clone the repo.
```bash
git clone https://github.com/project-serum/anchor
```
And change directories to the [example](https://github.com/project-serum/anchor/tree/master/examples/tutorial/basic-4).
```bash
cd anchor/examples/tutorial/basic-4
```
If you've ever programmed on a blockchain, you've probably been frustrated by
either non existant or opaque error codes. Anchor attempts to address this by
providing the `#[error]` attribute, which can be used to create typed Errors with
descriptive messages that automatically propagate to the client.
## Defining a Program
<<< @/../examples/tutorial/basic-4/programs/basic-4/src/lib.rs#code
For example,
Unlike the previous examples, all the instructions here not only take in an `Accounts`
struct, but they also operate over a mutable, global account marked by the `#[state]`
attribute. Every instruction defined in the corresponding `impl` block will have access
to this account, making it a great place to store global program state.
```rust
use anchor_lang::prelude::*;
### How it works
#[program]
mod errors {
use super::*;
pub fn hello(_ctx: Context<Hello>) -> Result<()> {
Err(ErrorCode::Hello.into())
}
}
We are able to give a program the illusion of state by adopting conventions in the framework. When invoking the `new` constructor, Anchor will automatically create a
program-owned account inside the program itself, invoking the system program's [create_account_with_seed](https://docs.rs/solana-program/1.5.5/solana_program/system_instruction/fn.create_account_with_seed.html) instruction, using `Pubkey::find_program_address(&[], program_id)` as the **base** and a deterministic string as the **seed** (the string doesn't
matter, as long as the framework is consistent).
#[derive(Accounts)]
pub struct Hello {}
This all has the effect of
giving the `#[state]` account a deterministic address, and so as long as all clients
and programs adopt this convention, programs can have the illusion of state in addition
to the full power of the lower level Solana accounts API. Of course, Anchor will handle this all for you, so you never have to worry about these details.
#[error]
pub enum ErrorCode {
#[msg("This is an error message clients will automatically display")]
Hello,
}
```
## Using the client
Observe the [#[error]](https://docs.rs/anchor-lang/latest/anchor_lang/attr.error.html) attribute on the `ErrorCode` enum. This macro generates two types: an `Error` and a `Result`, both of which can be used when returning from your program.
### Invoke the constructor
To use the `Error`, you can simply use the user defined `ErrorCode` with Rust's [From](https://doc.rust-lang.org/std/convert/trait.From.html) trait. If you're unfamiliar with `From`, no worries. Just know that you need to either call
`.into()` when using your `ErrorCode`. Or use Rust's `?` operator, when returning an error.
Both of these will automatically convert *into* the correct `Error`.
To access the `#[state]` account and associated instructions, you can use the
`anchor.state` namespace on the client. For example, to invoke the constructor,
::: details
What's the deal with this From stuff? Well, because the Solana runtime expects a [ProgramError](https://docs.rs/solana-program/1.5.5/solana_program/program_error/enum.ProgramError.html) in the return value. The framework needs to wrap the user defined error code into a
`ProgramError::Code` variant, before returning. The alternative would be to use the
`ProgramError` directly.
:::
<<< @/../examples/tutorial/basic-4/tests/basic-4.js#ctor
## Using the Client
Note that the constructor can only be invoked once per program. All subsequent calls
to it will fail, since, as explained above, an account at a deterministic address
will be created.
When using the client, we get the error message.
### Fetch the state
```javascript
try {
const tx = await program.rpc.hello();
assert.ok(false);
} catch (err) {
const errMsg = "This is an error message clients will automatically display";
assert.equal(err.toString(), errMsg);
}
```
To fetch the state account,
It's that easy. :)
<<< @/../examples/tutorial/basic-4/tests/basic-4.js#accessor
### Invoke an instruction
To invoke an instruction,
<<< @/../examples/tutorial/basic-4/tests/basic-4.js#instruction
## CPI
Performing CPI from one Anchor program to another's state methods is very similar to performing CPI to normal Anchor instructions, except for two differences:
1. All the generated instructions are located under the `<my_program>::cpi::state` module.
2. You must use a [CpiStateContext](https://docs.rs/anchor-lang/latest/anchor_lang/struct.CpiStateContext.html), instead of a `[CpiContext](https://docs.rs/anchor-lang/latest/anchor_lang/struct.CpiContext.html).
For a full example, see the `test_state_cpi` instruction, [here](https://github.com/project-serum/anchor/blob/master/examples/misc/programs/misc/src/lib.rs#L39).
## Conclusion
Using state structs is intuitive. However, due to the fact that accounts
on Solana have a fixed size, applications often need to use accounts
directly in addition to `#[state]` stucts.
## Next Steps
Next we'll discuss errors.
To run the full example, go [here](https://github.com/project-serum/anchor/tree/master/examples/errors).

View File

@ -1,61 +0,0 @@
# Errors
If you've ever programmed on a blockchain, you've probably been frustrated by
either non existant or opaque error codes. Anchor attempts to address this by
providing the `#[error]` attribute, which can be used to create typed Errors with
descriptive messages that automatically propagate to the client.
## Defining a Program
For example,
```rust
use anchor_lang::prelude::*;
#[program]
mod errors {
use super::*;
pub fn hello(_ctx: Context<Hello>) -> Result<()> {
Err(ErrorCode::Hello.into())
}
}
#[derive(Accounts)]
pub struct Hello {}
#[error]
pub enum ErrorCode {
#[msg("This is an error message clients will automatically display")]
Hello,
}
```
Observe the [#[error]](https://docs.rs/anchor-lang/latest/anchor_lang/attr.error.html) attribute on the `ErrorCode` enum. This macro generates two types: an `Error` and a `Result`, both of which can be used when returning from your program.
To use the `Error`, you can simply use the user defined `ErrorCode` with Rust's [From](https://doc.rust-lang.org/std/convert/trait.From.html) trait. If you're unfamiliar with `From`, no worries. Just know that you need to either call
`.into()` when using your `ErrorCode`. Or use Rust's `?` operator, when returning an error.
Both of these will automatically convert *into* the correct `Error`.
::: details
What's the deal with this From stuff? Well, because the Solana runtime expects a [ProgramError](https://docs.rs/solana-program/1.5.5/solana_program/program_error/enum.ProgramError.html) in the return value. The framework needs to wrap the user defined error code into a
`ProgramError::Code` variant, before returning. The alternative would be to use the
`ProgramError` directly.
:::
## Using the Client
When using the client, we get the error message.
```javascript
try {
const tx = await program.rpc.hello();
assert.ok(false);
} catch (err) {
const errMsg = "This is an error message clients will automatically display";
assert.equal(err.toString(), errMsg);
}
```
It's that easy. :)
To run the full example, go [here](https://github.com/project-serum/anchor/tree/master/examples/errors).

View File

@ -703,10 +703,10 @@ impl<'info> DropStakeReward<'info> {
fn into_srm_reward(&self) -> CpiContext<'_, '_, '_, 'info, registry::DropReward<'info>> {
let program = self.registry_program.clone();
let accounts = registry::DropReward {
registrar: ProgramAccount::try_from(&self.srm.registrar).unwrap(),
reward_event_q: ProgramAccount::try_from(&self.srm.reward_event_q).unwrap(),
registrar: ProgramAccount::try_from(program.key, &self.srm.registrar).unwrap(),
reward_event_q: ProgramAccount::try_from(program.key, &self.srm.reward_event_q).unwrap(),
pool_mint: self.srm.pool_mint.clone(),
vendor: ProgramAccount::try_from(&self.srm.vendor).unwrap(),
vendor: ProgramAccount::try_from(program.key, &self.srm.vendor).unwrap(),
vendor_vault: CpiAccount::try_from(&self.srm.vendor_vault).unwrap(),
depositor: self.stake.to_account_info(),
depositor_authority: self.officer.to_account_info(),
@ -720,10 +720,10 @@ impl<'info> DropStakeReward<'info> {
fn into_msrm_reward(&self) -> CpiContext<'_, '_, '_, 'info, registry::DropReward<'info>> {
let program = self.registry_program.clone();
let accounts = registry::DropReward {
registrar: ProgramAccount::try_from(&self.msrm.registrar).unwrap(),
reward_event_q: ProgramAccount::try_from(&self.msrm.reward_event_q).unwrap(),
registrar: ProgramAccount::try_from(program.key, &self.msrm.registrar).unwrap(),
reward_event_q: ProgramAccount::try_from(program.key, &self.msrm.reward_event_q).unwrap(),
pool_mint: self.msrm.pool_mint.clone(),
vendor: ProgramAccount::try_from(&self.msrm.vendor).unwrap(),
vendor: ProgramAccount::try_from(program.key, &self.msrm.vendor).unwrap(),
vendor_vault: CpiAccount::try_from(&self.msrm.vendor_vault).unwrap(),
depositor: self.stake.to_account_info(),
depositor_authority: self.officer.to_account_info(),

View File

@ -17,7 +17,7 @@ pub struct CpiAccount<'a, T: AccountDeserialize + Clone> {
}
impl<'a, T: AccountDeserialize + Clone> CpiAccount<'a, T> {
pub fn new(info: AccountInfo<'a>, account: Box<T>) -> CpiAccount<'a, T> {
fn new(info: AccountInfo<'a>, account: Box<T>) -> CpiAccount<'a, T> {
Self { info, account }
}

View File

@ -39,9 +39,14 @@ impl<'info, T: ZeroCopy> Loader<'info, T> {
/// Constructs a new `Loader` from a previously initialized account.
#[inline(never)]
pub fn try_from(acc_info: &AccountInfo<'info>) -> Result<Loader<'info, T>, ProgramError> {
pub fn try_from(
program_id: &Pubkey,
acc_info: &AccountInfo<'info>,
) -> Result<Loader<'info, T>, ProgramError> {
if acc_info.owner != program_id {
return Err(ErrorCode::AccountNotProgramOwned.into());
}
let data: &[u8] = &acc_info.try_borrow_data()?;
// Discriminator must match.
let mut disc_bytes = [0u8; 8];
disc_bytes.copy_from_slice(&data[..8]);
@ -55,8 +60,12 @@ impl<'info, T: ZeroCopy> Loader<'info, T> {
/// Constructs a new `Loader` from an uninitialized account.
#[inline(never)]
pub fn try_from_unchecked(
program_id: &Pubkey,
acc_info: &AccountInfo<'info>,
) -> Result<Loader<'info, T>, ProgramError> {
if acc_info.owner != program_id {
return Err(ErrorCode::AccountNotProgramOwned.into());
}
Ok(Loader::new(acc_info.clone()))
}
@ -131,10 +140,7 @@ impl<'info, T: ZeroCopy> Accounts<'info> for Loader<'info, T> {
}
let account = &accounts[0];
*accounts = &accounts[1..];
let l = Loader::try_from(account)?;
if l.acc_info.owner != program_id {
return Err(ErrorCode::AccountNotProgramOwned.into());
}
let l = Loader::try_from(program_id, account)?;
Ok(l)
}
}

View File

@ -24,7 +24,7 @@ struct Inner<'info, T: AccountSerialize + AccountDeserialize + Clone> {
}
impl<'a, T: AccountSerialize + AccountDeserialize + Clone> ProgramAccount<'a, T> {
pub fn new(info: AccountInfo<'a>, account: T) -> ProgramAccount<'a, T> {
fn new(info: AccountInfo<'a>, account: T) -> ProgramAccount<'a, T> {
Self {
inner: Box::new(Inner { info, account }),
}
@ -32,7 +32,13 @@ impl<'a, T: AccountSerialize + AccountDeserialize + Clone> ProgramAccount<'a, T>
/// Deserializes the given `info` into a `ProgramAccount`.
#[inline(never)]
pub fn try_from(info: &AccountInfo<'a>) -> Result<ProgramAccount<'a, T>, ProgramError> {
pub fn try_from(
program_id: &Pubkey,
info: &AccountInfo<'a>,
) -> Result<ProgramAccount<'a, T>, ProgramError> {
if info.owner != program_id {
return Err(ErrorCode::AccountNotProgramOwned.into());
}
let mut data: &[u8] = &info.try_borrow_data()?;
Ok(ProgramAccount::new(
info.clone(),
@ -40,14 +46,17 @@ impl<'a, T: AccountSerialize + AccountDeserialize + Clone> ProgramAccount<'a, T>
))
}
/// Deserializes the zero-initialized `info` into a `ProgramAccount` without
/// checking the account type. This should only be used upon program account
/// initialization (since the entire account data array is zeroed and thus
/// no account type is set).
/// Deserializes the given `info` into a `ProgramAccount` without checking
/// the account discriminator. Be careful when using this and avoid it if
/// possible.
#[inline(never)]
pub fn try_from_unchecked(
program_id: &Pubkey,
info: &AccountInfo<'a>,
) -> Result<ProgramAccount<'a, T>, ProgramError> {
if info.owner != program_id {
return Err(ErrorCode::AccountNotProgramOwned.into());
}
let mut data: &[u8] = &info.try_borrow_data()?;
Ok(ProgramAccount::new(
info.clone(),
@ -75,11 +84,7 @@ where
}
let account = &accounts[0];
*accounts = &accounts[1..];
let pa = ProgramAccount::try_from(account)?;
if pa.inner.info.owner != program_id {
return Err(ErrorCode::AccountNotProgramOwned.into());
}
Ok(pa)
ProgramAccount::try_from(program_id, account)
}
}

View File

@ -25,7 +25,7 @@ struct Inner<'info, T: AccountSerialize + AccountDeserialize + Clone> {
}
impl<'a, T: AccountSerialize + AccountDeserialize + Clone> ProgramState<'a, T> {
pub fn new(info: AccountInfo<'a>, account: T) -> ProgramState<'a, T> {
fn new(info: AccountInfo<'a>, account: T) -> ProgramState<'a, T> {
Self {
inner: Box::new(Inner { info, account }),
}
@ -33,7 +33,17 @@ impl<'a, T: AccountSerialize + AccountDeserialize + Clone> ProgramState<'a, T> {
/// Deserializes the given `info` into a `ProgramState`.
#[inline(never)]
pub fn try_from(info: &AccountInfo<'a>) -> Result<ProgramState<'a, T>, ProgramError> {
pub fn try_from(
program_id: &Pubkey,
info: &AccountInfo<'a>,
) -> Result<ProgramState<'a, T>, ProgramError> {
if info.owner != program_id {
return Err(ErrorCode::AccountNotProgramOwned.into());
}
if info.key != &Self::address(program_id) {
solana_program::msg!("Invalid state address");
return Err(ErrorCode::StateInvalidAddress.into());
}
let mut data: &[u8] = &info.try_borrow_data()?;
Ok(ProgramState::new(
info.clone(),
@ -65,18 +75,7 @@ where
}
let account = &accounts[0];
*accounts = &accounts[1..];
if account.key != &Self::address(program_id) {
solana_program::msg!("Invalid state address");
return Err(ErrorCode::StateInvalidAddress.into());
}
let pa = ProgramState::try_from(account)?;
if pa.inner.info.owner != program_id {
solana_program::msg!("Invalid state owner");
return Err(ErrorCode::AccountNotProgramOwned.into());
}
Ok(pa)
ProgramState::try_from(program_id, account)
}
}

View File

@ -153,6 +153,7 @@ pub fn generate_constraint_zeroed(f: &Field, _c: &ConstraintZeroed) -> proc_macr
return Err(anchor_lang::__private::ErrorCode::ConstraintZero.into());
}
#account_wrapper_ty::try_from_unchecked(
program_id,
&#field,
)?
};
@ -439,6 +440,7 @@ pub fn generate_pda(
},
quote! {
#account_wrapper_ty::try_from_unchecked(
program_id,
&#field.to_account_info(),
)?
},

View File

@ -227,7 +227,7 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream {
)?;
// Zero copy deserialize.
let loader: anchor_lang::Loader<#mod_name::#name> = anchor_lang::Loader::try_from_unchecked(&ctor_accounts.to)?;
let loader: anchor_lang::Loader<#mod_name::#name> = anchor_lang::Loader::try_from_unchecked(program_id, &ctor_accounts.to)?;
// Invoke the ctor in a new lexical scope so that
// the zero-copy RefMut gets dropped. Required
@ -367,7 +367,7 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream {
return Err(anchor_lang::__private::ErrorCode::AccountNotEnoughKeys.into());
}
let state_account = &remaining_accounts[0];
let loader: anchor_lang::Loader<#mod_name::#name> = anchor_lang::Loader::try_from(&state_account)?;
let loader: anchor_lang::Loader<#mod_name::#name> = anchor_lang::Loader::try_from(program_id, &state_account)?;
remaining_accounts = &remaining_accounts[1..];
// Deserialize accounts.