diff --git a/.travis.yml b/.travis.yml index f5b65931..194b593c 100644 --- a/.travis.yml +++ b/.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: diff --git a/Cargo.lock b/Cargo.lock index 587322ba..7e83d8f2 100644 --- a/Cargo.lock +++ b/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", diff --git a/Cargo.toml b/Cargo.toml index 727f3902..709c5271 100644 --- a/Cargo.toml +++ b/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/*", ] diff --git a/README.md b/README.md index ad0b756e..16e0fd01 100644 --- a/README.md +++ b/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 = )]` | 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("")]` | On `ProgramAccount` structs | Executes the given code literal as a constraint. The literal should evaluate to a boolean. | diff --git a/attributes/access-control/Cargo.toml b/attribute/access-control/Cargo.toml similarity index 76% rename from attributes/access-control/Cargo.toml rename to attribute/access-control/Cargo.toml index 429049f2..57b46fc9 100644 --- a/attributes/access-control/Cargo.toml +++ b/attribute/access-control/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "anchor-attributes-access-control" +name = "anchor-attribute-access-control" version = "0.1.0" authors = ["armaniferrante "] edition = "2018" diff --git a/attributes/access-control/src/lib.rs b/attribute/access-control/src/lib.rs similarity index 75% rename from attributes/access-control/src/lib.rs rename to attribute/access-control/src/lib.rs index b3707b0e..075d5551 100644 --- a/attributes/access-control/src/lib.rs +++ b/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, diff --git a/attributes/program/Cargo.toml b/attribute/account/Cargo.toml similarity index 77% rename from attributes/program/Cargo.toml rename to attribute/account/Cargo.toml index 4005a927..925a7777 100644 --- a/attributes/program/Cargo.toml +++ b/attribute/account/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "anchor-attributes-program" +name = "anchor-attribute-account" version = "0.1.0" authors = ["armaniferrante "] 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" } \ No newline at end of file +anchor-syn = { path = "../../syn" } diff --git a/attribute/account/src/lib.rs b/attribute/account/src/lib.rs new file mode 100644 index 00000000..81aaf539 --- /dev/null +++ b/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(&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 { + 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 { + 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 + }) +} diff --git a/attribute/program/Cargo.toml b/attribute/program/Cargo.toml new file mode 100644 index 00000000..8f6da5cc --- /dev/null +++ b/attribute/program/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "anchor-attribute-program" +version = "0.1.0" +authors = ["armaniferrante "] +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" } diff --git a/attributes/program/src/lib.rs b/attribute/program/src/lib.rs similarity index 83% rename from attributes/program/src/lib.rs rename to attribute/program/src/lib.rs index 3424c347..45f62b9b 100644 --- a/attributes/program/src/lib.rs +++ b/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, diff --git a/derive/Cargo.toml b/derive/accounts/Cargo.toml similarity index 78% rename from derive/Cargo.toml rename to derive/accounts/Cargo.toml index 4c937d4b..82896800 100644 --- a/derive/Cargo.toml +++ b/derive/accounts/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "anchor-derive" +name = "anchor-derive-accounts" version = "0.1.0" authors = ["armaniferrante "] 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" } diff --git a/derive/src/lib.rs b/derive/accounts/src/lib.rs similarity index 100% rename from derive/src/lib.rs rename to derive/accounts/src/lib.rs diff --git a/docs/src/getting-started/installation.md b/docs/src/getting-started/installation.md index 6175dcda..360a9b78 100644 --- a/docs/src/getting-started/installation.md +++ b/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, diff --git a/docs/src/tutorials/tutorial-0.md b/docs/src/tutorials/tutorial-0.md index b59b1c4b..ff31aa84 100644 --- a/docs/src/tutorials/tutorial-0.md +++ b/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 diff --git a/docs/src/tutorials/tutorial-1.md b/docs/src/tutorials/tutorial-1.md index fd5803a9..cec77861 100644 --- a/docs/src/tutorials/tutorial-1.md +++ b/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`. -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`. +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. -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. ;) +::: 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 + +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. + +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. diff --git a/docs/src/tutorials/tutorial-2.md b/docs/src/tutorials/tutorial-2.md index 2d19fe80..125fd993 100644 --- a/docs/src/tutorials/tutorial-2.md +++ b/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 diff --git a/examples/tutorial/basic-1/programs/basic-1/src/lib.rs b/examples/tutorial/basic-1/programs/basic-1/src/lib.rs index a11549ee..0839dcb6 100644 --- a/examples/tutorial/basic-1/programs/basic-1/src/lib.rs +++ b/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, 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, } diff --git a/examples/tutorial/basic-1/tests/basic-1.js b/examples/tutorial/basic-1/tests/basic-1.js index 3e692a8d..299a4f9e 100644 --- a/examples/tutorial/basic-1/tests/basic-1.js +++ b/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 }); }); diff --git a/examples/tutorial/basic-2/programs/basic-2/src/lib.rs b/examples/tutorial/basic-2/programs/basic-2/src/lib.rs index 8b5b0fce..a8aa757d 100644 --- a/examples/tutorial/basic-2/programs/basic-2/src/lib.rs +++ b/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, diff --git a/src/lib.rs b/src/lib.rs index 2374e396..122cb20b 100644 --- a/src/lib.rs +++ b/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; +} + +/// 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(&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; + + /// 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; +} + +/// 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, 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, 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; -} - +/// 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; diff --git a/syn/src/codegen/accounts.rs b/syn/src/codegen/accounts.rs index 832d98e6..d348de8e 100644 --- a/syn/src/codegen/accounts.rs +++ b/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 { + fn try_accounts(program_id: &Pubkey, accounts: &[AccountInfo<'info>]) -> Result { 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 = 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 = f + .constraints + .iter() + .map(|c| generate_constraint(&f, c)) + .collect(); quote! { #assign_ty #(#checks)* diff --git a/syn/src/codegen/idl.rs b/syn/src/codegen/idl.rs deleted file mode 100644 index 85a33c36..00000000 --- a/syn/src/codegen/idl.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub fn generate() { - // todo -} diff --git a/syn/src/codegen/program.rs b/syn/src/codegen/program.rs index a2c04b24..82d4b9ce 100644 --- a/syn/src/codegen/program.rs +++ b/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, diff --git a/syn/src/lib.rs b/syn/src/lib.rs index f904a1a6..e189f716 100644 --- a/syn/src/lib.rs +++ b/syn/src/lib.rs @@ -64,6 +64,7 @@ pub struct Field { pub constraints: Vec, pub is_mut: bool, pub is_signer: bool, + pub is_init: bool, } // A type of an account field. diff --git a/syn/src/parser/accounts.rs b/syn/src/parser/accounts.rs index 6f68f38e..a574ece6 100644 --- a/syn/src/parser/accounts.rs +++ b/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, bool, bool) { +fn parse_constraints(anchor: &syn::Attribute, ty: &Ty) -> (Vec, 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, 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, bool } } - (constraints, is_mut, is_signer) + (constraints, is_mut, is_signer, is_init) } diff --git a/ts/package.json b/ts/package.json index 45f4d20e..5f49ed9c 100644 --- a/ts/package.json +++ b/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": { diff --git a/ts/src/rpc.ts b/ts/src/rpc.ts index 1d081e06..86cead99 100644 --- a/ts/src/rpc.ts +++ b/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; +export type RpcFn = (...args: any[]) => Promise; /** - * 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 = (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 => { 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; diff --git a/ts/yarn.lock b/ts/yarn.lock index c343319b..43450554 100644 --- a/ts/yarn.lock +++ b/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==