Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lang: Zero copy state structs #206

Merged
merged 3 commits into from
Apr 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions examples/zero-copy/programs/zero-copy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,41 @@ use anchor_lang::prelude::*;
pub mod zero_copy {
use super::*;

#[state(zero_copy)]
pub struct Globals {
pub authority: Pubkey,
// The solana runtime currently restricts how much one can resize an
// account on CPI to ~10240 bytes. State accounts are program derived
// addresses, which means its max size is limited by this restriction
// (i.e., this is not an Anchor specific issue).
//
// As a result, we only use 250 events here.
//
// For larger zero-copy data structures, one must use non-state anchor
// accounts, as is demonstrated below.
pub events: [Event; 250],

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems possible to get 2 contacts out of solana sdk and calculated that number

}

impl Globals {
// Note that the `new` constructor is different from non-zero-copy
// state accounts. Namely, it takes in a `&mut self` parameter.
pub fn new(&mut self, ctx: Context<New>) -> ProgramResult {
self.authority = *ctx.accounts.authority.key;
Ok(())
}

#[access_control(auth(&self, &ctx))]
pub fn set_event(
&mut self,
ctx: Context<SetEvent>,
idx: u32,
event: RpcEvent,
) -> ProgramResult {
self.events[idx as usize] = event.into();
Ok(())
}
}

pub fn create_foo(ctx: Context<CreateFoo>) -> ProgramResult {
let foo = &mut ctx.accounts.foo.load_init()?;
foo.authority = *ctx.accounts.authority.key;
Expand Down Expand Up @@ -58,6 +93,18 @@ pub mod zero_copy {
}
}

#[derive(Accounts)]
pub struct New<'info> {
#[account(signer)]
authority: AccountInfo<'info>,
}

#[derive(Accounts)]
pub struct SetEvent<'info> {
#[account(signer)]
authority: AccountInfo<'info>,
}

#[derive(Accounts)]
pub struct CreateFoo<'info> {
#[account(init)]
Expand Down Expand Up @@ -142,3 +189,36 @@ pub struct Event {
pub from: Pubkey,
pub data: u64,
}

// A separate type is used for the RPC interface for two main reasons.
//
// 1. AnchorSerialize and AnchorDeserialize must be derived. Anchor requires
// *all* instructions to implement the AnchorSerialize and AnchorDeserialize
// traits, so any types in method signatures must as well.
// 2. All types for zero copy deserialization are `#[repr(packed)]`. However,
// the implementation of AnchorSerialize (i.e. borsh), uses references
// to the fields it serializes. So if we were to just throw tehse derives
// onto the other `Event` struct, we would have references to
// `#[repr(packed)]` fields, which is unsafe. To avoid the unsafeness, we
// just use a separate type.
#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct RpcEvent {
pub from: Pubkey,
pub data: u64,
}

impl From<RpcEvent> for Event {
fn from(e: RpcEvent) -> Event {
Event {
from: e.from,
data: e.data,
}
}
}

