浏览代码

Account discriminators (#14)

Armani Ferrante 4 年之前
父节点
当前提交
de353cb4e4

+ 1 - 1
.travis.yml

@@ -21,7 +21,7 @@ _examples: &examples
   - export PATH="/home/travis/.local/share/solana/install/active_release/bin:$PATH"
   - export NODE_PATH="/home/travis/.nvm/versions/node/v$NODE_VERSION/lib/node_modules/:$NODE_PATH"
   - yes | solana-keygen new
-  - cargo install --git https://github.com/project-serum/anchor anchor-cli
+  - cargo install --git https://github.com/project-serum/anchor anchor-cli --locked
 
 jobs:
   include:

+ 18 - 6
Cargo.lock

@@ -47,9 +47,10 @@ dependencies = [
 name = "anchor"
 version = "0.1.0"
 dependencies = [
- "anchor-attributes-access-control",
- "anchor-attributes-program",
- "anchor-derive",
+ "anchor-attribute-access-control",
+ "anchor-attribute-account",
+ "anchor-attribute-program",
+ "anchor-derive-accounts",
  "borsh",
  "solana-program",
  "solana-sdk",
@@ -57,7 +58,7 @@ dependencies = [
 ]
 
 [[package]]
-name = "anchor-attributes-access-control"
+name = "anchor-attribute-access-control"
 version = "0.1.0"
 dependencies = [
  "anchor-syn",
@@ -68,7 +69,18 @@ dependencies = [
 ]
 
 [[package]]
-name = "anchor-attributes-program"
+name = "anchor-attribute-account"
+version = "0.1.0"
+dependencies = [
+ "anchor-syn",
+ "anyhow",
+ "proc-macro2 1.0.24",
+ "quote 1.0.8",
+ "syn 1.0.54",
+]
+
+[[package]]
+name = "anchor-attribute-program"
 version = "0.1.0"
 dependencies = [
  "anchor-syn",
@@ -99,7 +111,7 @@ dependencies = [
 ]
 
 [[package]]
-name = "anchor-derive"
+name = "anchor-derive-accounts"
 version = "0.1.0"
 dependencies = [
  "anchor-syn",

+ 6 - 5
Cargo.toml

@@ -13,15 +13,16 @@ default = []
 thiserror = "1.0.20"
 solana-program = "1.4.3"
 solana-sdk = { version = "1.3.14", default-features = false, features = ["program"] }
-anchor-derive = { path = "./derive" }
-anchor-attributes-program = { path = "./attributes/program" }
-anchor-attributes-access-control = { path = "./attributes/access-control" }
+anchor-derive-accounts = { path = "./derive/accounts" }
+anchor-attribute-program = { path = "./attribute/program" }
+anchor-attribute-access-control = { path = "./attribute/access-control" }
+anchor-attribute-account = { path = "./attribute/account" }
 borsh = { git = "https://github.com/project-serum/borsh", branch = "serum", features = ["serum-program"] }
 
 [workspace]
 members = [
     "cli",
     "syn",
-    "attributes/*",
-    "derive",
+    "attribute/*",
+    "derive/*",
 ]

+ 1 - 0
README.md

@@ -91,6 +91,7 @@ purposes of the Accounts macro) that can be specified on a struct deriving `Acco
 |:--|:--|:--|
 | `#[account(signer)]` | On raw `AccountInfo` structs. | Checks the given account signed the transaction. |
 | `#[account(mut)]` | On `ProgramAccount` structs. | Marks the account as mutable and persists the state transition. |
+| `#[account(init)]` | On `ProgramAccount` structs. | Marks the account as being initialized, skipping the account discriminator check. |
 | `#[account(belongs_to = <target>)]` | On `ProgramAccount` structs | Checks the `target` field on the account matches the `target` field in the accounts array. |
 | `#[account(owner = program \| skip)]` | On `ProgramAccount` and `AccountInfo` structs | Checks the owner of the account is the current program or skips the check. |
 | `#[account("<literal>")]` | On `ProgramAccount` structs | Executes the given code literal as a constraint. The literal should evaluate to a boolean. |

+ 1 - 1
attributes/program/Cargo.toml → attribute/access-control/Cargo.toml

@@ -1,5 +1,5 @@
 [package]
-name = "anchor-attributes-program"
+name = "anchor-attribute-access-control"
 version = "0.1.0"
 authors = ["armaniferrante <armaniferrante@gmail.com>"]
 edition = "2018"

+ 3 - 0
attributes/access-control/src/lib.rs → attribute/access-control/src/lib.rs

@@ -3,6 +3,9 @@ extern crate proc_macro;
 use quote::quote;
 use syn::parse_macro_input;
 
+/// Executes the given access control method before running the decorated
+/// instruction handler. Any method in scope of the attribute can be invoked
+/// with any arguments from the associated instruction handler.
 #[proc_macro_attribute]
 pub fn access_control(
     args: proc_macro::TokenStream,

+ 2 - 2
attributes/access-control/Cargo.toml → attribute/account/Cargo.toml

@@ -1,5 +1,5 @@
 [package]
-name = "anchor-attributes-access-control"
+name = "anchor-attribute-account"
 version = "0.1.0"
 authors = ["armaniferrante <armaniferrante@gmail.com>"]
 edition = "2018"
@@ -12,4 +12,4 @@ proc-macro2 = "1.0"
 quote = "1.0"
 syn = { version = "1.0.54", features = ["full"] }
 anyhow = "1.0.32"
-anchor-syn = { path = "../../syn" }
+anchor-syn = { path = "../../syn" }

+ 75 - 0
attribute/account/src/lib.rs

@@ -0,0 +1,75 @@
+extern crate proc_macro;
+
+use quote::quote;
+use syn::parse_macro_input;
+
+/// A data structure representing a Solana account.
+#[proc_macro_attribute]
+pub fn account(
+    _args: proc_macro::TokenStream,
+    input: proc_macro::TokenStream,
+) -> proc_macro::TokenStream {
+    let account_strct = parse_macro_input!(input as syn::ItemStruct);
+    let account_name = &account_strct.ident;
+    // Namespace the discriminator to prevent future collisions, e.g.,
+    // if we (for some unforseen reason) wanted to hash other parts of the
+    // program.
+    let discriminator_preimage = format!("account:{}", account_name.to_string());
+
+    let coder = quote! {
+        impl anchor::AccountSerialize for #account_name {
+            fn try_serialize<W: std::io::Write>(&self, writer: &mut W) -> Result<(), ProgramError> {
+                // TODO: we shouldn't have to hash at runtime. However, rust
+                //       is not happy when trying to include solana-sdk from
+                //       the proc-macro crate.
+                let mut discriminator = [0u8; 8];
+                discriminator.copy_from_slice(
+                    &solana_program::hash::hash(
+                        #discriminator_preimage.as_bytes(),
+                    ).to_bytes()[..8],
+                );
+
+                writer.write_all(&discriminator).map_err(|_| ProgramError::InvalidAccountData)?;
+                AnchorSerialize::serialize(
+                    self,
+                    writer
+                )
+                    .map_err(|_| ProgramError::InvalidAccountData)?;
+                Ok(())
+            }
+        }
+
+        impl anchor::AccountDeserialize for #account_name {
+            fn try_deserialize(buf: &mut &[u8]) -> Result<Self, ProgramError> {
+                let mut discriminator = [0u8; 8];
+                discriminator.copy_from_slice(
+                    &solana_program::hash::hash(
+                        #discriminator_preimage.as_bytes(),
+                    ).to_bytes()[..8],
+                );
+
+                if buf.len() < discriminator.len() {
+                    return Err(ProgramError::AccountDataTooSmall);
+                }
+                let given_disc = &buf[..8];
+                if &discriminator != given_disc {
+                    return Err(ProgramError::InvalidInstructionData);
+                }
+                Self::try_deserialize_unchecked(buf)
+            }
+
+            fn try_deserialize_unchecked(buf: &mut &[u8]) -> Result<Self, ProgramError> {
+                let mut data: &[u8] = &buf[8..];
+                AnchorDeserialize::deserialize(&mut data)
+                    .map_err(|_| ProgramError::InvalidAccountData)
+            }
+        }
+    };
+
+    proc_macro::TokenStream::from(quote! {
+        #[derive(AnchorSerialize, AnchorDeserialize)]
+        #account_strct
+
+        #coder
+    })
+}

+ 15 - 0
attribute/program/Cargo.toml

@@ -0,0 +1,15 @@
+[package]
+name = "anchor-attribute-program"
+version = "0.1.0"
+authors = ["armaniferrante <armaniferrante@gmail.com>"]
+edition = "2018"
+
+[lib]
+proc-macro = true
+
+[dependencies]
+proc-macro2 = "1.0"
+quote = "1.0"
+syn = { version = "1.0.54", features = ["full"] }
+anyhow = "1.0.32"
+anchor-syn = { path = "../../syn" }

+ 2 - 0
attributes/program/src/lib.rs → attribute/program/src/lib.rs

@@ -4,6 +4,8 @@ use anchor_syn::codegen::program as program_codegen;
 use anchor_syn::parser::program as program_parser;
 use syn::parse_macro_input;
 
+/// The module containing all instruction handlers defining all entries to the
+/// Solana program.
 #[proc_macro_attribute]
 pub fn program(
     _args: proc_macro::TokenStream,

+ 2 - 2
derive/Cargo.toml → derive/accounts/Cargo.toml

@@ -1,5 +1,5 @@
 [package]
-name = "anchor-derive"
+name = "anchor-derive-accounts"
 version = "0.1.0"
 authors = ["armaniferrante <armaniferrante@gmail.com>"]
 edition = "2018"
@@ -12,4 +12,4 @@ proc-macro2 = "1.0"
 quote = "1.0"
 syn = { version = "1.0.54", features = ["full"] }
 anyhow = "1.0.32"
-anchor-syn = { path = "../syn" }
+anchor-syn = { path = "../../syn" }

+ 0 - 0
derive/src/lib.rs → derive/accounts/src/lib.rs


+ 1 - 1
docs/src/getting-started/installation.md

@@ -34,7 +34,7 @@ npm install -g mocha
 For now, we can use Cargo to install the CLI.
 
 ```bash
-cargo install --git https://github.com/project-serum/anchor anchor-cli
+cargo install --git https://github.com/project-serum/anchor anchor-cli --locked
 ```
 
 On Linux systems you may need to install additional dependencies. On Ubuntu,

+ 1 - 1
docs/src/tutorials/tutorial-0.md

@@ -100,7 +100,7 @@ Once built, we can deploy the program by running
 anchor deploy
 ```
 
-Take note of program's deployed address. We'll use it next.
+Take note of the program's deployed address. We'll use it next.
 
 ## Generating a Client
 

+ 47 - 16
docs/src/tutorials/tutorial-1.md

@@ -1,4 +1,4 @@
-# Tutorial 1: Accounts, Arguments, and Types
+# Tutorial 1: Arguments and Accounts
 
 This tutorial covers the basics of creating and mutating accounts using Anchor.
 It's recommended to read [Tutorial 0](./tutorial-0.md) first, as this tutorial will
@@ -22,25 +22,56 @@ cd anchor/examples/tutorial/basic-1
 
 We define our program as follows
 
-<<< @/../examples/tutorial/basic-1/programs/basic-1/src/lib.rs#program
+<<< @/../examples/tutorial/basic-1/programs/basic-1/src/lib.rs
 
 Some new syntax elements are introduced here.
 
-First, notice the `data` argument passed into the program. This argument and any other valid
-Rust types can be passed to the instruction to define inputs to the program. If you'd like to
-pass in your own type, then it must be defined in the same `src/lib.rs` file as the
-`#[program]` module (so that the IDL can pick it up). Additionally,
+### `initialize` instruction
+
+First, let's start with the initialize instruction. Notice the `data` argument passed into the program. This argument and any other valid
+Rust types can be passed to the instruction to define inputs to the program.
+
+::: tip
+If you'd like to pass in your own type as an input to an instruction handler, then it must be
+defined in the same `src/lib.rs` file as the `#[program]` module, so that the IDL parser can
+pick it up.
+:::
+
+Additionally,
 notice how we take a mutable reference to `my_account` and assign the `data` to it. This leads us
-the `Initialize` struct, deriving `Accounts`.
+the `Initialize` struct, deriving `Accounts`. There are two things to notice about `Initialize`.
+
+1. The `my_account` field is of type `ProgramAccount<'info, MyAccount>`, telling the program it *must*
+be **owned** by the currently executing program, and the deserialized data structure is `MyAccount`.
+2. The `my_account` field is marked with the `#[account(init)]` attribute. This should be used
+in one situation: when a given `ProgramAccount` is newly created and is being used by the program
+for the first time (and thus it's data field is all zero). If `#[account(init)]` is not used
+when account data is zero initialized, the transaction will be rejected.
+
+::: details
+All accounts created with Anchor are laid out as follows: `8-byte-discriminator || borsh
+serialized data`. The 8-byte-discriminator is created from the first 8 bytes of the
+`Sha256` hash of the account's type--using the example above, `sha256("MyAccount")[..8]`.
+
+Importantly, this allows a program to know for certain an account is indeed of a given type.
+Without it, a program would be vulnerable to account injection attacks, where a malicious user
+specifies an account of an unexpected type, causing the program to do unexpected things.
+
+On account creation, this 8-byte discriminator doesn't exist, since the account storage is
+zeroed. The first time an Anchor program mutates an account, this discriminator is prepended
+to the account storage array and all subsequent accesses to the account (not decorated with
+`#[account(init)]`) will check for this discriminator.
+:::
+
+### `update` instruction
 
-There are two things to notice about `Initialize`. First, the
-`my_account` field is marked with the `#[account(mut)]` attribute. This means that any
-changes to the field will be persisted upon exiting the program. Second, the field is of
-type `ProgramAccount<'info, MyAccount>`, telling the program it *must* be **owned**
-by the currently executing program and the deserialized data structure is `MyAccount`.
+Similarly, the `Update` accounts struct is marked  with the `#[account(mut)]` attribute.
+Marking an account as `mut` persists any changes made upon exiting the program.
 
-In a later tutorial we'll delve more deeply into deriving `Accounts`. For now, just know
-one must mark their accounts `mut` if they want them to, well, mutate. ;)
+Here we've covered the basics of how to interact with accounts. In a later tutorial,
+we'll delve more deeply into deriving `Accounts`, but for now, just know
+one must mark their accounts `init` when using an account for the first time and `mut`
+for persisting changes.
 
 ## Creating and Initializing Accounts
 
@@ -74,8 +105,8 @@ which in this case is `initialize`. Because we are creating `myAccount`, it need
 sign the transaction, as required by the Solana runtime.
 
 ::: details
-In future work, we might want to add something like a *Builder* pattern for constructing
-common transactions like creating and then initializing an account.
+In future work, we can simplify this example further by using something like a *Builder*
+pattern for constructing common transactions like creating and then initializing an account.
 :::
 
 As before, we can run the example tests.

+ 11 - 4
docs/src/tutorials/tutorial-2.md

@@ -1,11 +1,18 @@
 # Tutorial 2: Account Constraints and Access Control
 
-Building on the previous two, this tutorial covers how to specify constraints and access control
-on accounts.
+This tutorial covers how to specify constraints and access control on accounts.
 
-Because Solana programs are stateless, a transaction must specify accounts to be executed. And because an untrusted client specifies those accounts, a program must responsibily validate all input to the program to ensure it is what it claims to be--in addition to any instruction specific access control the program needs to do. This is particularly burdensome when there are lots of dependencies between accounts, leading to repetitive [boilerplate](https://github.com/project-serum/serum-dex/blob/master/registry/src/access_control.rs) code for account validation along with the ability to easily shoot oneself in the foot by forgetting to validate any particular account.
+Because Solana programs are stateless, a transaction must specify accounts to be executed. And because an untrusted client specifies those accounts, a program must responsibily validate all input to the program to ensure it is what it claims to be--in addition to any instruction specific access control the program needs to do.
 
-For example, one could imagine easily writing a faulty token program that forgets to check if the signer of a transaction claiming to be the owner of a token account actually matches the owner on the account. So one must write an `if` statement to check for all such conditions. Instead, one can use the Anchor DSL to do these checks by specifying **constraints** when deriving `Accounts`.
+For example, one could imagine easily writing a faulty token program that forgets to check if the signer of a transaction claiming to be the owner of a token account actually matches the owner on the account. A simpler question that must be asked: what happens if the program expects a `Mint` account but a `Token` account is given instead?
+
+
+Doing these checks is particularly burdensome when there are lots of dependencies between
+accounts, leading to repetitive [boilerplate](https://github.com/project-serum/serum-dex/blob/master/registry/src/access_control.rs)
+code for account validation along with the ability to easily shoot oneself in the foot.
+Instead, one can use the Anchor DSL to do these checks by specifying **constraints** when deriving
+`Accounts`. We briefly touched on the most basic (and important) type of account constraint in the
+[previous tutorial](./tutorial-1.md), the account discriminator. Here, we demonstrate others.
 
 ## Clone the Repo
 

+ 13 - 1
examples/tutorial/basic-1/programs/basic-1/src/lib.rs

@@ -11,15 +11,27 @@ mod basic_1 {
         my_account.data = data;
         Ok(())
     }
+
+    pub fn update(ctx: Context<Update>, data: u64) -> ProgramResult {
+        let my_account = &mut ctx.accounts.my_account;
+        my_account.data = data;
+        Ok(())
+    }
 }
 
 #[derive(Accounts)]
 pub struct Initialize<'info> {
+    #[account(init)]
+    pub my_account: ProgramAccount<'info, MyAccount>,
+}
+
+#[derive(Accounts)]
+pub struct Update<'info> {
     #[account(mut)]
     pub my_account: ProgramAccount<'info, MyAccount>,
 }
 
-#[derive(AnchorSerialize, AnchorDeserialize)]
+#[account]
 pub struct MyAccount {
     pub data: u64,
 }

+ 34 - 4
examples/tutorial/basic-1/tests/basic-1.js

@@ -22,8 +22,8 @@ describe('basic-1', () => {
       anchor.web3.SystemProgram.createAccount({
         fromPubkey: provider.wallet.publicKey,
         newAccountPubkey: myAccount.publicKey,
-        space: 8,
-        lamports: await provider.connection.getMinimumBalanceForRentExemption(8),
+        space: 8+8,
+        lamports: await provider.connection.getMinimumBalanceForRentExemption(8+8),
         programId: program.programId,
       }),
     );
@@ -47,6 +47,8 @@ describe('basic-1', () => {
     assert.ok(account.data.eq(new anchor.BN(1234)));
   });
 
+  // Reference to an account to use between multiple tests.
+  let _myAccount = undefined;
 
   it('Creates and initializes an account in a single atomic transaction', async () => {
     // The program to execute.
@@ -66,8 +68,8 @@ describe('basic-1', () => {
         anchor.web3.SystemProgram.createAccount({
           fromPubkey: provider.wallet.publicKey,
           newAccountPubkey: myAccount.publicKey,
-          space: 8,
-          lamports: await provider.connection.getMinimumBalanceForRentExemption(8),
+          space: 8+8, // Add 8 for the account discriminator.
+          lamports: await provider.connection.getMinimumBalanceForRentExemption(8+8),
           programId: program.programId,
         }),
       ],
@@ -79,5 +81,33 @@ describe('basic-1', () => {
     // Check it's state was initialized.
     assert.ok(account.data.eq(new anchor.BN(1234)));
     // #endregion code
+
+    // Store the account for the next test.
+    _myAccount = myAccount;
+  });
+
+  it('Updates a previously created account', async () => {
+
+    const myAccount = _myAccount;
+
+    // #region update-test
+
+    // The program to execute.
+    const program = anchor.workspace.Basic1;
+
+    // Invoke the update rpc.
+    await program.rpc.update(new anchor.BN(4321), {
+      accounts: {
+        myAccount: myAccount.publicKey,
+      },
+    });
+
+    // Fetch the newly updated account.
+    const account = await program.account.myAccount(myAccount.publicKey);
+
+    // Check it's state was mutated.
+    assert.ok(account.data.eq(new anchor.BN(4321)));
+
+    // #endregion update-test
   });
 });

+ 7 - 8
examples/tutorial/basic-2/programs/basic-2/src/lib.rs

@@ -50,7 +50,7 @@ mod basic_2 {
 
 #[derive(Accounts)]
 pub struct CreateRoot<'info> {
-    #[account(mut, "!root.initialized")]
+    #[account(init)]
     pub root: ProgramAccount<'info, Root>,
 }
 
@@ -58,15 +58,14 @@ pub struct CreateRoot<'info> {
 pub struct UpdateRoot<'info> {
     #[account(signer)]
     pub authority: AccountInfo<'info>,
-    #[account(mut, "root.initialized", "&root.authority == authority.key")]
+    #[account(mut, "&root.authority == authority.key")]
     pub root: ProgramAccount<'info, Root>,
 }
 
 #[derive(Accounts)]
 pub struct CreateLeaf<'info> {
-    #[account("root.initialized")]
     pub root: ProgramAccount<'info, Root>,
-    #[account(mut, "!leaf.initialized")]
+    #[account(init)]
     pub leaf: ProgramAccount<'info, Leaf>,
 }
 
@@ -74,22 +73,22 @@ pub struct CreateLeaf<'info> {
 pub struct UpdateLeaf<'info> {
     #[account(signer)]
     pub authority: AccountInfo<'info>,
-    #[account("root.initialized", "&root.authority == authority.key")]
+    #[account("&root.authority == authority.key")]
     pub root: ProgramAccount<'info, Root>,
-    #[account(mut, belongs_to = root, "leaf.initialized")]
+    #[account(mut, belongs_to = root)]
     pub leaf: ProgramAccount<'info, Leaf>,
 }
 
 // Define the program owned accounts.
 
-#[derive(AnchorSerialize, AnchorDeserialize)]
+#[account]
 pub struct Root {
     pub initialized: bool,
     pub authority: Pubkey,
     pub data: u64,
 }
 
-#[derive(AnchorSerialize, AnchorDeserialize)]
+#[account]
 pub struct Leaf {
     pub initialized: bool,
     pub root: Pubkey,

+ 86 - 13
src/lib.rs

@@ -1,25 +1,99 @@
 use solana_sdk::account_info::AccountInfo;
 use solana_sdk::program_error::ProgramError;
 use solana_sdk::pubkey::Pubkey;
+use std::io::Write;
 use std::ops::{Deref, DerefMut};
 
-pub use anchor_attributes_access_control::access_control;
-pub use anchor_attributes_program::program;
-pub use anchor_derive::Accounts;
+pub use anchor_attribute_access_control::access_control;
+pub use anchor_attribute_account::account;
+pub use anchor_attribute_program::program;
+pub use anchor_derive_accounts::Accounts;
 pub use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize};
 
-pub struct ProgramAccount<'a, T: AnchorSerialize + AnchorDeserialize> {
+/// A data structure of Solana accounts that can be deserialized from the input
+/// of a Solana program. Due to the freewheeling nature of the accounts array,
+/// implementations of this trait should perform any and all constraint checks
+/// (in addition to any done within `AccountDeserialize`) on accounts to ensure
+/// the accounts maintain any invariants required for the program to run
+/// securely.
+pub trait Accounts<'info>: Sized {
+    fn try_accounts(program_id: &Pubkey, from: &[AccountInfo<'info>])
+        -> Result<Self, ProgramError>;
+}
+
+/// A data structure that can be serialized and stored in an `AccountInfo` data
+/// array.
+///
+/// Implementors of this trait should ensure that any subsequent usage the
+/// `AccountDeserialize` trait succeeds if and only if the account is of the
+/// correct type. For example, the implementation provided by the `#[account]`
+/// attribute sets the first 8 bytes to be a unique account discriminator,
+/// defined as the first 8 bytes of the SHA256 of the account's Rust ident.
+/// Thus, any subsequent  calls via `AccountDeserialize`'s `try_deserialize`
+/// will check this discriminator. If it doesn't match, an invalid account
+/// was given, and the program will exit with an error.
+pub trait AccountSerialize {
+    /// Serilalizes the account data into `writer`.
+    fn try_serialize<W: Write>(&self, writer: &mut W) -> Result<(), ProgramError>;
+}
+
+/// A data structure that can be deserialized from an `AccountInfo` data array.
+pub trait AccountDeserialize: Sized {
+    /// Deserializes the account data.
+    fn try_deserialize(buf: &mut &[u8]) -> Result<Self, ProgramError>;
+
+    /// Deserializes account data without checking the account discriminator.
+    /// This should only be used on account initialization, when the
+    /// discriminator is not yet set (since the entire account data is zeroed).
+    fn try_deserialize_unchecked(buf: &mut &[u8]) -> Result<Self, ProgramError>;
+}
+
+/// A container for a deserialized `account` and raw `AccountInfo` object.
+///
+/// Using this within a data structure deriving `Accounts` will ensure the
+/// account is owned by the currently executing program.
+pub struct ProgramAccount<'a, T: AccountSerialize + AccountDeserialize> {
     pub info: AccountInfo<'a>,
     pub account: T,
 }
 
-impl<'a, T: AnchorSerialize + AnchorDeserialize> ProgramAccount<'a, T> {
+impl<'a, T: AccountSerialize + AccountDeserialize> ProgramAccount<'a, T> {
     pub fn new(info: AccountInfo<'a>, account: T) -> ProgramAccount<'a, T> {
         Self { info, account }
     }
+
+    /// Deserializes the given `info` into a `ProgramAccount`.
+    pub fn try_from(info: &AccountInfo<'a>) -> Result<ProgramAccount<'a, T>, ProgramError> {
+        let mut data: &[u8] = &info.try_borrow_data()?;
+        Ok(ProgramAccount::new(
+            info.clone(),
+            T::try_deserialize(&mut data)?,
+        ))
+    }
+
+    /// 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).
+    pub fn try_from_init(info: &AccountInfo<'a>) -> Result<ProgramAccount<'a, T>, ProgramError> {
+        let mut data: &[u8] = &info.try_borrow_data()?;
+
+        // The discriminator should be zero, since we're initializing.
+        let mut disc_bytes = [0u8; 8];
+        disc_bytes.copy_from_slice(&data[..8]);
+        let discriminator = u64::from_le_bytes(disc_bytes);
+        if discriminator != 0 {
+            return Err(ProgramError::InvalidAccountData);
+        }
+
+        Ok(ProgramAccount::new(
+            info.clone(),
+            T::try_deserialize_unchecked(&mut data)?,
+        ))
+    }
 }
 
-impl<'a, T: AnchorSerialize + AnchorDeserialize> Deref for ProgramAccount<'a, T> {
+impl<'a, T: AccountSerialize + AccountDeserialize> Deref for ProgramAccount<'a, T> {
     type Target = T;
 
     fn deref(&self) -> &Self::Target {
@@ -27,16 +101,15 @@ impl<'a, T: AnchorSerialize + AnchorDeserialize> Deref for ProgramAccount<'a, T>
     }
 }
 
-impl<'a, T: AnchorSerialize + AnchorDeserialize> DerefMut for ProgramAccount<'a, T> {
+impl<'a, T: AccountSerialize + AccountDeserialize> DerefMut for ProgramAccount<'a, T> {
     fn deref_mut(&mut self) -> &mut Self::Target {
         &mut self.account
     }
 }
 
-pub trait Accounts<'info>: Sized {
-    fn try_anchor(program_id: &Pubkey, from: &[AccountInfo<'info>]) -> Result<Self, ProgramError>;
-}
-
+/// A data structure providing non-argument inputs to the Solana program, namely
+/// the currently executing program's ID and the set of validated, deserialized
+/// accounts.
 pub struct Context<'a, 'b, T> {
     pub accounts: &'a mut T,
     pub program_id: &'b Pubkey,
@@ -44,8 +117,8 @@ pub struct Context<'a, 'b, T> {
 
 pub mod prelude {
     pub use super::{
-        access_control, program, Accounts, AnchorDeserialize, AnchorSerialize, Context,
-        ProgramAccount,
+        access_control, account, program, AccountDeserialize, AccountSerialize, Accounts,
+        AnchorDeserialize, AnchorSerialize, Context, ProgramAccount,
     };
 
     pub use solana_program::msg;

+ 14 - 16
syn/src/codegen/accounts.rs

@@ -52,8 +52,7 @@ pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream {
                     let mut data = self.#info.try_borrow_mut_data()?;
                     let dst: &mut [u8] = &mut data;
                     let mut cursor = std::io::Cursor::new(dst);
-                    self.#ident.account.serialize(&mut cursor)
-                        .map_err(|_| ProgramError::InvalidAccountData)?;
+                    self.#ident.account.try_serialize(&mut cursor)?;
                 },
             }
         })
@@ -70,7 +69,7 @@ pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream {
 
     quote! {
         impl#combined_generics Accounts#trait_generics for #name#strct_generics {
-            fn try_anchor(program_id: &Pubkey, accounts: &[AccountInfo<'info>]) -> Result<Self, ProgramError> {
+            fn try_accounts(program_id: &Pubkey, accounts: &[AccountInfo<'info>]) -> Result<Self, ProgramError> {
                 let acc_infos = &mut accounts.iter();
 
                 #(#acc_infos)*
@@ -92,13 +91,7 @@ pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream {
     }
 }
 
-// Unpacks the field, if needed.
 pub fn generate_field(f: &Field) -> proc_macro2::TokenStream {
-    let checks: Vec<proc_macro2::TokenStream> = f
-        .constraints
-        .iter()
-        .map(|c| generate_constraint(&f, c))
-        .collect();
     let ident = &f.ident;
     let assign_ty = match &f.ty {
         Ty::AccountInfo => quote! {
@@ -106,16 +99,21 @@ pub fn generate_field(f: &Field) -> proc_macro2::TokenStream {
         },
         Ty::ProgramAccount(acc) => {
             let account_struct = &acc.account_ident;
-            quote! {
-                let mut data: &[u8] = &#ident.try_borrow_data()?;
-                let #ident = ProgramAccount::new(
-                    #ident.clone(),
-                    #account_struct::deserialize(&mut data)
-                    .map_err(|_| ProgramError::InvalidAccountData)?
-                );
+            match f.is_init {
+                false => quote! {
+                    let #ident: ProgramAccount<#account_struct> = ProgramAccount::try_from(#ident)?;
+                },
+                true => quote! {
+                    let #ident: ProgramAccount<#account_struct> = ProgramAccount::try_from_init(#ident)?;
+                },
             }
         }
     };
+    let checks: Vec<proc_macro2::TokenStream> = f
+        .constraints
+        .iter()
+        .map(|c| generate_constraint(&f, c))
+        .collect();
     quote! {
         #assign_ty
         #(#checks)*

+ 0 - 3
syn/src/codegen/idl.rs

@@ -1,3 +0,0 @@
-pub fn generate() {
-    // todo
-}

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

@@ -10,7 +10,7 @@ pub fn generate(program: Program) -> proc_macro2::TokenStream {
     let instruction = generate_instruction(&program);
 
     quote! {
-        // Import everything in the mod, in case the user wants to put anchors
+        // Import everything in the mod, in case the user wants to put types
         // in there.
         use #mod_name::*;
 
@@ -61,7 +61,7 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
 
             quote! {
                 instruction::#variant_arm => {
-                    let mut accounts = #anchor::try_anchor(program_id, accounts)?;
+                    let mut accounts = #anchor::try_accounts(program_id, accounts)?;
                     #program_name::#rpc_name(
                         Context {
                             accounts: &mut accounts,

+ 1 - 0
syn/src/lib.rs

@@ -64,6 +64,7 @@ pub struct Field {
     pub constraints: Vec<Constraint>,
     pub is_mut: bool,
     pub is_signer: bool,
+    pub is_init: bool,
 }
 
 // A type of an account field.

+ 18 - 6
syn/src/parser/accounts.rs

@@ -27,8 +27,11 @@ pub fn parse(strct: &syn::ItemStruct) -> AccountsStruct {
                         Some(attr)
                     })
                     .collect();
-                assert!(anchor_attrs.len() == 1);
-                anchor_attrs[0]
+                match anchor_attrs.len() {
+                    0 => None,
+                    1 => Some(anchor_attrs[0]),
+                    _ => panic!("invalid syntax: only one account attribute is allowed"),
+                }
             };
             parse_field(f, anchor_attr)
         })
@@ -38,16 +41,20 @@ pub fn parse(strct: &syn::ItemStruct) -> AccountsStruct {
 }
 
 // Parses an inert #[anchor] attribute specifying the DSL.
-fn parse_field(f: &syn::Field, anchor: &syn::Attribute) -> Field {
+fn parse_field(f: &syn::Field, anchor: Option<&syn::Attribute>) -> Field {
     let ident = f.ident.clone().unwrap();
     let ty = parse_ty(f);
-    let (constraints, is_mut, is_signer) = parse_constraints(anchor, &ty);
+    let (constraints, is_mut, is_signer, is_init) = match anchor {
+        None => (vec![], false, false, false),
+        Some(anchor) => parse_constraints(anchor, &ty),
+    };
     Field {
         ident,
         ty,
         constraints,
         is_mut,
         is_signer,
+        is_init,
     }
 }
 
@@ -90,13 +97,14 @@ fn parse_program_account(path: &syn::Path) -> ProgramAccountTy {
     ProgramAccountTy { account_ident }
 }
 
-fn parse_constraints(anchor: &syn::Attribute, ty: &Ty) -> (Vec<Constraint>, bool, bool) {
+fn parse_constraints(anchor: &syn::Attribute, ty: &Ty) -> (Vec<Constraint>, bool, bool, bool) {
     let mut tts = anchor.tokens.clone().into_iter();
     let g_stream = match tts.next().expect("Must have a token group") {
         proc_macro2::TokenTree::Group(g) => g.stream(),
         _ => panic!("Invalid syntax"),
     };
 
+    let mut is_init = false;
     let mut is_mut = false;
     let mut is_signer = false;
     let mut constraints = vec![];
@@ -106,6 +114,10 @@ fn parse_constraints(anchor: &syn::Attribute, ty: &Ty) -> (Vec<Constraint>, bool
     while let Some(token) = inner_tts.next() {
         match token {
             proc_macro2::TokenTree::Ident(ident) => match ident.to_string().as_str() {
+                "init" => {
+                    is_init = true;
+                    is_mut = true;
+                }
                 "mut" => {
                     is_mut = true;
                 }
@@ -175,5 +187,5 @@ fn parse_constraints(anchor: &syn::Attribute, ty: &Ty) -> (Vec<Constraint>, bool
         }
     }
 
-    (constraints, is_mut, is_signer)
+    (constraints, is_mut, is_signer, is_init)
 }

+ 2 - 1
ts/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@project-serum/anchor",
-  "version": "0.0.0-alpha.1",
+  "version": "0.0.0-alpha.2",
   "description": "Anchor client",
   "main": "dist/cjs/index.js",
   "module": "dist/esm/index.js",
@@ -30,6 +30,7 @@
     "bn.js": "^5.1.2",
     "buffer-layout": "^1.2.0",
     "camelcase": "^5.3.1",
+    "crypto-hash": "^1.3.0",
     "find": "^0.3.0"
   },
   "devDependencies": {

+ 24 - 7
ts/src/rpc.ts

@@ -7,6 +7,7 @@ import {
   TransactionSignature,
   TransactionInstruction,
 } from "@solana/web3.js";
+import { sha256 } from "crypto-hash";
 import { Idl, IdlInstruction } from "./idl";
 import { IdlError } from "./error";
 import Coder from "./coder";
@@ -35,24 +36,24 @@ export interface Accounts {
 }
 
 /**
- * RpcFn is a single rpc method.
+ * RpcFn is a single rpc method generated from an IDL.
  */
-export type RpcFn = (...args: any[]) => Promise<any>;
+export type RpcFn = (...args: any[]) => Promise<TransactionSignature>;
 
 /**
- * Ix is a function to create a `TransactionInstruction`.
+ * Ix is a function to create a `TransactionInstruction` generated from an IDL.
  */
 export type IxFn = (...args: any[]) => TransactionInstruction;
 
 /**
  * Account is a function returning a deserialized account, given an address.
  */
-export type AccountFn = (address: PublicKey) => any;
+export type AccountFn<T = any> = (address: PublicKey) => T;
 
 /**
  * Options for an RPC invocation.
  */
-type RpcOptions = ConfirmOptions;
+export type RpcOptions = ConfirmOptions;
 
 /**
  * RpcContext provides all arguments for an RPC/IX invocation that are not
@@ -107,7 +108,6 @@ export class RpcFactory {
 
     if (idl.accounts) {
       idl.accounts.forEach((idlAccount) => {
-        // todo
         const accountFn = async (address: PublicKey): Promise<any> => {
           const provider = getProvider();
           if (provider === null) {
@@ -117,7 +117,24 @@ export class RpcFactory {
           if (accountInfo === null) {
             throw new Error(`Entity does not exist ${address}`);
           }
-          return coder.accounts.decode(idlAccount.name, accountInfo.data);
+
+          // Assert the account discriminator is correct.
+          const expectedDiscriminator = Buffer.from(
+            (
+              await sha256(`account:${idlAccount.name}`, {
+                outputFormat: "buffer",
+              })
+            ).slice(0, 8)
+          );
+          const discriminator = accountInfo.data.slice(0, 8);
+
+          if (expectedDiscriminator.compare(discriminator)) {
+            throw new Error("Invalid account discriminator");
+          }
+
+          // Chop off the discriminator before decoding.
+          const data = accountInfo.data.slice(8);
+          return coder.accounts.decode(idlAccount.name, data);
         };
         const name = camelCase(idlAccount.name);
         accountFns[name] = accountFn;

+ 1 - 1
ts/yarn.lock

@@ -1800,7 +1800,7 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2:
     shebang-command "^2.0.0"
     which "^2.0.1"
 
-crypto-hash@^1.2.2:
+crypto-hash@^1.2.2, crypto-hash@^1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/crypto-hash/-/crypto-hash-1.3.0.tgz#b402cb08f4529e9f4f09346c3e275942f845e247"
   integrity sha512-lyAZ0EMyjDkVvz8WOeVnuCPvKVBXcMv1l5SVqO1yC7PzTwrD/pPje/BIRbWhMoPe436U+Y2nD7f5bFx0kt+Sbg==