Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

lang: add realloc constraint group #1943

Merged
merged 14 commits into from
Jun 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,8 @@ jobs:
path: tests/escrow
- cmd: cd tests/pyth && anchor test --skip-lint && npx tsc --noEmit
path: tests/pyth
- cmd: cd tests/realloc && anchor test --skip-lint && npx tsc --noEmit
path: tests/realloc
- cmd: cd tests/system-accounts && anchor test --skip-lint
path: tests/system-accounts
- cmd: cd tests/misc && anchor test --skip-lint && npx tsc --noEmit
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ The minor version will be incremented upon a breaking change and the patch versi

### Features

* lang: Add `realloc`, `realloc::payer`, and `realloc::zero` as a new constraint group for program accounts ([#1943](https://github.com/project-serum/anchor/pull/1943)).
* lang: Add `PartialEq` and `Eq` for `anchor_lang::Error` ([#1544](https://github.com/project-serum/anchor/pull/1544)).
* cli: Add `--skip-build` to `anchor publish` ([#1786](https://github.
com/project-serum/anchor/pull/1841)).
Expand Down
39 changes: 39 additions & 0 deletions lang/derive/accounts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ use syn::parse_macro_input;
///
/// - [Normal Constraints](#normal-constraints)
/// - [SPL Constraints](#spl-constraints)
///
/// # Normal Constraints
/// <table>
/// <thead>
Expand Down Expand Up @@ -418,6 +419,44 @@ use syn::parse_macro_input;
/// </code></pre>
/// </td>
/// </tr>
/// <tr>
/// <td>
/// <code>#[account(realloc = &lt;space&gt;, realloc::payer = &lt;target&gt;, realloc::zero = &lt;bool&gt;)]</code>
/// </td>
/// <td>
/// Used to <a href="https://docs.rs/solana-program/latest/solana_program/account_info/struct.AccountInfo.html#method.realloc" target = "_blank" rel = "noopener noreferrer">realloc</a>
/// program account space at the beginning of an instruction.
/// <br><br>
/// The account must be marked as <code>mut</code> and applied to either <code>Account</code> or <code>AccountLoader</code> types.
/// <br><br>
/// If the change in account data length is additive, lamports will be transferred from the <code>realloc::payer</code> into the
/// program account in order to maintain rent exemption. Likewise, if the change is subtractive, lamports will be transferred from
/// the program account back into the <code>realloc::payer</code>.
/// <br><br>
/// The <code>realloc::zero</code> constraint is required in order to determine whether the new memory should be zero initialized after
/// reallocation. Please read the documentation on the <code>AccountInfo::realloc</code> function linked above to understand the
/// caveats regarding compute units when providing <code>true</code or <code>false</code> to this flag.
/// <br><br>
/// Example:
/// <pre>
/// #[derive(Accounts)]
/// pub struct Example {
/// #[account(mut)]
/// pub payer: Signer<'info>,
/// #[account(
/// mut,
/// seeds = [b"example"],
/// bump,
/// realloc = 8 + std::mem::size_of::<MyType>() + 100,
/// realloc::payer = payer,
/// realloc::zero = false,
/// )]
/// pub acc: Account<'info, MyType>,
/// pub system_program: Program<'info, System>,
/// }
/// </pre>
/// </td>
/// </tr>
/// </tbody>
/// </table>
///
Expand Down
45 changes: 45 additions & 0 deletions lang/syn/src/codegen/accounts/constraints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ pub fn linearize(c_group: &ConstraintGroup) -> Vec<Constraint> {
associated_token,
token_account,
mint,
realloc,
} = c_group.clone();

let mut constraints = Vec::new();
Expand All @@ -69,6 +70,9 @@ pub fn linearize(c_group: &ConstraintGroup) -> Vec<Constraint> {
if let Some(c) = init {
constraints.push(Constraint::Init(c));
}
if let Some(c) = realloc {
constraints.push(Constraint::Realloc(c));
}
if let Some(c) = seeds {
constraints.push(Constraint::Seeds(c));
}
Expand Down Expand Up @@ -130,6 +134,7 @@ fn generate_constraint(f: &Field, c: &Constraint) -> proc_macro2::TokenStream {
Constraint::AssociatedToken(c) => generate_constraint_associated_token(f, c),
Constraint::TokenAccount(c) => generate_constraint_token_account(f, c),
Constraint::Mint(c) => generate_constraint_mint(f, c),
Constraint::Realloc(c) => generate_constraint_realloc(f, c),
}
}

Expand Down Expand Up @@ -320,6 +325,46 @@ pub fn generate_constraint_rent_exempt(
}
}

fn generate_constraint_realloc(f: &Field, c: &ConstraintReallocGroup) -> proc_macro2::TokenStream {
let field = &f.ident;
let new_space = &c.space;
let payer = &c.payer;
let zero = &c.zero;

quote! {
let __anchor_rent = Rent::get()?;
let __field_info = #field.to_account_info();
let __additive = #new_space > __field_info.data_len();

let __delta_space = if __additive {
#new_space.checked_sub(__field_info.data_len()).unwrap()
} else {
__field_info.data_len().checked_sub(#new_space).unwrap()
};

if __delta_space > 0 {
Copy link
Contributor

Choose a reason for hiding this comment

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

from the solana source code:

    /// Note:  Account data can be increased within a single call by up to
    /// `solana_program::entrypoint::MAX_PERMITTED_DATA_INCREASE` bytes.

perhaps we can give an informative error if delta_space exceeds this? Not sure what that error looks like on the Solana end.

if __additive {
anchor_lang::system_program::transfer(
anchor_lang::context::CpiContext::new(
system_program.to_account_info(),
anchor_lang::system_program::Transfer {
from: #payer.to_account_info(),
to: __field_info.clone(),
},
),
__anchor_rent.minimum_balance(#new_space).checked_sub(__field_info.lamports()).unwrap(),
)?;
} else {
let __lamport_amt = __field_info.lamports().checked_sub(__anchor_rent.minimum_balance(#new_space)).unwrap();
**#payer.to_account_info().lamports.borrow_mut() = #payer.to_account_info().lamports().checked_add(__lamport_amt).unwrap();
**__field_info.lamports.borrow_mut() = __field_info.lamports().checked_sub(__lamport_amt).unwrap();
}

#field.to_account_info().realloc(#new_space, #zero)?;
}
}
}

fn generate_constraint_init_group(f: &Field, c: &ConstraintInitGroup) -> proc_macro2::TokenStream {
let field = &f.ident;
let name_str = f.ident.to_string();
Expand Down
27 changes: 27 additions & 0 deletions lang/syn/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,7 @@ pub struct ConstraintGroup {
associated_token: Option<ConstraintAssociatedToken>,
token_account: Option<ConstraintTokenAccountGroup>,
mint: Option<ConstraintTokenMintGroup>,
realloc: Option<ConstraintReallocGroup>,
}

impl ConstraintGroup {
Expand Down Expand Up @@ -680,6 +681,7 @@ pub enum Constraint {
Address(ConstraintAddress),
TokenAccount(ConstraintTokenAccountGroup),
Mint(ConstraintTokenMintGroup),
Realloc(ConstraintReallocGroup),
}

// Constraint token is a single keyword in a `#[account(<TOKEN>)]` attribute.
Expand Down Expand Up @@ -711,6 +713,9 @@ pub enum ConstraintToken {
MintDecimals(Context<ConstraintMintDecimals>),
Bump(Context<ConstraintTokenBump>),
ProgramSeed(Context<ConstraintProgramSeed>),
Realloc(Context<ConstraintRealloc>),
ReallocPayer(Context<ConstraintReallocPayer>),
ReallocZero(Context<ConstraintReallocZero>),
}

impl Parse for ConstraintToken {
Expand All @@ -735,6 +740,28 @@ pub struct ConstraintMut {
pub error: Option<Expr>,
}

#[derive(Debug, Clone)]
pub struct ConstraintReallocGroup {
pub payer: Expr,
pub space: Expr,
pub zero: Expr,
}

#[derive(Debug, Clone)]
pub struct ConstraintRealloc {
pub space: Expr,
}

#[derive(Debug, Clone)]
pub struct ConstraintReallocPayer {
pub target: Expr,
}

#[derive(Debug, Clone)]
pub struct ConstraintReallocZero {
pub zero: Expr,
}

#[derive(Debug, Clone)]
pub struct ConstraintSigner {
pub error: Option<Expr>,
Expand Down
124 changes: 124 additions & 0 deletions lang/syn/src/parser/accounts/constraints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,47 @@ pub fn parse_token(stream: ParseStream) -> ParseResult<ConstraintToken> {
))
}
}
"realloc" => {
if stream.peek(Token![=]) {
stream.parse::<Token![=]>()?;
let span = ident
.span()
.join(stream.span())
.unwrap_or_else(|| ident.span());
ConstraintToken::Realloc(Context::new(
span,
ConstraintRealloc {
space: stream.parse()?,
},
))
} else {
stream.parse::<Token![:]>()?;
stream.parse::<Token![:]>()?;
let kw = stream.call(Ident::parse_any)?.to_string();
stream.parse::<Token![=]>()?;

let span = ident
.span()
.join(stream.span())
.unwrap_or_else(|| ident.span());

match kw.as_str() {
"payer" => ConstraintToken::ReallocPayer(Context::new(
span,
ConstraintReallocPayer {
target: stream.parse()?,
},
)),
"zero" => ConstraintToken::ReallocZero(Context::new(
span,
ConstraintReallocZero {
zero: stream.parse()?,
},
)),
_ => return Err(ParseError::new(ident.span(), "Invalid attribute. realloc::payer and realloc::zero are the only valid attributes")),
}
}
}
_ => {
stream.parse::<Token![=]>()?;
let span = ident
Expand Down Expand Up @@ -336,6 +377,9 @@ pub struct ConstraintGroupBuilder<'ty> {
pub mint_decimals: Option<Context<ConstraintMintDecimals>>,
pub bump: Option<Context<ConstraintTokenBump>>,
pub program_seed: Option<Context<ConstraintProgramSeed>>,
pub realloc: Option<Context<ConstraintRealloc>>,
pub realloc_payer: Option<Context<ConstraintReallocPayer>>,
pub realloc_zero: Option<Context<ConstraintReallocZero>>,
}

impl<'ty> ConstraintGroupBuilder<'ty> {
Expand Down Expand Up @@ -367,6 +411,9 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
mint_decimals: None,
bump: None,
program_seed: None,
realloc: None,
realloc_payer: None,
realloc_zero: None,
}
}

Expand Down Expand Up @@ -462,6 +509,22 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
}
}

// Realloc.
if let Some(r) = &self.realloc {
if self.realloc_payer.is_none() {
return Err(ParseError::new(
r.span(),
"realloc::payer must be provided when using realloc",
));
}
if self.realloc_zero.is_none() {
return Err(ParseError::new(
r.span(),
"realloc::zero must be provided when using realloc",
));
}
}

// Zero.
if let Some(z) = &self.zeroed {
match self.mutable {
Expand Down Expand Up @@ -549,6 +612,9 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
mint_decimals,
bump,
program_seed,
realloc,
realloc_payer,
realloc_zero,
} = self;

// Converts Option<Context<T>> -> Option<T>.
Expand Down Expand Up @@ -667,6 +733,11 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
}
},
})).transpose()?,
realloc: realloc.as_ref().map(|r| ConstraintReallocGroup {
payer: into_inner!(realloc_payer).unwrap().target,
space: r.space.clone(),
zero: into_inner!(realloc_zero).unwrap().zero,
}),
zeroed: into_inner!(zeroed),
mutable: into_inner!(mutable),
signer: into_inner!(signer),
Expand Down Expand Up @@ -713,6 +784,9 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
ConstraintToken::MintDecimals(c) => self.add_mint_decimals(c),
ConstraintToken::Bump(c) => self.add_bump(c),
ConstraintToken::ProgramSeed(c) => self.add_program_seed(c),
ConstraintToken::Realloc(c) => self.add_realloc(c),
ConstraintToken::ReallocPayer(c) => self.add_realloc_payer(c),
ConstraintToken::ReallocZero(c) => self.add_realloc_zero(c),
}
}

