lang: Add `LazyAccount` (#3194)
This commit is contained in:
parent
dd14b2cce2
commit
879601e632
|
@ -463,6 +463,8 @@ jobs:
|
|||
path: tests/bench
|
||||
- cmd: cd tests/idl && ./test.sh
|
||||
path: tests/idl
|
||||
- cmd: cd tests/lazy-account && anchor test
|
||||
path: tests/lazy-account
|
||||
# TODO: Enable when `solang` becomes compatible with the new IDL spec
|
||||
# - cmd: cd tests/solang && anchor test
|
||||
# path: tests/solang
|
||||
|
|
|
@ -45,6 +45,7 @@ The minor version will be incremented upon a breaking change and the patch versi
|
|||
- cli: Make `clean` command also remove the `.anchor` directory ([#3192](https://github.com/coral-xyz/anchor/pull/3192)).
|
||||
- lang: Deprecate `#[interface]` attribute ([#3195](https://github.com/coral-xyz/anchor/pull/3195)).
|
||||
- ts: Include unresolved accounts in the resolution error message ([#3207](https://github.com/coral-xyz/anchor/pull/3207)).
|
||||
- lang: Add `LazyAccount` ([#3194](https://github.com/coral-xyz/anchor/pull/3194)).
|
||||
|
||||
### Fixes
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ idl-build = [
|
|||
]
|
||||
init-if-needed = ["anchor-derive-accounts/init-if-needed"]
|
||||
interface-instructions = ["anchor-attribute-program/interface-instructions"]
|
||||
lazy-account = ["anchor-attribute-account/lazy-account", "anchor-derive-serde/lazy-account"]
|
||||
|
||||
[dependencies]
|
||||
anchor-attribute-access-control = { path = "./attribute/access-control", version = "0.30.1" }
|
||||
|
|
|
@ -13,6 +13,7 @@ proc-macro = true
|
|||
[features]
|
||||
anchor-debug = ["anchor-syn/anchor-debug"]
|
||||
idl-build = ["anchor-syn/idl-build"]
|
||||
lazy-account = []
|
||||
|
||||
[dependencies]
|
||||
anchor-syn = { path = "../../syn", version = "0.30.1", features = ["hash"] }
|
||||
|
|
|
@ -0,0 +1,302 @@
|
|||
use proc_macro2::{Literal, TokenStream};
|
||||
use quote::{format_ident, quote, ToTokens};
|
||||
|
||||
pub fn gen_lazy(strct: &syn::ItemStruct) -> syn::Result<TokenStream> {
|
||||
let ident = &strct.ident;
|
||||
let lazy_ident = format_ident!("Lazy{}", ident);
|
||||
let load_common_ident = to_private_ident("load_common");
|
||||
let initialize_fields = to_private_ident("initialize_fields");
|
||||
let lazy_acc_ty = quote! { anchor_lang::accounts::lazy_account::LazyAccount };
|
||||
let disc_len = quote! { <#ident as anchor_lang::Discriminator>::DISCRIMINATOR.len() };
|
||||
|
||||
let load_common_docs = quote! {
|
||||
/// The deserialized value is cached for future uses i.e. all subsequent calls to this
|
||||
/// method do not deserialize the data again, instead, they return the cached value.
|
||||
///
|
||||
/// To reload the data from the underlying account info (e.g. after a CPI call), run
|
||||
/// [`LazyAccount::unload`] before running this method.
|
||||
///
|
||||
/// See [`LazyAccount`]'s documentation for more information.
|
||||
};
|
||||
let load_panic_docs = quote! {
|
||||
/// # Panics
|
||||
///
|
||||
/// If there is an existing mutable reference crated by any of the `load_mut` methods.
|
||||
};
|
||||
let load_mut_panic_docs = quote! {
|
||||
/// # Panics
|
||||
///
|
||||
/// If there is an existing reference (mutable or not) created by any of the `load` methods.
|
||||
};
|
||||
|
||||
let (loader_signatures, loader_impls) = strct
|
||||
.fields
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, field)| {
|
||||
let field_ident = to_field_ident(field, i);
|
||||
let load_ident = format_ident!("load_{field_ident}");
|
||||
let load_mut_ident = format_ident!("load_mut_{field_ident}");
|
||||
let load_common_ident = to_private_ident(format!("load_common_{field_ident}"));
|
||||
let offset_of_ident = to_private_ident(format!("offset_of_{field_ident}"));
|
||||
let size_of_ident = to_private_ident(format!("size_of_{field_ident}"));
|
||||
|
||||
let offset = i.eq(&0).then(|| quote!(#disc_len)).unwrap_or_else(|| {
|
||||
// Current offset is the previous field's offset + size
|
||||
strct
|
||||
.fields
|
||||
.iter()
|
||||
.nth(i - 1)
|
||||
.map(|field| {
|
||||
let field_ident = to_field_ident(field, i - 1);
|
||||
let offset_of_ident = to_private_ident(format!("offset_of_{field_ident}"));
|
||||
let size_of_ident = to_private_ident(format!("size_of_{field_ident}"));
|
||||
quote! { self.#offset_of_ident() + self.#size_of_ident() }
|
||||
})
|
||||
.expect("Previous field should always exist when i > 0")
|
||||
});
|
||||
|
||||
let ty = &field.ty;
|
||||
let size = quote! {
|
||||
<#ty as anchor_lang::__private::Lazy>::size_of(
|
||||
&self.__info.data.borrow()[self.#offset_of_ident()..]
|
||||
)
|
||||
};
|
||||
|
||||
let signatures = quote! {
|
||||
/// Load a reference to the field.
|
||||
///
|
||||
#load_common_docs
|
||||
///
|
||||
#load_panic_docs
|
||||
fn #load_ident(&self) -> anchor_lang::Result<::core::cell::Ref<'_, #ty>>;
|
||||
|
||||
/// Load a mutable reference to the field.
|
||||
///
|
||||
#load_common_docs
|
||||
///
|
||||
#load_mut_panic_docs
|
||||
fn #load_mut_ident(&self) -> anchor_lang::Result<::core::cell::RefMut<'_, #ty>>;
|
||||
|
||||
#[doc(hidden)]
|
||||
fn #load_common_ident<R>(&self, f: impl FnOnce() -> R) -> anchor_lang::Result<R>;
|
||||
|
||||
#[doc(hidden)]
|
||||
fn #offset_of_ident(&self) -> usize;
|
||||
|
||||
#[doc(hidden)]
|
||||
fn #size_of_ident(&self) -> usize;
|
||||
};
|
||||
|
||||
let impls = quote! {
|
||||
fn #load_ident(&self) -> anchor_lang::Result<::core::cell::Ref<'_, #ty>> {
|
||||
self.#load_common_ident(|| {
|
||||
// SAFETY: The common load method makes sure the field is initialized.
|
||||
::core::cell::Ref::map(self.__account.borrow(), |acc| unsafe {
|
||||
&*::core::ptr::addr_of!((*acc.as_ptr()).#field_ident)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn #load_mut_ident(&self) -> anchor_lang::Result<::core::cell::RefMut<'_, #ty>> {
|
||||
self.#load_common_ident(|| {
|
||||
// SAFETY: The common load method makes sure the field is initialized.
|
||||
::core::cell::RefMut::map(self.__account.borrow_mut(), |acc| unsafe {
|
||||
&mut *::core::ptr::addr_of_mut!((*acc.as_mut_ptr()).#field_ident)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
fn #load_common_ident<R>(&self, f: impl FnOnce() -> R) -> anchor_lang::Result<R> {
|
||||
self.#initialize_fields();
|
||||
|
||||
// Return early if initialized
|
||||
if self.__fields.borrow().as_ref().unwrap()[#i] {
|
||||
return Ok(f());
|
||||
}
|
||||
|
||||
// Deserialize and write
|
||||
let offset = self.#offset_of_ident();
|
||||
let size = self.#size_of_ident();
|
||||
let data = self.__info.data.borrow();
|
||||
let val = anchor_lang::AnchorDeserialize::try_from_slice(
|
||||
&data[offset..offset + size]
|
||||
)?;
|
||||
unsafe {
|
||||
::core::ptr::addr_of_mut!(
|
||||
(*self.__account.borrow_mut().as_mut_ptr()).#field_ident
|
||||
).write(val)
|
||||
};
|
||||
|
||||
// Set initialized
|
||||
self.__fields.borrow_mut().as_mut().unwrap()[#i] = true;
|
||||
|
||||
Ok(f())
|
||||
}
|
||||
|
||||
// If this method gets inlined when there are >= 12 fields, compilation breaks with
|
||||
// `LLVM ERROR: Branch target out of insn range`
|
||||
#[inline(never)]
|
||||
fn #offset_of_ident(&self) -> usize {
|
||||
#offset
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn #size_of_ident(&self) -> usize {
|
||||
#size
|
||||
}
|
||||
};
|
||||
|
||||
Ok((signatures, impls))
|
||||
})
|
||||
.collect::<syn::Result<Vec<_>>>()?
|
||||
.into_iter()
|
||||
.unzip::<_, _, Vec<_>, Vec<_>>();
|
||||
|
||||
let load_idents = strct
|
||||
.fields
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, field)| to_field_ident(field, i))
|
||||
.map(|field| format_ident!("load_{field}"));
|
||||
let total_fields = strct.fields.len();
|
||||
|
||||
Ok(quote! {
|
||||
pub trait #lazy_ident {
|
||||
/// Load a reference to the entire account.
|
||||
///
|
||||
#load_common_docs
|
||||
///
|
||||
#load_panic_docs
|
||||
fn load(&self) -> anchor_lang::Result<::core::cell::Ref<'_, #ident>>;
|
||||
|
||||
/// Load a mutable reference to the entire account.
|
||||
///
|
||||
#load_common_docs
|
||||
///
|
||||
#load_mut_panic_docs
|
||||
fn load_mut(&self) -> anchor_lang::Result<::core::cell::RefMut<'_, #ident>>;
|
||||
|
||||
#[doc(hidden)]
|
||||
fn #load_common_ident<R>(&self, f: impl FnOnce() -> R) -> anchor_lang::Result<R>;
|
||||
|
||||
#(#loader_signatures)*
|
||||
|
||||
#[doc(hidden)]
|
||||
fn #initialize_fields(&self);
|
||||
|
||||
/// Run the exit routine of the account, similar to [`AccountsExit`] but implemented
|
||||
/// as a regular method because we can't implement external traits for external structs.
|
||||
fn exit(&self, program_id: &anchor_lang::prelude::Pubkey) -> anchor_lang::Result<()>;
|
||||
}
|
||||
|
||||
impl<'info> #lazy_ident for #lazy_acc_ty<'info, #ident> {
|
||||
fn load(&self) -> anchor_lang::Result<::core::cell::Ref<'_, #ident>> {
|
||||
self.#load_common_ident(|| {
|
||||
// SAFETY: The common load method makes sure all fields are initialized.
|
||||
::core::cell::Ref::map(self.__account.borrow(), |acc| unsafe {
|
||||
acc.assume_init_ref()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn load_mut(&self) -> anchor_lang::Result<::core::cell::RefMut<'_, #ident>> {
|
||||
self.#load_common_ident(|| {
|
||||
// SAFETY: The common load method makes sure all fields are initialized.
|
||||
::core::cell::RefMut::map(self.__account.borrow_mut(), |acc| unsafe {
|
||||
acc.assume_init_mut()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
fn #load_common_ident<R>(&self, f: impl FnOnce() -> R) -> anchor_lang::Result<R> {
|
||||
self.#initialize_fields();
|
||||
|
||||
// Create a scope to drop the `__fields` borrow
|
||||
let all_uninit = {
|
||||
// Return early if all fields are initialized
|
||||
let fields = self.__fields.borrow();
|
||||
let fields = fields.as_ref().unwrap();
|
||||
if !fields.contains(&false) {
|
||||
return Ok(f());
|
||||
}
|
||||
|
||||
!fields.contains(&true)
|
||||
};
|
||||
|
||||
if all_uninit {
|
||||
// Nothing is initialized, initialize all
|
||||
let offset = #disc_len;
|
||||
let mut data = self.__info.data.borrow();
|
||||
let val = anchor_lang::AnchorDeserialize::deserialize(&mut &data[offset..])?;
|
||||
unsafe { self.__account.borrow_mut().as_mut_ptr().write(val) };
|
||||
|
||||
// Set fields to initialized
|
||||
let mut fields = self.__fields.borrow_mut();
|
||||
let fields = fields.as_mut().unwrap();
|
||||
for field in fields {
|
||||
*field = true;
|
||||
}
|
||||
} else {
|
||||
// Only initialize uninitialized fields (`load` methods already do this).
|
||||
//
|
||||
// This is not exactly efficient because `load` methods have a bit of
|
||||
// runtime ownership overhead. This could be optimized further, but it
|
||||
// requires some refactoring and also makes the code harder to reason about.
|
||||
//
|
||||
// We can return back to this if benchmarks show this is a bottleneck.
|
||||
#(self.#load_idents()?;)*
|
||||
}
|
||||
|
||||
Ok(f())
|
||||
}
|
||||
|
||||
#(#loader_impls)*
|
||||
|
||||
#[inline(always)]
|
||||
fn #initialize_fields(&self) {
|
||||
if self.__fields.borrow().is_none() {
|
||||
*self.__fields.borrow_mut() = Some(vec![false; #total_fields]);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: This method can be optimized to *only* serialize the fields that we have
|
||||
// initialized rather than deserializing the whole account, and then serializing it
|
||||
// back, which consumes a lot more CUs than it should for most accounts.
|
||||
fn exit(&self, program_id: &anchor_lang::prelude::Pubkey) -> anchor_lang::Result<()> {
|
||||
// Only persist if the owner is the current program and the account is not closed
|
||||
if &<#ident as anchor_lang::Owner>::owner() == program_id
|
||||
&& !anchor_lang::__private::is_closed(self.__info)
|
||||
{
|
||||
// Make sure all fields are initialized
|
||||
let acc = self.load()?;
|
||||
let mut data = self.__info.try_borrow_mut_data()?;
|
||||
let dst: &mut [u8] = &mut data;
|
||||
let mut writer = anchor_lang::__private::BpfWriter::new(dst);
|
||||
acc.try_serialize(&mut writer)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the field's ident and if the ident doesn't exist (e.g. for tuple structs), default to the
|
||||
/// given index.
|
||||
fn to_field_ident(field: &syn::Field, i: usize) -> TokenStream {
|
||||
field
|
||||
.ident
|
||||
.as_ref()
|
||||
.map(ToTokens::to_token_stream)
|
||||
.unwrap_or_else(|| Literal::usize_unsuffixed(i).to_token_stream())
|
||||
}
|
||||
|
||||
/// Convert to private ident.
|
||||
///
|
||||
/// This is used to indicate to the users that they shouldn't use this identifier.
|
||||
fn to_private_ident<S: AsRef<str>>(ident: S) -> syn::Ident {
|
||||
format_ident!("__{}", ident.as_ref())
|
||||
}
|
|
@ -12,6 +12,9 @@ use syn::{
|
|||
|
||||
mod id;
|
||||
|
||||
#[cfg(feature = "lazy-account")]
|
||||
mod lazy;
|
||||
|
||||
/// An attribute for a data structure representing a Solana account.
|
||||
///
|
||||
/// `#[account]` generates trait implementations for the following traits:
|
||||
|
@ -207,6 +210,17 @@ pub fn account(
|
|||
#owner_impl
|
||||
}
|
||||
} else {
|
||||
let lazy = {
|
||||
#[cfg(feature = "lazy-account")]
|
||||
match namespace.is_empty().then(|| lazy::gen_lazy(&account_strct)) {
|
||||
Some(Ok(lazy)) => lazy,
|
||||
// If lazy codegen fails for whatever reason, return empty tokenstream which
|
||||
// will make the account unusable with `LazyAccount<T>`
|
||||
_ => Default::default(),
|
||||
}
|
||||
#[cfg(not(feature = "lazy-account"))]
|
||||
proc_macro2::TokenStream::default()
|
||||
};
|
||||
quote! {
|
||||
#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
|
||||
#account_strct
|
||||
|
@ -251,6 +265,8 @@ pub fn account(
|
|||
}
|
||||
|
||||
#owner_impl
|
||||
|
||||
#lazy
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -12,6 +12,7 @@ proc-macro = true
|
|||
|
||||
[features]
|
||||
idl-build = ["anchor-syn/idl-build"]
|
||||
lazy-account = []
|
||||
|
||||
[dependencies]
|
||||
anchor-syn = { path = "../../syn", version = "0.30.1" }
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
use proc_macro2::Literal;
|
||||
use quote::{format_ident, quote};
|
||||
use syn::{spanned::Spanned, Fields, Item};
|
||||
|
||||
pub fn gen_lazy(input: proc_macro::TokenStream) -> syn::Result<proc_macro2::TokenStream> {
|
||||
let item = syn::parse::<Item>(input)?;
|
||||
let (name, generics, size) = match &item {
|
||||
Item::Struct(strct) => (&strct.ident, &strct.generics, sum_fields(&strct.fields)),
|
||||
Item::Enum(enm) => {
|
||||
let arms = enm
|
||||
.variants
|
||||
.iter()
|
||||
.map(|variant| sum_fields(&variant.fields))
|
||||
.enumerate()
|
||||
.map(|(i, size)| (Literal::usize_unsuffixed(i), size))
|
||||
.map(|(i, size)| quote! { Some(#i) => { #size } });
|
||||
|
||||
(
|
||||
&enm.ident,
|
||||
&enm.generics,
|
||||
quote! {
|
||||
1 + match buf.first() {
|
||||
#(#arms,)*
|
||||
_ => unreachable!(),
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
Item::Union(_) => return Err(syn::Error::new(item.span(), "Unions are not supported")),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
|
||||
|
||||
Ok(quote! {
|
||||
impl #impl_generics anchor_lang::__private::Lazy for #name #ty_generics #where_clause {
|
||||
#[inline(always)]
|
||||
fn size_of(buf: &[u8]) -> usize {
|
||||
#size
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn sum_fields(fields: &Fields) -> proc_macro2::TokenStream {
|
||||
let names = fields
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, _)| format_ident!("s{i}"))
|
||||
.collect::<Vec<_>>();
|
||||
let declarations = fields.iter().enumerate().map(|(i, field)| {
|
||||
let ty = &field.ty;
|
||||
let name = &names[i];
|
||||
let sum = &names[..i];
|
||||
let buf = quote! { &buf[0 #(+ #sum)*..] };
|
||||
quote! { let #name = <#ty as anchor_lang::__private::Lazy>::size_of(#buf) }
|
||||
});
|
||||
|
||||
quote! {
|
||||
#(#declarations;)*
|
||||
0 #(+ #names)*
|
||||
}
|
||||
}
|
|
@ -1,5 +1,8 @@
|
|||
extern crate proc_macro;
|
||||
|
||||
#[cfg(feature = "lazy-account")]
|
||||
mod lazy;
|
||||
|
||||
use borsh_derive_internal::*;
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::{Span, TokenStream as TokenStream2};
|
||||
|
@ -73,5 +76,24 @@ fn gen_borsh_deserialize(input: TokenStream) -> TokenStream2 {
|
|||
|
||||
#[proc_macro_derive(AnchorDeserialize, attributes(borsh_skip, borsh_init))]
|
||||
pub fn borsh_deserialize(input: TokenStream) -> TokenStream {
|
||||
TokenStream::from(gen_borsh_deserialize(input))
|
||||
#[cfg(feature = "lazy-account")]
|
||||
{
|
||||
let deser = gen_borsh_deserialize(input.clone());
|
||||
let lazy = lazy::gen_lazy(input).unwrap_or_else(|e| e.to_compile_error());
|
||||
quote::quote! {
|
||||
#deser
|
||||
#lazy
|
||||
}
|
||||
.into()
|
||||
}
|
||||
#[cfg(not(feature = "lazy-account"))]
|
||||
gen_borsh_deserialize(input).into()
|
||||
}
|
||||
|
||||
#[cfg(feature = "lazy-account")]
|
||||
#[proc_macro_derive(Lazy)]
|
||||
pub fn lazy(input: TokenStream) -> TokenStream {
|
||||
lazy::gen_lazy(input)
|
||||
.unwrap_or_else(|e| e.to_compile_error())
|
||||
.into()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,338 @@
|
|||
//! Like [`Account`](crate::Account), but deserializes on-demand.
|
||||
|
||||
use std::{cell::RefCell, collections::BTreeSet, fmt, mem::MaybeUninit, rc::Rc};
|
||||
|
||||
use crate::{
|
||||
error::{Error, ErrorCode},
|
||||
AccountInfo, AccountMeta, AccountSerialize, Accounts, AccountsClose, Discriminator, Key, Owner,
|
||||
Pubkey, Result, ToAccountInfo, ToAccountInfos, ToAccountMetas,
|
||||
};
|
||||
|
||||
/// Deserialize account data lazily (on-demand).
|
||||
///
|
||||
/// Anchor uses [`borsh`] deserialization by default, which can be expensive for both memory and
|
||||
/// compute units usage.
|
||||
///
|
||||
/// With the regular [`Account`] type, all account data gets deserialized, even the fields not used
|
||||
/// by your instruction. On contrast, [`LazyAccount`] allows you to deserialize individual fields,
|
||||
/// saving both memory and compute units.
|
||||
///
|
||||
/// # Table of contents
|
||||
///
|
||||
/// - [When to use](#when-to-use)
|
||||
/// - [Features](#features)
|
||||
/// - [Example](#example)
|
||||
/// - [Safety](#safety)
|
||||
/// - [Performance](#performance)
|
||||
/// - [Memory](#memory)
|
||||
/// - [Compute units](#compute-units)
|
||||
///
|
||||
/// # When to use
|
||||
///
|
||||
/// This is currently an experimental account type, and therefore should only be used when you're
|
||||
/// running into performance issues.
|
||||
///
|
||||
/// It's best to use [`LazyAccount`] when you only need to deserialize some of the fields,
|
||||
/// especially if the account is read-only.
|
||||
///
|
||||
/// Replacing [`Account`] (including `Box`ed) with [`LazyAccount`] *can* improve both stack memory
|
||||
/// and compute unit usage. However, this is not guaranteed. For example, if you need to
|
||||
/// deserialize the account fully, using [`LazyAccount`] will have additional overhead and
|
||||
/// therefore use slightly more compute units.
|
||||
///
|
||||
/// Currently, using the `mut` constraint eventually results in the whole account getting
|
||||
/// deserialized, meaning it won't use fewer compute units compared to [`Account`]. This might get
|
||||
/// optimized in the future.
|
||||
///
|
||||
/// # Features
|
||||
///
|
||||
/// - Can be used as a replacement for [`Account`].
|
||||
/// - Checks the account owner and its discriminator.
|
||||
/// - Does **not** check the type layout matches the defined layout.
|
||||
/// - All account data can be deserialized with `load` and `load_mut` methods. These methods are
|
||||
/// non-inlined, meaning that they're less likely to cause stack violation errors.
|
||||
/// - Each individual field can be deserialized with the generated `load_<field>` and
|
||||
/// `load_mut_<field>` methods.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use anchor_lang::prelude::*;
|
||||
///
|
||||
/// declare_id!("LazyAccount11111111111111111111111111111111");
|
||||
///
|
||||
/// #[program]
|
||||
/// pub mod lazy_account {
|
||||
/// use super::*;
|
||||
///
|
||||
/// pub fn init(ctx: Context<Init>) -> Result<()> {
|
||||
/// let mut my_account = ctx.accounts.my_account.load_mut()?;
|
||||
/// my_account.authority = ctx.accounts.authority.key();
|
||||
///
|
||||
/// // Fill the dynamic data
|
||||
/// for _ in 0..MAX_DATA_LEN {
|
||||
/// my_account.dynamic.push(ctx.accounts.authority.key());
|
||||
/// }
|
||||
///
|
||||
/// Ok(())
|
||||
/// }
|
||||
///
|
||||
/// pub fn read(ctx: Context<Read>) -> Result<()> {
|
||||
/// // Cached load due to the `has_one` constraint
|
||||
/// let authority = ctx.accounts.my_account.load_authority()?;
|
||||
/// msg!("Authority: {}", authority);
|
||||
/// Ok(())
|
||||
/// }
|
||||
///
|
||||
/// pub fn write(ctx: Context<Write>, new_authority: Pubkey) -> Result<()> {
|
||||
/// // Cached load due to the `has_one` constraint
|
||||
/// *ctx.accounts.my_account.load_mut_authority()? = new_authority;
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Accounts)]
|
||||
/// pub struct Init<'info> {
|
||||
/// #[account(mut)]
|
||||
/// pub authority: Signer<'info>,
|
||||
/// #[account(
|
||||
/// init,
|
||||
/// payer = authority,
|
||||
/// space = MyAccount::DISCRIMINATOR.len() + MyAccount::INIT_SPACE
|
||||
/// )]
|
||||
/// pub my_account: LazyAccount<'info, MyAccount>,
|
||||
/// pub system_program: Program<'info, System>,
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Accounts)]
|
||||
/// pub struct Read<'info> {
|
||||
/// pub authority: Signer<'info>,
|
||||
/// #[account(has_one = authority)]
|
||||
/// pub my_account: LazyAccount<'info, MyAccount>,
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Accounts)]
|
||||
/// pub struct Write<'info> {
|
||||
/// pub authority: Signer<'info>,
|
||||
/// #[account(mut, has_one = authority)]
|
||||
/// pub my_account: LazyAccount<'info, MyAccount>,
|
||||
/// }
|
||||
///
|
||||
/// const MAX_DATA_LEN: usize = 256;
|
||||
///
|
||||
/// #[account]
|
||||
/// #[derive(InitSpace)]
|
||||
/// pub struct MyAccount {
|
||||
/// pub authority: Pubkey,
|
||||
/// pub fixed: [Pubkey; 8],
|
||||
/// // Dynamic sized data also works, unlike `AccountLoader`
|
||||
/// #[max_len(MAX_DATA_LEN)]
|
||||
/// pub dynamic: Vec<Pubkey>,
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The safety checks are done using the account's discriminator and the account's owner (similar
|
||||
/// to [`Account`]). However, you should be extra careful when deserializing individual fields if,
|
||||
/// for example, the account needs to be migrated. Make sure the previously serialized data always
|
||||
/// matches the account's type identically.
|
||||
///
|
||||
/// # Performance
|
||||
///
|
||||
/// ## Memory
|
||||
///
|
||||
/// All fields (including the inner account type) are heap allocated. It only uses 24 bytes (3x
|
||||
/// pointer size) of stack memory in total.
|
||||
///
|
||||
/// It's worth noting that where the account is being deserialized matters. For example, the main
|
||||
/// place where Anchor programs are likely to hit stack violation errors is a generated function
|
||||
/// called `try_accounts` (you might be familiar with it from the mangled build logs). This is
|
||||
/// where the instruction is deserialized and constraints are run. Although having everything at the
|
||||
/// same place is convenient for using constraints, this also makes it very easy to use the fixed
|
||||
/// amount of stack space (4096 bytes) SVM allocates just by increasing the number of accounts the
|
||||
/// instruction has. In SVM, each function has its own stack frame, meaning that it's possible to
|
||||
/// deserialize more accounts simply by deserializing them inside other functions (rather than in
|
||||
/// `try_accounts` which is already quite heavy).
|
||||
///
|
||||
/// The mentioned stack limitation can be solved using dynamic stack frames, see [SIMD-0166].
|
||||
///
|
||||
/// ## Compute units
|
||||
///
|
||||
/// Compute is harder to formulate, as it varies based on the inner account's type. That being said,
|
||||
/// there are a few things you can do to optimize compute units usage when using [`LazyAccount`]:
|
||||
///
|
||||
/// - Order account fields from fixed-size data (e.g. `u8`, `Pubkey`) to dynamic data (e.g. `Vec`).
|
||||
/// - Order account fields based on how frequently the field is accessed (starting with the most
|
||||
/// frequent).
|
||||
/// - Reduce or limit dynamic fields.
|
||||
///
|
||||
/// [`borsh`]: crate::prelude::borsh
|
||||
/// [`Account`]: crate::prelude::Account
|
||||
/// [SIMD-0166]: https://github.com/solana-foundation/solana-improvement-documents/pull/166
|
||||
pub struct LazyAccount<'info, T>
|
||||
where
|
||||
T: AccountSerialize + Discriminator + Owner + Clone,
|
||||
{
|
||||
/// **INTERNAL FIELD DO NOT USE!**
|
||||
#[doc(hidden)]
|
||||
pub __info: &'info AccountInfo<'info>,
|
||||
/// **INTERNAL FIELD DO NOT USE!**
|
||||
#[doc(hidden)]
|
||||
pub __account: Rc<RefCell<MaybeUninit<T>>>,
|
||||
/// **INTERNAL FIELD DO NOT USE!**
|
||||
#[doc(hidden)]
|
||||
pub __fields: Rc<RefCell<Option<Vec<bool>>>>,
|
||||
}
|
||||
|
||||
impl<'info, T> fmt::Debug for LazyAccount<'info, T>
|
||||
where
|
||||
T: AccountSerialize + Discriminator + Owner + Clone + fmt::Debug,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.debug_struct("LazyAccount")
|
||||
.field("info", &self.__info)
|
||||
.field("account", &self.__account)
|
||||
.field("fields", &self.__fields)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'info, T> LazyAccount<'info, T>
|
||||
where
|
||||
T: AccountSerialize + Discriminator + Owner + Clone,
|
||||
{
|
||||
fn new(info: &'info AccountInfo<'info>) -> LazyAccount<'info, T> {
|
||||
Self {
|
||||
__info: info,
|
||||
__account: Rc::new(RefCell::new(MaybeUninit::uninit())),
|
||||
__fields: Rc::new(RefCell::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check both the owner and the discriminator.
|
||||
pub fn try_from(info: &'info AccountInfo<'info>) -> Result<LazyAccount<'info, T>> {
|
||||
let data = &info.try_borrow_data()?;
|
||||
let disc = T::DISCRIMINATOR;
|
||||
if data.len() < disc.len() {
|
||||
return Err(ErrorCode::AccountDiscriminatorNotFound.into());
|
||||
}
|
||||
|
||||
let given_disc = &data[..disc.len()];
|
||||
if given_disc != disc {
|
||||
return Err(ErrorCode::AccountDiscriminatorMismatch.into());
|
||||
}
|
||||
|
||||
Self::try_from_unchecked(info)
|
||||
}
|
||||
|
||||
/// Check the owner but **not** the discriminator.
|
||||
pub fn try_from_unchecked(info: &'info AccountInfo<'info>) -> Result<LazyAccount<'info, T>> {
|
||||
if info.owner != &T::owner() {
|
||||
return Err(Error::from(ErrorCode::AccountOwnedByWrongProgram)
|
||||
.with_pubkeys((*info.owner, T::owner())));
|
||||
}
|
||||
|
||||
Ok(LazyAccount::new(info))
|
||||
}
|
||||
|
||||
/// Unload the deserialized account value by resetting the cache.
|
||||
///
|
||||
/// This is useful when observing side-effects of CPIs.
|
||||
///
|
||||
/// # Usage
|
||||
///
|
||||
/// ```ignore
|
||||
/// // Load the initial value
|
||||
/// let initial_value = ctx.accounts.my_account.load_field()?;
|
||||
///
|
||||
/// // Do CPI...
|
||||
///
|
||||
/// // We still have a reference to the account from `initial_value`, drop it before `unload`
|
||||
/// drop(initial_value);
|
||||
///
|
||||
/// // Load the updated value
|
||||
/// let updated_value = ctx.accounts.my_account.unload()?.load_field()?;
|
||||
/// ```
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// If there is an existing reference (mutable or not) created by any of the `load` methods.
|
||||
pub fn unload(&self) -> Result<&Self> {
|
||||
// TODO: Should we drop the initialized fields manually?
|
||||
*self.__account.borrow_mut() = MaybeUninit::uninit();
|
||||
*self.__fields.borrow_mut() = None;
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'info, B, T> Accounts<'info, B> for LazyAccount<'info, T>
|
||||
where
|
||||
T: AccountSerialize + Discriminator + Owner + Clone,
|
||||
{
|
||||
#[inline(never)]
|
||||
fn try_accounts(
|
||||
_program_id: &Pubkey,
|
||||
accounts: &mut &'info [AccountInfo<'info>],
|
||||
_ix_data: &[u8],
|
||||
_bumps: &mut B,
|
||||
_reallocs: &mut BTreeSet<Pubkey>,
|
||||
) -> Result<Self> {
|
||||
if accounts.is_empty() {
|
||||
return Err(ErrorCode::AccountNotEnoughKeys.into());
|
||||
}
|
||||
let account = &accounts[0];
|
||||
*accounts = &accounts[1..];
|
||||
LazyAccount::try_from(account)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'info, T> AccountsClose<'info> for LazyAccount<'info, T>
|
||||
where
|
||||
T: AccountSerialize + Discriminator + Owner + Clone,
|
||||
{
|
||||
fn close(&self, sol_destination: AccountInfo<'info>) -> Result<()> {
|
||||
crate::common::close(self.to_account_info(), sol_destination)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'info, T> ToAccountMetas for LazyAccount<'info, T>
|
||||
where
|
||||
T: AccountSerialize + Discriminator + Owner + Clone,
|
||||
{
|
||||
fn to_account_metas(&self, is_signer: Option<bool>) -> Vec<AccountMeta> {
|
||||
let is_signer = is_signer.unwrap_or(self.__info.is_signer);
|
||||
let meta = match self.__info.is_writable {
|
||||
false => AccountMeta::new_readonly(*self.__info.key, is_signer),
|
||||
true => AccountMeta::new(*self.__info.key, is_signer),
|
||||
};
|
||||
vec![meta]
|
||||
}
|
||||
}
|
||||
|
||||
impl<'info, T> ToAccountInfos<'info> for LazyAccount<'info, T>
|
||||
where
|
||||
T: AccountSerialize + Discriminator + Owner + Clone,
|
||||
{
|
||||
fn to_account_infos(&self) -> Vec<AccountInfo<'info>> {
|
||||
vec![self.to_account_info()]
|
||||
}
|
||||
}
|
||||
|
||||
impl<'info, T> AsRef<AccountInfo<'info>> for LazyAccount<'info, T>
|
||||
where
|
||||
T: AccountSerialize + Discriminator + Owner + Clone,
|
||||
{
|
||||
fn as_ref(&self) -> &AccountInfo<'info> {
|
||||
self.__info
|
||||
}
|
||||
}
|
||||
|
||||
impl<'info, T> Key for LazyAccount<'info, T>
|
||||
where
|
||||
T: AccountSerialize + Discriminator + Owner + Clone,
|
||||
{
|
||||
fn key(&self) -> Pubkey {
|
||||
*self.__info.key
|
||||
}
|
||||
}
|
|
@ -12,3 +12,6 @@ pub mod signer;
|
|||
pub mod system_account;
|
||||
pub mod sysvar;
|
||||
pub mod unchecked_account;
|
||||
|
||||
#[cfg(feature = "lazy-account")]
|
||||
pub mod lazy_account;
|
||||
|
|
|
@ -0,0 +1,202 @@
|
|||
use crate::{AnchorDeserialize, Pubkey};
|
||||
|
||||
/// A helper trait to make lazy deserialization work.
|
||||
///
|
||||
/// Currently this is only implemented for [`borsh`], as it's not necessary for zero copy via
|
||||
/// [`bytemuck`]. However, the functionality can be extended when we support custom serialization
|
||||
/// in the future.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// You should avoid implementing this trait manually.
|
||||
///
|
||||
/// It's currently implemented automatically if you derive [`AnchorDeserialize`]:
|
||||
///
|
||||
/// ```ignore
|
||||
/// #[derive(AnchorDeserialize)]
|
||||
/// pub struct MyStruct {
|
||||
/// field: u8,
|
||||
/// }
|
||||
/// ```
|
||||
pub trait Lazy: AnchorDeserialize {
|
||||
/// Get the serialized size of the type from the given buffer.
|
||||
///
|
||||
/// For performance reasons, this method does not verify the validity of the data, and should
|
||||
/// never fail.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// If the given buffer cannot be used to deserialize the data e.g. it's shorter than the
|
||||
/// expected data. However, this doesn't mean it will panic **whenever** there is an incorrect
|
||||
/// data e.g. passing **any** data for `bool::size_of` works, even when the buffer is empty.
|
||||
fn size_of(buf: &[u8]) -> usize;
|
||||
}
|
||||
|
||||
macro_rules! impl_sized {
|
||||
($ty: ty) => {
|
||||
impl Lazy for $ty {
|
||||
#[inline(always)]
|
||||
fn size_of(_buf: &[u8]) -> usize {
|
||||
::core::mem::size_of::<$ty>()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_sized!(bool);
|
||||
impl_sized!(u8);
|
||||
impl_sized!(u16);
|
||||
impl_sized!(u32);
|
||||
impl_sized!(u64);
|
||||
impl_sized!(u128);
|
||||
impl_sized!(i8);
|
||||
impl_sized!(i16);
|
||||
impl_sized!(i32);
|
||||
impl_sized!(i64);
|
||||
impl_sized!(i128);
|
||||
impl_sized!(f32);
|
||||
impl_sized!(f64);
|
||||
impl_sized!(Pubkey);
|
||||
|
||||
impl<T: Lazy, const N: usize> Lazy for [T; N] {
|
||||
#[inline(always)]
|
||||
fn size_of(buf: &[u8]) -> usize {
|
||||
N * T::size_of(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl Lazy for String {
|
||||
#[inline(always)]
|
||||
fn size_of(buf: &[u8]) -> usize {
|
||||
LEN + get_len(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Lazy> Lazy for Option<T> {
|
||||
#[inline(always)]
|
||||
fn size_of(buf: &[u8]) -> usize {
|
||||
1 + match buf.first() {
|
||||
Some(0) => 0,
|
||||
Some(1) => T::size_of(&buf[1..]),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Lazy> Lazy for Vec<T> {
|
||||
#[inline(always)]
|
||||
fn size_of(buf: &[u8]) -> usize {
|
||||
(0..get_len(buf)).fold(LEN, |acc, _| acc + T::size_of(&buf[acc..]))
|
||||
}
|
||||
}
|
||||
|
||||
/// `borsh` length identifier of unsized types.
|
||||
const LEN: usize = 4;
|
||||
|
||||
#[inline(always)]
|
||||
fn get_len(buf: &[u8]) -> usize {
|
||||
u32::from_le_bytes((buf[..LEN].try_into()).unwrap())
|
||||
.try_into()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::AnchorSerialize;
|
||||
|
||||
macro_rules! len {
|
||||
($val: expr) => {
|
||||
$val.try_to_vec().unwrap().len()
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sized() {
|
||||
// Sized inputs don't care about the passed data
|
||||
const EMPTY: &[u8] = &[];
|
||||
assert_eq!(bool::size_of(EMPTY), len!(true));
|
||||
assert_eq!(u8::size_of(EMPTY), len!(0u8));
|
||||
assert_eq!(u16::size_of(EMPTY), len!(0u16));
|
||||
assert_eq!(u32::size_of(EMPTY), len!(0u32));
|
||||
assert_eq!(u64::size_of(EMPTY), len!(0u64));
|
||||
assert_eq!(u128::size_of(EMPTY), len!(0u128));
|
||||
assert_eq!(i8::size_of(EMPTY), len!(0i8));
|
||||
assert_eq!(i16::size_of(EMPTY), len!(0i16));
|
||||
assert_eq!(i32::size_of(EMPTY), len!(0i32));
|
||||
assert_eq!(i64::size_of(EMPTY), len!(0i64));
|
||||
assert_eq!(i128::size_of(EMPTY), len!(0i128));
|
||||
assert_eq!(f32::size_of(EMPTY), len!(0f32));
|
||||
assert_eq!(f64::size_of(EMPTY), len!(0f64));
|
||||
assert_eq!(Pubkey::size_of(EMPTY), len!(Pubkey::default()));
|
||||
assert_eq!(<[i32; 4]>::size_of(EMPTY), len!([0i32; 4]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn r#unsized() {
|
||||
assert_eq!(String::size_of(&[1, 0, 0, 0, 65]), len!(String::from("a")));
|
||||
assert_eq!(<Option<u8>>::size_of(&[0]), len!(Option::<u8>::None));
|
||||
assert_eq!(<Option<u8>>::size_of(&[1, 1]), len!(Some(1u8)));
|
||||
assert_eq!(<Vec<u8>>::size_of(&[1, 0, 0, 0, 1]), len!(vec![1u8]));
|
||||
assert_eq!(
|
||||
<Vec<String>>::size_of(&[1, 0, 0, 0, 1, 0, 0, 0, 65]),
|
||||
len!(vec![String::from("a")])
|
||||
);
|
||||
assert_eq!(
|
||||
<Vec<String>>::size_of(&[2, 0, 0, 0, 1, 0, 0, 0, 65, 2, 0, 0, 0, 65, 66]),
|
||||
len!(vec![String::from("a"), String::from("ab")])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defined() {
|
||||
// Struct
|
||||
#[derive(AnchorSerialize, AnchorDeserialize)]
|
||||
struct MyStruct {
|
||||
a: u8,
|
||||
b: Vec<u8>,
|
||||
c: Option<String>,
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
MyStruct::size_of(&[1, 2, 0, 0, 0, 1, 2, 1, 1, 0, 0, 0, 65]),
|
||||
len!(MyStruct {
|
||||
a: 1,
|
||||
b: vec![1u8, 2],
|
||||
c: Some(String::from("a"))
|
||||
})
|
||||
);
|
||||
|
||||
// Enum
|
||||
#[derive(AnchorSerialize, AnchorDeserialize)]
|
||||
enum MyEnum {
|
||||
Unit,
|
||||
Named { a: u8 },
|
||||
Unnamed(i16, i16),
|
||||
}
|
||||
|
||||
assert_eq!(MyEnum::size_of(&[0]), len!(MyEnum::Unit));
|
||||
assert_eq!(MyEnum::size_of(&[1, 23]), len!(MyEnum::Named { a: 1 }));
|
||||
assert_eq!(
|
||||
MyEnum::size_of(&[2, 1, 2, 1, 2]),
|
||||
len!(MyEnum::Unnamed(1, 2))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generic() {
|
||||
#[derive(AnchorSerialize, AnchorDeserialize)]
|
||||
struct GenericStruct<T: Lazy> {
|
||||
t: T,
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
GenericStruct::<i64>::size_of(&[1, 2, 3, 4, 5, 6, 7, 8]),
|
||||
len!(GenericStruct { t: 1i64 })
|
||||
);
|
||||
assert_eq!(
|
||||
GenericStruct::<Vec<u8>>::size_of(&[8, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8]),
|
||||
len!(GenericStruct { t: vec![0u8; 8] })
|
||||
);
|
||||
}
|
||||
}
|
|
@ -44,8 +44,11 @@ pub mod event;
|
|||
#[doc(hidden)]
|
||||
pub mod idl;
|
||||
pub mod system_program;
|
||||
|
||||
mod vec;
|
||||
|
||||
#[cfg(feature = "lazy-account")]
|
||||
mod lazy;
|
||||
|
||||
pub use crate::bpf_upgradeable_state::*;
|
||||
pub use anchor_attribute_access_control::access_control;
|
||||
pub use anchor_attribute_account::{account, declare_id, pubkey, zero_copy};
|
||||
|
@ -440,19 +443,21 @@ pub mod prelude {
|
|||
|
||||
#[cfg(feature = "interface-instructions")]
|
||||
pub use super::interface;
|
||||
|
||||
#[cfg(feature = "lazy-account")]
|
||||
pub use super::accounts::lazy_account::LazyAccount;
|
||||
}
|
||||
|
||||
/// Internal module used by macros and unstable apis.
|
||||
#[doc(hidden)]
|
||||
pub mod __private {
|
||||
pub use anchor_attribute_account::ZeroCopyAccessor;
|
||||
|
||||
pub use anchor_attribute_event::EventIndex;
|
||||
|
||||
pub use base64;
|
||||
|
||||
pub use bytemuck;
|
||||
|
||||
pub use crate::{bpf_writer::BpfWriter, common::is_closed};
|
||||
|
||||
use solana_program::pubkey::Pubkey;
|
||||
|
||||
// Used to calculate the maximum between two expressions.
|
||||
|
@ -478,6 +483,11 @@ pub mod __private {
|
|||
input.to_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "lazy-account")]
|
||||
pub use crate::lazy::Lazy;
|
||||
#[cfg(feature = "lazy-account")]
|
||||
pub use anchor_derive_serde::Lazy;
|
||||
}
|
||||
|
||||
/// Ensures a condition is true, otherwise returns with the given error.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use quote::quote;
|
||||
use quote::{format_ident, quote};
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::*;
|
||||
|
@ -260,6 +260,13 @@ pub fn generate_constraint_has_one(
|
|||
Ty::AccountLoader(_) => quote! {#ident.load()?},
|
||||
_ => quote! {#ident},
|
||||
};
|
||||
let my_key = match &f.ty {
|
||||
Ty::LazyAccount(_) => {
|
||||
let load_ident = format_ident!("load_{}", target.to_token_stream().to_string());
|
||||
quote! { *#field.#load_ident()? }
|
||||
}
|
||||
_ => quote! { #field.#target },
|
||||
};
|
||||
let error = generate_custom_error(
|
||||
ident,
|
||||
&c.error,
|
||||
|
@ -272,7 +279,7 @@ pub fn generate_constraint_has_one(
|
|||
quote! {
|
||||
{
|
||||
#target_optional_check
|
||||
let my_key = #field.#target;
|
||||
let my_key = #my_key;
|
||||
let target_key = #target.key();
|
||||
if my_key != target_key {
|
||||
return #error;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::accounts_codegen::constraints::OptionalCheckScope;
|
||||
use crate::codegen::accounts::{generics, ParsedGenerics};
|
||||
use crate::{AccountField, AccountsStruct};
|
||||
use crate::{AccountField, AccountsStruct, Ty};
|
||||
use quote::quote;
|
||||
|
||||
// Generates the `Exit` trait implementation.
|
||||
|
@ -46,9 +46,16 @@ pub fn generate(accs: &AccountsStruct) -> proc_macro2::TokenStream {
|
|||
} else {
|
||||
match f.constraints.is_mutable() {
|
||||
false => quote! {},
|
||||
true => quote! {
|
||||
anchor_lang::AccountsExit::exit(&self.#ident, program_id)
|
||||
.map_err(|e| e.with_account_name(#name_str))?;
|
||||
true => match &f.ty {
|
||||
// `LazyAccount` is special because it has a custom `exit` method.
|
||||
Ty::LazyAccount(_) => quote! {
|
||||
self.#ident.exit(program_id)
|
||||
.map_err(|e| e.with_account_name(#name_str))?;
|
||||
},
|
||||
_ => quote! {
|
||||
anchor_lang::AccountsExit::exit(&self.#ident, program_id)
|
||||
.map_err(|e| e.with_account_name(#name_str))?;
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,6 +59,7 @@ pub fn gen_idl_build_impl_accounts_struct(accounts: &AccountsStruct) -> TokenStr
|
|||
{
|
||||
Some(&ty.account_type_path)
|
||||
}
|
||||
Ty::LazyAccount(ty) => Some(&ty.account_type_path),
|
||||
Ty::AccountLoader(ty) => Some(&ty.account_type_path),
|
||||
Ty::InterfaceAccount(ty) => Some(&ty.account_type_path),
|
||||
_ => None,
|
||||
|
|
|
@ -261,6 +261,7 @@ impl AccountField {
|
|||
let qualified_ty_name = match self {
|
||||
AccountField::Field(field) => match &field.ty {
|
||||
Ty::Account(account) => Some(parser::tts_to_string(&account.account_type_path)),
|
||||
Ty::LazyAccount(account) => Some(parser::tts_to_string(&account.account_type_path)),
|
||||
_ => None,
|
||||
},
|
||||
AccountField::CompositeField(field) => Some(field.symbol.clone()),
|
||||
|
@ -404,6 +405,23 @@ impl Field {
|
|||
stream
|
||||
}
|
||||
}
|
||||
Ty::LazyAccount(_) => {
|
||||
if checked {
|
||||
quote! {
|
||||
match #container_ty::try_from(&#field) {
|
||||
Ok(val) => val,
|
||||
Err(e) => return Err(e.with_account_name(#field_str))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
match #container_ty::try_from_unchecked(&#field) {
|
||||
Ok(val) => val,
|
||||
Err(e) => return Err(e.with_account_name(#field_str))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ty::AccountLoader(_) => {
|
||||
if checked {
|
||||
quote! {
|
||||
|
@ -446,6 +464,9 @@ impl Field {
|
|||
Ty::Account(_) => quote! {
|
||||
anchor_lang::accounts::account::Account
|
||||
},
|
||||
Ty::LazyAccount(_) => quote! {
|
||||
anchor_lang::accounts::lazy_account::LazyAccount
|
||||
},
|
||||
Ty::AccountLoader(_) => quote! {
|
||||
anchor_lang::accounts::account_loader::AccountLoader
|
||||
},
|
||||
|
@ -487,6 +508,12 @@ impl Field {
|
|||
#ident
|
||||
}
|
||||
}
|
||||
Ty::LazyAccount(ty) => {
|
||||
let ident = &ty.account_type_path;
|
||||
quote! {
|
||||
#ident
|
||||
}
|
||||
}
|
||||
Ty::InterfaceAccount(ty) => {
|
||||
let ident = &ty.account_type_path;
|
||||
quote! {
|
||||
|
@ -545,6 +572,7 @@ pub enum Ty {
|
|||
AccountLoader(AccountLoaderTy),
|
||||
Sysvar(SysvarTy),
|
||||
Account(AccountTy),
|
||||
LazyAccount(LazyAccountTy),
|
||||
Program(ProgramTy),
|
||||
Interface(InterfaceTy),
|
||||
InterfaceAccount(InterfaceAccountTy),
|
||||
|
@ -581,6 +609,12 @@ pub struct AccountTy {
|
|||
pub boxed: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct LazyAccountTy {
|
||||
// The struct type of the account.
|
||||
pub account_type_path: TypePath,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct InterfaceAccountTy {
|
||||
// The struct type of the account.
|
||||
|
|
|
@ -1177,7 +1177,10 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
|
|||
|
||||
// Require a known account type that implements the `Discriminator` trait so that we can
|
||||
// get the discriminator length dynamically
|
||||
if !matches!(&self.f_ty, Some(Ty::Account(_) | Ty::AccountLoader(_))) {
|
||||
if !matches!(
|
||||
&self.f_ty,
|
||||
Some(Ty::Account(_) | Ty::LazyAccount(_) | Ty::AccountLoader(_))
|
||||
) {
|
||||
return Err(ParseError::new(
|
||||
c.span(),
|
||||
"`zero` constraint requires the type to implement the `Discriminator` trait",
|
||||
|
@ -1189,11 +1192,12 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
|
|||
|
||||
fn add_realloc(&mut self, c: Context<ConstraintRealloc>) -> ParseResult<()> {
|
||||
if !matches!(self.f_ty, Some(Ty::Account(_)))
|
||||
&& !matches!(self.f_ty, Some(Ty::LazyAccount(_)))
|
||||
&& !matches!(self.f_ty, Some(Ty::AccountLoader(_)))
|
||||
{
|
||||
return Err(ParseError::new(
|
||||
c.span(),
|
||||
"realloc must be on an Account or AccountLoader",
|
||||
"realloc must be on an Account, LazyAccount or AccountLoader",
|
||||
));
|
||||
}
|
||||
if self.mutable.is_none() {
|
||||
|
@ -1239,11 +1243,12 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
|
|||
|
||||
fn add_close(&mut self, c: Context<ConstraintClose>) -> ParseResult<()> {
|
||||
if !matches!(self.f_ty, Some(Ty::Account(_)))
|
||||
&& !matches!(self.f_ty, Some(Ty::LazyAccount(_)))
|
||||
&& !matches!(self.f_ty, Some(Ty::AccountLoader(_)))
|
||||
{
|
||||
return Err(ParseError::new(
|
||||
c.span(),
|
||||
"close must be on an Account, AccountLoader",
|
||||
"close must be on an Account, LazyAccount or AccountLoader",
|
||||
));
|
||||
}
|
||||
if self.mutable.is_none() {
|
||||
|
|
|
@ -334,6 +334,7 @@ fn is_field_primitive(f: &syn::Field) -> ParseResult<bool> {
|
|||
| "UncheckedAccount"
|
||||
| "AccountLoader"
|
||||
| "Account"
|
||||
| "LazyAccount"
|
||||
| "Program"
|
||||
| "Interface"
|
||||
| "InterfaceAccount"
|
||||
|
@ -352,6 +353,7 @@ fn parse_ty(f: &syn::Field) -> ParseResult<(Ty, bool)> {
|
|||
"UncheckedAccount" => Ty::UncheckedAccount,
|
||||
"AccountLoader" => Ty::AccountLoader(parse_program_account_loader(&path)?),
|
||||
"Account" => Ty::Account(parse_account_ty(&path)?),
|
||||
"LazyAccount" => Ty::LazyAccount(parse_lazy_account_ty(&path)?),
|
||||
"Program" => Ty::Program(parse_program_ty(&path)?),
|
||||
"Interface" => Ty::Interface(parse_interface_ty(&path)?),
|
||||
"InterfaceAccount" => Ty::InterfaceAccount(parse_interface_account_ty(&path)?),
|
||||
|
@ -444,6 +446,11 @@ fn parse_account_ty(path: &syn::Path) -> ParseResult<AccountTy> {
|
|||
})
|
||||
}
|
||||
|
||||
fn parse_lazy_account_ty(path: &syn::Path) -> ParseResult<LazyAccountTy> {
|
||||
let account_type_path = parse_account(path)?;
|
||||
Ok(LazyAccountTy { account_type_path })
|
||||
}
|
||||
|
||||
fn parse_interface_account_ty(path: &syn::Path) -> ParseResult<InterfaceAccountTy> {
|
||||
let account_type_path = parse_account(path)?;
|
||||
let boxed = parser::tts_to_string(path)
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
[programs.localnet]
|
||||
lazy_account = "LazyAccount11111111111111111111111111111111"
|
||||
|
||||
[provider]
|
||||
cluster = "localnet"
|
||||
wallet = "~/.config/solana/id.json"
|
||||
|
||||
[scripts]
|
||||
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
|
|
@ -0,0 +1,14 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"programs/*"
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[profile.release]
|
||||
overflow-checks = true
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
||||
[profile.release.build-override]
|
||||
opt-level = 3
|
||||
incremental = false
|
||||
codegen-units = 1
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "lazy-account",
|
||||
"version": "0.30.1",
|
||||
"license": "(MIT OR Apache-2.0)",
|
||||
"homepage": "https://github.com/coral-xyz/anchor#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/coral-xyz/anchor/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/coral-xyz/anchor.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=17"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "lazy-account"
|
||||
version = "0.1.0"
|
||||
description = "Created with Anchor"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
name = "lazy_account"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
cpi = ["no-entrypoint"]
|
||||
no-entrypoint = []
|
||||
no-idl = []
|
||||
idl-build = ["anchor-lang/idl-build"]
|
||||
|
||||
[dependencies]
|
||||
anchor-lang = { path = "../../../../lang", features = ["lazy-account"] }
|
|
@ -0,0 +1,2 @@
|
|||
[target.bpfel-unknown-unknown.dependencies.std]
|
||||
features = []
|
|
@ -0,0 +1,114 @@
|
|||
//! Tests demonstraing the usage of [`LazyAccount`].
|
||||
//!
|
||||
//! The tests have been simplied by using a stack heavy account in order to demonstrate the usage
|
||||
//! and its usefulness without adding excessive amount of accounts.
|
||||
//!
|
||||
//! See the individual instructions for more documentation: [`Init`], [`Read`], [`Write`].
|
||||
|
||||
use anchor_lang::prelude::*;
|
||||
|
||||
declare_id!("LazyAccount11111111111111111111111111111111");
|
||||
|
||||
#[program]
|
||||
pub mod lazy_account {
|
||||
use super::*;
|
||||
|
||||
pub fn init(ctx: Context<Init>) -> Result<()> {
|
||||
let mut my_account = ctx.accounts.my_account.load_mut()?;
|
||||
my_account.authority = ctx.accounts.authority.key();
|
||||
|
||||
for _ in 0..MAX_DATA_LEN {
|
||||
my_account.dynamic.push(ctx.accounts.authority.key());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read(ctx: Context<Read>) -> Result<()> {
|
||||
// Cached load due to the `has_one` constraint
|
||||
let authority = ctx.accounts.my_account.load_authority()?;
|
||||
msg!("Authority: {}", authority);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write(ctx: Context<Write>, new_authority: Pubkey) -> Result<()> {
|
||||
// Cached load due to the `has_one` constraint
|
||||
*ctx.accounts.my_account.load_mut_authority()? = new_authority;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct Init<'info> {
|
||||
#[account(mut)]
|
||||
pub authority: Signer<'info>,
|
||||
#[account(
|
||||
init,
|
||||
payer = authority,
|
||||
space = MyAccount::DISCRIMINATOR.len() + MyAccount::INIT_SPACE,
|
||||
seeds = [b"my_account"],
|
||||
bump
|
||||
)]
|
||||
pub my_account: LazyAccount<'info, MyAccount>,
|
||||
/// Using `Account` instead of `LazyAccount` would either make the instruction fail due to
|
||||
/// access violation errors, or worse, it would cause undefined behavior instead.
|
||||
///
|
||||
/// Using `Account` with Solana v1.18.17 (`platform-tools` v1.41) results in a stack violation
|
||||
/// error (without a compiler error/warning on build).
|
||||
#[account(
|
||||
init,
|
||||
payer = authority,
|
||||
space = StackHeavyAccount::DISCRIMINATOR.len() + StackHeavyAccount::INIT_SPACE,
|
||||
seeds = [b"stack_heavy_account"],
|
||||
bump
|
||||
)]
|
||||
pub stack_heavy_account: LazyAccount<'info, StackHeavyAccount>,
|
||||
pub system_program: Program<'info, System>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct Read<'info> {
|
||||
pub authority: Signer<'info>,
|
||||
/// Using `Account` or `Box<Account>` instead of `LazyAccount` would increase the compute
|
||||
/// units usage by ~90k units due to the unnecessary deserialization of the unused fields.
|
||||
#[account(seeds = [b"my_account"], bump, has_one = authority)]
|
||||
pub my_account: LazyAccount<'info, MyAccount>,
|
||||
/// This account imitates heavy stack usage in more complex programs
|
||||
#[account(seeds = [b"stack_heavy_account"], bump)]
|
||||
pub stack_heavy_account: Account<'info, StackHeavyAccount>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct Write<'info> {
|
||||
pub authority: Signer<'info>,
|
||||
/// Using `Account` instead of `LazyAccount` would either make the instruction fail due to stack
|
||||
/// violation errors, or worse, it would cause undefined behavior instead.
|
||||
///
|
||||
/// Using `Account` with Solana v1.18.17 (`platform-tools` v1.41) results in undefined behavior
|
||||
/// in this instruction, and the authority field gets corrupted when writing.
|
||||
#[account(mut, seeds = [b"my_account"], bump, has_one = authority)]
|
||||
pub my_account: LazyAccount<'info, MyAccount>,
|
||||
/// This account imitates heavy stack usage in more complex programs
|
||||
#[account(seeds = [b"stack_heavy_account"], bump)]
|
||||
pub stack_heavy_account: Account<'info, StackHeavyAccount>,
|
||||
}
|
||||
|
||||
const MAX_DATA_LEN: usize = 256;
|
||||
|
||||
#[account]
|
||||
#[derive(InitSpace)]
|
||||
pub struct MyAccount {
|
||||
pub authority: Pubkey,
|
||||
/// Fixed size data
|
||||
pub fixed: [Pubkey; 8],
|
||||
/// Dynamic sized data also works, unlike `AccountLoader`
|
||||
#[max_len(MAX_DATA_LEN)]
|
||||
pub dynamic: Vec<Pubkey>,
|
||||
}
|
||||
|
||||
/// Stack heavy filler account that imitates heavy stack usage caused my many accounts
|
||||
#[account]
|
||||
#[derive(InitSpace)]
|
||||
pub struct StackHeavyAccount {
|
||||
pub data: [u8; 1600],
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import * as anchor from "@coral-xyz/anchor";
|
||||
import assert from "assert";
|
||||
|
||||
import type { LazyAccount } from "../target/types/lazy_account";
|
||||
|
||||
describe("lazy-account", () => {
|
||||
anchor.setProvider(anchor.AnchorProvider.env());
|
||||
const program: anchor.Program<LazyAccount> = anchor.workspace.lazyAccount;
|
||||
|
||||
it("Can init", async () => {
|
||||
const { pubkeys, signature } = await program.methods.init().rpcAndKeys();
|
||||
await program.provider.connection.confirmTransaction(
|
||||
signature,
|
||||
"confirmed"
|
||||
);
|
||||
const myAccount = await program.account.myAccount.fetch(pubkeys.myAccount);
|
||||
assert(myAccount.authority.equals(program.provider.publicKey!));
|
||||
});
|
||||
|
||||
it("Can read", async () => {
|
||||
await program.methods.read().rpc();
|
||||
});
|
||||
|
||||
it("Can write", async () => {
|
||||
const newAuthority = anchor.web3.PublicKey.default;
|
||||
const { pubkeys, signature } = await program.methods
|
||||
.write(newAuthority)
|
||||
.rpcAndKeys();
|
||||
await program.provider.connection.confirmTransaction(
|
||||
signature,
|
||||
"confirmed"
|
||||
);
|
||||
const myAccount = await program.account.myAccount.fetch(pubkeys.myAccount);
|
||||
assert(myAccount.authority.equals(newAuthority));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"types": ["mocha", "chai"],
|
||||
"lib": ["es2015"],
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
|
@ -25,6 +25,7 @@
|
|||
"idl",
|
||||
"ido-pool",
|
||||
"interface",
|
||||
"lazy-account",
|
||||
"lockup",
|
||||
"misc",
|
||||
"multisig",
|
||||
|
|
Loading…
Reference in New Issue