fn auth(globals: &Globals, ctx: &Context<SetEvent>) -> ProgramResult {
if &globals.authority != ctx.accounts.authority.key {
return Err(ProgramError::Custom(1)); // Arbitrary error.
}
Ok(())
}
39 changes: 39 additions & 0 deletions examples/zero-copy/tests/zero-copy.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,45 @@ describe("zero-copy", () => {

const foo = new anchor.web3.Account();

it("Creates zero copy state", async () => {
await program.state.rpc.new({
accounts: {
authority: program.provider.wallet.publicKey,
},
});
const state = await program.state();
assert.ok(state.authority.equals(program.provider.wallet.publicKey));
assert.ok(state.events.length === 250);
state.events.forEach((event, idx) => {
assert.ok(event.from.equals(new anchor.web3.PublicKey()));
assert.ok(event.data.toNumber() === 0);
});
});

it("Updates zero copy state", async () => {
let event = {
from: new anchor.web3.PublicKey(),
data: new anchor.BN(1234),
};
await program.state.rpc.setEvent(5, event, {
accounts: {
authority: program.provider.wallet.publicKey,
},
});
const state = await program.state();
assert.ok(state.authority.equals(program.provider.wallet.publicKey));
assert.ok(state.events.length === 250);
state.events.forEach((event, idx) => {
if (idx === 5) {
assert.ok(event.from.equals(event.from));
assert.ok(event.data.eq(event.data));
} else {
assert.ok(event.from.equals(new anchor.web3.PublicKey()));
assert.ok(event.data.toNumber() === 0);
}
});
});

it("Is creates a zero copy account", async () => {
await program.rpc.createFoo({
accounts: {
Expand Down
44 changes: 36 additions & 8 deletions lang/attribute/state/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,32 @@ use syn::parse_macro_input;
/// account size. When determining a size, make sure to reserve enough space
/// for the 8 byte account discriminator prepended to the account. That is,
/// always use 8 extra bytes.
///
/// # Zero Copy Deserialization
///
/// Similar to the `#[account]` attribute one can enable zero copy
/// deserialization by using the `zero_copy` argument:
///
/// ```ignore
/// #[state(zero_copy)]
/// ```
///
/// For more, see the [`account`](./attr.account.html) attribute.
#[proc_macro_attribute]
pub fn state(
args: proc_macro::TokenStream,
input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
let item_struct = parse_macro_input!(input as syn::ItemStruct);
let struct_ident = &item_struct.ident;
let is_zero_copy = args.to_string() == "zero_copy";

let size_override = {
if args.is_empty() {
// No size override given. The account size is whatever is given
// as the initialized value. Use the default implementation.
quote! {
impl anchor_lang::AccountSize for #struct_ident {
impl anchor_lang::__private::AccountSize for #struct_ident {
fn size(&self) -> std::result::Result<u64, anchor_lang::solana_program::program_error::ProgramError> {
Ok(8 + self
.try_to_vec()
Expand All @@ -35,20 +47,36 @@ pub fn state(
}
}
} else {
let size = proc_macro2::TokenStream::from(args);
// Size override given to the macro. Use it.
quote! {
impl anchor_lang::AccountSize for #struct_ident {
fn size(&self) -> std::result::Result<u64, anchor_lang::solana_program::program_error::ProgramError> {
Ok(#size)
if is_zero_copy {
quote! {
impl anchor_lang::__private::AccountSize for #struct_ident {
fn size(&self) -> std::result::Result<u64, anchor_lang::solana_program::program_error::ProgramError> {
let len = anchor_lang::__private::bytemuck::bytes_of(self).len() as u64;
Ok(8 + len)
}
}
}
} else {
let size = proc_macro2::TokenStream::from(args.clone());
// Size override given to the macro. Use it.
quote! {
impl anchor_lang::__private::AccountSize for #struct_ident {
fn size(&self) -> std::result::Result<u64, anchor_lang::solana_program::program_error::ProgramError> {
Ok(#size)
}
}
}
}
}
};

let attribute = match is_zero_copy {
false => quote! {#[account]},
true => quote! {#[account(zero_copy)]},
};

proc_macro::TokenStream::from(quote! {
#[account]
#attribute
#item_struct

#size_override
Expand Down
17 changes: 10 additions & 7 deletions lang/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,13 +177,6 @@ pub trait InstructionData: AnchorSerialize {
fn data(&self) -> Vec<u8>;
}

// Calculates the size of an account, which may be larger than the deserialized
// data in it. This trait is currently only used for `#[state]` accounts.
#[doc(hidden)]
pub trait AccountSize: AnchorSerialize {
fn size(&self) -> Result<u64, ProgramError>;
}

/// An event that can be emitted via a Solana log.
pub trait Event: AnchorSerialize + AnchorDeserialize + Discriminator {
fn data(&self) -> Vec<u8>;
Expand Down Expand Up @@ -242,6 +235,7 @@ pub mod prelude {
// Internal module used by macros.
#[doc(hidden)]
pub mod __private {
use solana_program::program_error::ProgramError;
use solana_program::pubkey::Pubkey;

pub use crate::ctor::Ctor;
Expand All @@ -251,6 +245,13 @@ pub mod __private {
pub use base64;
pub use bytemuck;

// Calculates the size of an account, which may be larger than the deserialized
// data in it. This trait is currently only used for `#[state]` accounts.
#[doc(hidden)]
pub trait AccountSize {
fn size(&self) -> Result<u64, ProgramError>;
}

// Very experimental trait.
pub trait ZeroCopyAccessor<Ty> {
fn get(&self) -> Ty;
Expand All @@ -265,4 +266,6 @@ pub mod __private {
input.to_bytes()
}
}

pub use crate::state::PROGRAM_STATE_SEED;
}
4 changes: 3 additions & 1 deletion lang/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ use solana_program::program_error::ProgramError;
use solana_program::pubkey::Pubkey;
use std::ops::{Deref, DerefMut};

pub const PROGRAM_STATE_SEED: &'static str = "unversioned";

/// Boxed container for the program state singleton.
#[derive(Clone)]
pub struct ProgramState<'info, T: AccountSerialize + AccountDeserialize + Clone> {
Expand Down Expand Up @@ -39,7 +41,7 @@ impl<'a, T: AccountSerialize + AccountDeserialize + Clone> ProgramState<'a, T> {
}

pub fn seed() -> &'static str {
"unversioned"
PROGRAM_STATE_SEED
}

pub fn address(program_id: &Pubkey) -> Pubkey {
Expand Down
Loading