Browse Source

lang: Move program check to try_from (#660)

Armani Ferrante 4 years ago
parent
commit
f4f60d7fab

+ 1 - 0
CHANGELOG.md

@@ -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
 

+ 0 - 1
docs/src/.vuepress/config.js

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

+ 45 - 68
docs/src/tutorials/tutorial-4.md

@@ -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
-
-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.
-
-### How it works
-
-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).
+For example,
 
-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.
+```rust
+use anchor_lang::prelude::*;
 
-## Using the client
+#[program]
+mod errors {
+    use super::*;
+    pub fn hello(_ctx: Context<Hello>) -> Result<()> {
+        Err(ErrorCode::Hello.into())
+    }
+}
 
-### Invoke the constructor
+#[derive(Accounts)]
+pub struct Hello {}
 
-To access the `#[state]` account and associated instructions, you can use the
-`anchor.state` namespace on the client. For example, to invoke the constructor,
-
-<<< @/../examples/tutorial/basic-4/tests/basic-4.js#ctor
-
-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.
-
-### Fetch the state
-
-To fetch the state account,
-
-<<< @/../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
+#[error]
+pub enum ErrorCode {
+    #[msg("This is an error message clients will automatically display")]
+    Hello,
+}
+```
 
-## CPI
+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.
 
-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:
+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`.
 
-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).
+::: 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.
+:::
 
-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).
+## Using the Client
 
-## Conclusion
+When using the client, we get the error message.
 
-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.
+```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);
+}
+```
 
-## Next Steps
+It's that easy. :)
 
-Next we'll discuss errors.
+To run the full example, go [here](https://github.com/project-serum/anchor/tree/master/examples/errors).

+ 0 - 61
docs/src/tutorials/tutorial-5.md

@@ -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).

+ 6 - 6
examples/cfo/programs/cfo/src/lib.rs

@@ -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(),

+ 1 - 1
lang/src/cpi_account.rs

@@ -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 }
     }
 

+ 12 - 6
lang/src/loader.rs

@@ -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)
     }
 }

+ 16 - 11
lang/src/program_account.rs

@@ -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)
     }
 }
 

+ 13 - 14
lang/src/state.rs

@@ -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)
     }
 }
 

+ 2 - 0
lang/syn/src/codegen/accounts/constraints.rs

@@ -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(),
                 )?
             },

+ 2 - 2
lang/syn/src/codegen/program/handlers.rs

@@ -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.