Account discriminators (#14)

This commit is contained in:
Armani Ferrante 2021-01-09 22:03:14 -08:00 committed by GitHub
parent 4792d1ce49
commit de353cb4e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 388 additions and 101 deletions

View File

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

24
Cargo.lock generated
View File

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

View File

@ -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/*",
]

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
[package]
name = "anchor-attributes-program"
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" }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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": {

View File

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

View File

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