lang: add `realloc` constraint group (#1943)

This commit is contained in:
Matthew Callens 2022-06-04 20:25:28 -04:00 committed by GitHub
parent 3b0d73ed7a
commit 7fe39c61ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 492 additions and 3 deletions

View File

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

View File

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

View File

@ -44,6 +44,7 @@ use syn::parse_macro_input;
///
/// - [Normal Constraints](#normal-constraints)
/// - [SPL Constraints](#spl-constraints)
///
/// # Normal Constraints
/// <table>
/// <thead>
@ -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>
///

View File

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

View File

@ -635,6 +635,7 @@ pub struct ConstraintGroup {
associated_token: Option<ConstraintAssociatedToken>,
token_account: Option<ConstraintTokenAccountGroup>,
mint: Option<ConstraintTokenMintGroup>,
realloc: Option<ConstraintReallocGroup>,
}
impl ConstraintGroup {
@ -678,6 +679,7 @@ pub enum Constraint {
Address(ConstraintAddress),
TokenAccount(ConstraintTokenAccountGroup),
Mint(ConstraintTokenMintGroup),
Realloc(ConstraintReallocGroup),
}
// Constraint token is a single keyword in a `#[account(<TOKEN>)]` attribute.
@ -709,6 +711,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 {
@ -733,6 +738,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>,

View File

@ -196,6 +196,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
@ -313,6 +354,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> {
@ -344,6 +388,9 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
mint_decimals: None,
bump: None,
program_seed: None,
realloc: None,
realloc_payer: None,
realloc_zero: None,
}
}
@ -439,6 +486,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 {
@ -526,6 +589,9 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
mint_decimals,
bump,
program_seed,
realloc,
realloc_payer,
realloc_zero,
} = self;
// Converts Option<Context<T>> -> Option<T>.
@ -644,6 +710,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),
@ -690,6 +761,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),
}
}
@ -757,6 +831,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(_)))

View File

@ -55,7 +55,7 @@ fn constraints_cross_checks(fields: &[AccountField]) -> ParseResult<()> {
init_fields[0].ident.span(),
"the init constraint requires \
the system_program field to exist in the account \
validation struct. Use the program type to add \
validation struct. Use the Program type to add \
the system_program field to your validation struct.",
));
}
@ -70,7 +70,7 @@ fn constraints_cross_checks(fields: &[AccountField]) -> ParseResult<()> {
init_fields[0].ident.span(),
"the init constraint requires \
the token_program field to exist in the account \
validation struct. Use the program type to add \
validation struct. Use the Program type to add \
the token_program field to your validation struct.",
));
}
@ -86,7 +86,7 @@ fn constraints_cross_checks(fields: &[AccountField]) -> ParseResult<()> {
init_fields[0].ident.span(),
"the init constraint requires \
the associated_token_program field to exist in the account \
validation struct. Use the program type to add \
validation struct. Use the Program type to add \
the associated_token_program field to your validation struct.",
));
}
@ -141,6 +141,61 @@ fn constraints_cross_checks(fields: &[AccountField]) -> ParseResult<()> {
}
}
}
// REALLOC
let realloc_fields: Vec<&Field> = fields
.iter()
.filter_map(|f| match f {
AccountField::Field(field) if field.constraints.realloc.is_some() => Some(field),
_ => None,
})
.collect();
if !realloc_fields.is_empty() {
// realloc needs system program.
if fields.iter().all(|f| f.ident() != "system_program") {
return Err(ParseError::new(
realloc_fields[0].ident.span(),
"the realloc constraint requires \
the system_program field to exist in the account \
validation struct. Use the Program type to add \
the system_program field to your validation struct.",
));
}
for field in realloc_fields {
// Get allocator for realloc-ed account
let associated_payer_name = match field.constraints.realloc.clone().unwrap().payer {
// composite allocator, check not supported
Expr::Field(_) => continue,
field_name => field_name.to_token_stream().to_string(),
};
// Check allocator is mutable
let associated_payer_field = fields.iter().find_map(|f| match f {
AccountField::Field(field) if *f.ident() == associated_payer_name => Some(field),
_ => None,
});
match associated_payer_field {
Some(associated_payer_field) => {
if !associated_payer_field.constraints.is_mutable() {
return Err(ParseError::new(
field.ident.span(),
"the realloc::payer specified for an realloc constraint must be mutable.",
));
}
}
_ => {
return Err(ParseError::new(
field.ident.span(),
"the realloc::payer specified does not exist.",
));
}
}
}
}
Ok(())
}