Expand Down Expand Up @@ -780,6 +854,56 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
Ok(())
}

fn add_realloc(&mut self, c: Context<ConstraintRealloc>) -> ParseResult<()> {
if !matches!(self.f_ty, Some(Ty::Account(_)))
&& !matches!(self.f_ty, Some(Ty::AccountLoader(_)))
{
return Err(ParseError::new(
c.span(),
"realloc must be on an Account or AccountLoader",
));
}
if self.mutable.is_none() {
return Err(ParseError::new(
c.span(),
"mut must be provided before realloc",
));
}
if self.realloc.is_some() {
return Err(ParseError::new(c.span(), "realloc already provided"));
}
self.realloc.replace(c);
Ok(())
}

fn add_realloc_payer(&mut self, c: Context<ConstraintReallocPayer>) -> ParseResult<()> {
if self.realloc.is_none() {
return Err(ParseError::new(
c.span(),
"realloc must be provided before realloc::payer",
));
}
if self.realloc_payer.is_some() {
return Err(ParseError::new(c.span(), "realloc::payer already provided"));
}
self.realloc_payer.replace(c);
Ok(())
}

fn add_realloc_zero(&mut self, c: Context<ConstraintReallocZero>) -> ParseResult<()> {
if self.realloc.is_none() {
return Err(ParseError::new(
c.span(),
"realloc must be provided before realloc::zero",
));
}
if self.realloc_zero.is_some() {
return Err(ParseError::new(c.span(), "realloc::zero already provided"));
}
self.realloc_zero.replace(c);
Ok(())
}

fn add_close(&mut self, c: Context<ConstraintClose>) -> ParseResult<()> {
if !matches!(self.f_ty, Some(Ty::ProgramAccount(_)))
&& !matches!(self.f_ty, Some(Ty::Account(_)))
Expand Down
Loading