acheron 1 жил өмнө
parent
commit
879601e632

+ 2 - 0
.github/workflows/reusable-tests.yaml

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

+ 1 - 0
CHANGELOG.md

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

+ 1 - 0
lang/Cargo.toml

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

+ 1 - 0
lang/attribute/account/Cargo.toml

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

+ 302 - 0
lang/attribute/account/src/lazy.rs

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

+ 16 - 0
lang/attribute/account/src/lib.rs

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

+ 1 - 0
lang/derive/serde/Cargo.toml

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

+ 63 - 0
lang/derive/serde/src/lazy.rs

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

+ 23 - 1
lang/derive/serde/src/lib.rs

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

+ 338 - 0
lang/src/accounts/lazy_account.rs

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

+ 3 - 0
lang/src/accounts/mod.rs

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

+ 202 - 0
lang/src/lazy.rs

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

+ 14 - 4
lang/src/lib.rs

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

+ 9 - 2
lang/syn/src/codegen/accounts/constraints.rs

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

+ 11 - 4
lang/syn/src/codegen/accounts/exit.rs

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

+ 1 - 0
lang/syn/src/idl/accounts.rs

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

+ 34 - 0
lang/syn/src/lib.rs

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

+ 8 - 3
lang/syn/src/parser/accounts/constraints.rs

@@ -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() {

+ 7 - 0
lang/syn/src/parser/accounts/mod.rs

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

+ 9 - 0
tests/lazy-account/Anchor.toml

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

+ 14 - 0
tests/lazy-account/Cargo.toml

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

+ 16 - 0
tests/lazy-account/package.json

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

+ 19 - 0
tests/lazy-account/programs/lazy-account/Cargo.toml

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

+ 2 - 0
tests/lazy-account/programs/lazy-account/Xargo.toml

@@ -0,0 +1,2 @@
+[target.bpfel-unknown-unknown.dependencies.std]
+features = []

+ 114 - 0
tests/lazy-account/programs/lazy-account/src/lib.rs

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

+ 36 - 0
tests/lazy-account/tests/lazy-account.ts

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

+ 11 - 0
tests/lazy-account/tsconfig.json

@@ -0,0 +1,11 @@
+{
+  "compilerOptions": {
+    "types": ["mocha", "chai"],
+    "lib": ["es2015"],
+    "module": "commonjs",
+    "target": "es6",
+    "esModuleInterop": true,
+    "strict": true,
+    "skipLibCheck": true
+  }
+}

+ 1 - 0
tests/package.json

@@ -25,6 +25,7 @@
     "idl",
     "ido-pool",
     "interface",
+    "lazy-account",
     "lockup",
     "misc",
     "multisig",