View File

@ -24,6 +24,7 @@
"permissioned-markets",
"pda-derivation",
"pyth",
"realloc",
"spl/token-proxy",
"swap",
"system-accounts",

15
tests/realloc/Anchor.toml Normal file
View File

@ -0,0 +1,15 @@
[features]
seeds = false
[programs.localnet]
realloc = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"
[registry]
url = "https://anchor.projectserum.com"
[provider]
cluster = "localnet"
wallet = "~/.config/solana/id.json"
[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

4
tests/realloc/Cargo.toml Normal file
View File

@ -0,0 +1,4 @@
[workspace]
members = [
"programs/*"
]

View File

@ -0,0 +1,20 @@
{
"name": "realloc",
"version": "0.24.2",
"license": "(MIT OR Apache-2.0)",
"homepage": "https://github.com/project-serum/anchor#readme",
"bugs": {
"url": "https://github.com/project-serum/anchor/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/project-serum/anchor.git"
},
"engines": {
"node": ">=11"
},
"scripts": {
"test": "anchor test"
}
}

View File

@ -0,0 +1,22 @@
[package]
name = "realloc"
version = "0.1.0"
description = "Created with Anchor"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
name = "realloc"
[features]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
cpi = ["no-entrypoint"]
default = []
[profile.release]
overflow-checks = true
[dependencies]
anchor-lang = { path = "../../../../lang" }

View File

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

View File

@ -0,0 +1,70 @@
use anchor_lang::prelude::*;
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
#[program]
pub mod realloc {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.sample.data = vec![0];
ctx.accounts.sample.bump = *ctx.bumps.get("sample").unwrap();
Ok(())
}
pub fn realloc(ctx: Context<Realloc>, len: u8) -> Result<()> {
ctx.accounts
.sample
.data
.resize_with(len as usize, Default::default);
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub authority: Signer<'info>,
#[account(
init,
payer = authority,
seeds = [b"sample"],
bump,
space = Sample::space(1),
)]
pub sample: Account<'info, Sample>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
#[instruction(len: u8)]
pub struct Realloc<'info> {
#[account(mut)]
pub authority: Signer<'info>,
#[account(
mut,
seeds = [b"sample"],
bump = sample.bump,
realloc = Sample::space(len as usize),
realloc::payer = authority,
realloc::zero = false,
)]
pub sample: Account<'info, Sample>,
pub system_program: Program<'info, System>,
}
#[account]
pub struct Sample {
pub data: Vec<u8>,
pub bump: u8,
}
impl Sample {
pub fn space(len: usize) -> usize {
8 + (4 + len) + 1
}
}

View File

@ -0,0 +1,52 @@
import * as anchor from "@project-serum/anchor";
import { Program } from "@project-serum/anchor";
import { assert } from "chai";
import { Realloc } from "../target/types/realloc";
describe("realloc", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.Realloc as Program<Realloc>;
const authority = (program.provider as any).wallet
.payer as anchor.web3.Keypair;
let sample: anchor.web3.PublicKey;
before(async () => {
[sample] = await anchor.web3.PublicKey.findProgramAddress(
[Buffer.from("sample")],
program.programId
);
});
it("Is initialized!", async () => {
await program.methods
.initialize()
.accounts({ authority: authority.publicKey, sample })
.rpc();
const s = await program.account.sample.fetch(sample);
assert.lengthOf(s.data, 1);
});
it("realloc additive", async () => {
await program.methods
.realloc(5)
.accounts({ authority: authority.publicKey, sample })
.rpc();
const s = await program.account.sample.fetch(sample);
assert.lengthOf(s.data, 5);
});
it("realloc substractive", async () => {
await program.methods
.realloc(1)
.accounts({ authority: authority.publicKey, sample })
.rpc();
const s = await program.account.sample.fetch(sample);
assert.lengthOf(s.data, 1);
});
});

View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"types": ["mocha", "chai"],
"typeRoots": ["./node_modules/@types"],
"lib": ["es2015"],
"module": "commonjs",
"target": "es6",
"esModuleInterop": true
}
}