Account discriminators (#14)
This commit is contained in:
parent
4792d1ce49
commit
de353cb4e4
|
@ -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:
|
||||
|
|
|
@ -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",
|
||||
|
|
11
Cargo.toml
11
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/*",
|
||||
]
|
||||
|
|
|
@ -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,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"
|
|
@ -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,
|
|
@ -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" }
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -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" }
|
|
@ -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,
|
|
@ -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" }
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
99
src/lib.rs
99
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;
|
||||
|
|
|
@ -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)*
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
pub fn generate() {
|
||||
// todo
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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==
|
||||
|
|
Loading…
Reference in New Issue