Idl: Limit account size to 60kb, allow closing idl accounts (#2329)

This commit is contained in:
Christian Kamm 2023-01-05 14:25:21 +01:00 committed by GitHub
parent 11642929ac
commit 27bb695685
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 280 additions and 14 deletions

View File

@ -13,6 +13,8 @@ The minor version will be incremented upon a breaking change and the patch versi
### Features
- cli: Add `env` option to verifiable builds ([#2325](https://github.com/coral-xyz/anchor/pull/2325)).
- cli: Add `idl close` command to close a program's IDL account ([#2329](https://github.com/coral-xyz/anchor/pull/2329)).
- cli: `idl init` now supports very large IDL files ([#2329](https://github.com/coral-xyz/anchor/pull/2329)).
- spl: Add `transfer_checked` function ([#2353](https://github.com/coral-xyz/anchor/pull/2353)).
### Fixes

View File

@ -308,6 +308,9 @@ pub enum IdlCommand {
#[clap(short, long)]
filepath: String,
},
Close {
program_id: Pubkey,
},
/// Writes an IDL into a buffer account. This can be used with SetBuffer
/// to perform an upgrade.
WriteBuffer {
@ -1565,7 +1568,9 @@ fn fetch_idl(cfg_override: &ConfigOverride, idl_addr: Pubkey) -> Result<Idl> {
let mut d: &[u8] = &account.data[8..];
let idl_account: IdlAccount = AnchorDeserialize::deserialize(&mut d)?;
let mut z = ZlibDecoder::new(&idl_account.data[..]);
let compressed_len: usize = idl_account.data_len.try_into().unwrap();
let compressed_bytes = &account.data[44..44 + compressed_len];
let mut z = ZlibDecoder::new(compressed_bytes);
let mut s = Vec::new();
z.read_to_end(&mut s)?;
serde_json::from_slice(&s[..]).map_err(Into::into)
@ -1596,6 +1601,7 @@ fn idl(cfg_override: &ConfigOverride, subcmd: IdlCommand) -> Result<()> {
program_id,
filepath,
} => idl_init(cfg_override, program_id, filepath),
IdlCommand::Close { program_id } => idl_close(cfg_override, program_id),
IdlCommand::WriteBuffer {
program_id,
filepath,
@ -1638,6 +1644,17 @@ fn idl_init(cfg_override: &ConfigOverride, program_id: Pubkey, idl_filepath: Str
})
}
fn idl_close(cfg_override: &ConfigOverride, program_id: Pubkey) -> Result<()> {
with_workspace(cfg_override, |cfg| {
let idl_address = IdlAccount::address(&program_id);
idl_close_account(cfg, &program_id, idl_address)?;
println!("Idl account closed: {:?}", idl_address);
Ok(())
})
}
fn idl_write_buffer(
cfg_override: &ConfigOverride,
program_id: Pubkey,
@ -1811,6 +1828,44 @@ fn idl_erase_authority(cfg_override: &ConfigOverride, program_id: Pubkey) -> Res
Ok(())
}
fn idl_close_account(cfg: &Config, program_id: &Pubkey, idl_address: Pubkey) -> Result<()> {
let keypair = solana_sdk::signature::read_keypair_file(&cfg.provider.wallet.to_string())
.map_err(|_| anyhow!("Unable to read keypair file"))?;
let url = cluster_url(cfg, &cfg.test_validator);
let client = RpcClient::new(url);
// Instruction accounts.
let accounts = vec![
AccountMeta::new(idl_address, false),
AccountMeta::new_readonly(keypair.pubkey(), true),
AccountMeta::new(keypair.pubkey(), true),
];
// Instruction.
let ix = Instruction {
program_id: *program_id,
accounts,
data: { serialize_idl_ix(anchor_lang::idl::IdlInstruction::Close {})? },
};
// Send transaction.
let latest_hash = client.get_latest_blockhash()?;
let tx = Transaction::new_signed_with_payer(
&[ix],
Some(&keypair.pubkey()),
&[&keypair],
latest_hash,
);
client.send_and_confirm_transaction_with_spinner_and_config(
&tx,
CommitmentConfig::confirmed(),
RpcSendTransactionConfig {
skip_preflight: true,
..RpcSendTransactionConfig::default()
},
)?;
Ok(())
}
// Write the idl to the account buffer, chopping up the IDL into pieces
// and sending multiple transactions in the event the IDL doesn't fit into
// a single transaction.
@ -2834,9 +2889,22 @@ fn create_idl_account(
// Run `Create instruction.
{
let data = serialize_idl_ix(anchor_lang::idl::IdlInstruction::Create {
data_len: (idl_data.len() as u64) * 2, // Double for future growth.
})?;
let pda_max_growth = 60_000;
let idl_header_size = 44;
let idl_data_len = idl_data.len() as u64;
// We're only going to support up to 6 instructions in one transaction
// because will anyone really have a >60kb IDL?
if idl_data_len > pda_max_growth {
return Err(anyhow!(
"Your IDL is over 60kb and this isn't supported right now"
));
}
// Double for future growth.
let data_len = (idl_data_len * 2).min(pda_max_growth - idl_header_size);
let num_additional_instructions = data_len / 10000;
let mut instructions = Vec::new();
let data = serialize_idl_ix(anchor_lang::idl::IdlInstruction::Create { data_len })?;
let program_signer = Pubkey::find_program_address(&[], program_id).0;
let accounts = vec![
AccountMeta::new_readonly(keypair.pubkey(), true),
@ -2846,14 +2914,27 @@ fn create_idl_account(
AccountMeta::new_readonly(*program_id, false),
AccountMeta::new_readonly(solana_program::sysvar::rent::ID, false),
];
let ix = Instruction {
instructions.push(Instruction {
program_id: *program_id,
accounts,
data,
};
});
for _ in 0..num_additional_instructions {
let data = serialize_idl_ix(anchor_lang::idl::IdlInstruction::Resize { data_len })?;
instructions.push(Instruction {
program_id: *program_id,
accounts: vec![
AccountMeta::new(idl_address, false),
AccountMeta::new_readonly(keypair.pubkey(), true),
AccountMeta::new_readonly(solana_program::system_program::ID, false),
],
data,
});
}
let latest_hash = client.get_latest_blockhash()?;
let tx = Transaction::new_signed_with_payer(
&[ix],
&instructions,
Some(&keypair.pubkey()),
&[&keypair],
latest_hash,

View File

@ -40,6 +40,9 @@ pub enum ErrorCode {
/// 1001 - Invalid program given to the IDL instruction
#[msg("Invalid program given to the IDL instruction")]
IdlInstructionInvalidProgram,
/// 1002 - IDL Account must be empty in order to resize
#[msg("IDL account must be empty in order to resize, try closing first")]
IdlAccountNotEmpty,
// Constraints
/// 2000 - A mut constraint was violated

View File

@ -45,6 +45,9 @@ pub enum IdlInstruction {
SetBuffer,
// Sets a new authority on the IdlAccount.
SetAuthority { new_authority: Pubkey },
Close,
// Increases account size for accounts that need over 10kb.
Resize { data_len: u64 },
}
// Accounts for the Create instruction.
@ -60,6 +63,17 @@ pub struct IdlAccounts<'info> {
pub authority: Signer<'info>,
}
// Accounts for resize account instruction
#[derive(Accounts)]
pub struct IdlResizeAccount<'info> {
#[account(mut, has_one = authority)]
#[allow(deprecated)]
pub idl: ProgramAccount<'info, IdlAccount>,
#[account(mut, constraint = authority.key != &ERASED_AUTHORITY)]
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}
// Accounts for creating an idl buffer.
#[derive(Accounts)]
pub struct IdlCreateBuffer<'info> {
@ -85,6 +99,18 @@ pub struct IdlSetBuffer<'info> {
pub authority: Signer<'info>,
}
// Accounts for closing the canonical Idl buffer.
#[derive(Accounts)]
pub struct IdlCloseAccount<'info> {
#[account(mut, has_one = authority, close = sol_destination)]
#[allow(deprecated)]
pub account: ProgramAccount<'info, IdlAccount>,
#[account(constraint = authority.key != &ERASED_AUTHORITY)]
pub authority: Signer<'info>,
#[account(mut)]
pub sol_destination: AccountInfo<'info>,
}
// The account holding a program's IDL. This is stored on chain so that clients
// can fetch it and generate a client with nothing but a program's ID.
//
@ -95,8 +121,9 @@ pub struct IdlSetBuffer<'info> {
pub struct IdlAccount {
// Address that can modify the IDL.
pub authority: Pubkey,
// Compressed idl bytes.
pub data: Vec<u8>,
// Length of compressed idl bytes.
pub data_len: u32,
// Followed by compressed idl bytes.
}
impl IdlAccount {
@ -109,3 +136,22 @@ impl IdlAccount {
"anchor:idl"
}
}
use std::cell::{Ref, RefMut};
pub trait IdlTrailingData<'info> {
fn trailing_data(self) -> Ref<'info, [u8]>;
fn trailing_data_mut(self) -> RefMut<'info, [u8]>;
}
#[allow(deprecated)]
impl<'a, 'info: 'a> IdlTrailingData<'a> for &'a ProgramAccount<'info, IdlAccount> {
fn trailing_data(self) -> Ref<'a, [u8]> {
let info = self.as_ref();
Ref::map(info.try_borrow_data().unwrap(), |d| &d[44..])
}
fn trailing_data_mut(self) -> RefMut<'a, [u8]> {
let info = self.as_ref();
RefMut::map(info.try_borrow_mut_data().unwrap(), |d| &mut d[44..])
}
}

View File

@ -32,6 +32,22 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream {
__idl_create_account(program_id, &mut accounts, data_len)?;
accounts.exit(program_id)?;
},
anchor_lang::idl::IdlInstruction::Resize { data_len } => {
let mut bumps = std::collections::BTreeMap::new();
let mut reallocs = std::collections::BTreeSet::new();
let mut accounts =
anchor_lang::idl::IdlResizeAccount::try_accounts(program_id, &mut accounts, &[], &mut bumps, &mut reallocs)?;
__idl_resize_account(program_id, &mut accounts, data_len)?;
accounts.exit(program_id)?;
},
anchor_lang::idl::IdlInstruction::Close => {
let mut bumps = std::collections::BTreeMap::new();
let mut reallocs = std::collections::BTreeSet::new();
let mut accounts =
anchor_lang::idl::IdlCloseAccount::try_accounts(program_id, &mut accounts, &[], &mut bumps, &mut reallocs)?;
__idl_close_account(program_id, &mut accounts)?;
accounts.exit(program_id)?;
},
anchor_lang::idl::IdlInstruction::CreateBuffer => {
let mut bumps = std::collections::BTreeMap::new();
let mut reallocs = std::collections::BTreeSet::new();
@ -95,7 +111,7 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream {
let owner = accounts.program.key;
let to = Pubkey::create_with_seed(&base, seed, owner).unwrap();
// Space: account discriminator || authority pubkey || vec len || vec data
let space = 8 + 32 + 4 + data_len as usize;
let space = std::cmp::min(8 + 32 + 4 + data_len as usize, 10_000);
let rent = Rent::get()?;
let lamports = rent.minimum_balance(space);
let seeds = &[&[nonce][..]];
@ -140,6 +156,64 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream {
Ok(())
}
#[inline(never)]
pub fn __idl_resize_account(
program_id: &Pubkey,
accounts: &mut anchor_lang::idl::IdlResizeAccount,
data_len: u64,
) -> anchor_lang::Result<()> {
#[cfg(not(feature = "no-log-ix-name"))]
anchor_lang::prelude::msg!("Instruction: IdlResizeAccount");
let data_len: usize = data_len as usize;
// We're not going to support increasing the size of accounts that already contain data
// because that would be messy and possibly dangerous
if accounts.idl.data_len != 0 {
return Err(anchor_lang::error::ErrorCode::IdlAccountNotEmpty.into());
}
let new_account_space = accounts.idl.to_account_info().data_len().checked_add(std::cmp::min(
data_len
.checked_sub(accounts.idl.to_account_info().data_len())
.expect("data_len should always be >= the current account space"),
10_000,
))
.unwrap();
if new_account_space > accounts.idl.to_account_info().data_len() {
let sysvar_rent = Rent::get()?;
let new_rent_minimum = sysvar_rent.minimum_balance(new_account_space);
anchor_lang::system_program::transfer(
anchor_lang::context::CpiContext::new(
accounts.system_program.to_account_info(),
anchor_lang::system_program::Transfer {
from: accounts.authority.to_account_info(),
to: accounts.idl.to_account_info().clone(),
},
),
new_rent_minimum
.checked_sub(accounts.idl.to_account_info().lamports())
.unwrap(),
)?;
accounts.idl.to_account_info().realloc(new_account_space, false)?;
}
Ok(())
}
#[inline(never)]
pub fn __idl_close_account(
program_id: &Pubkey,
accounts: &mut anchor_lang::idl::IdlCloseAccount,
) -> anchor_lang::Result<()> {
#[cfg(not(feature = "no-log-ix-name"))]
anchor_lang::prelude::msg!("Instruction: IdlCloseAccount");
Ok(())
}
#[inline(never)]
pub fn __idl_create_buffer(
program_id: &Pubkey,
@ -162,8 +236,16 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream {
#[cfg(not(feature = "no-log-ix-name"))]
anchor_lang::prelude::msg!("Instruction: IdlWrite");
let mut idl = &mut accounts.idl;
idl.data.extend(idl_data);
let prev_len: usize = ::std::convert::TryInto::<usize>::try_into(accounts.idl.data_len).unwrap();
let new_len: usize = prev_len + idl_data.len();
accounts.idl.data_len = accounts.idl.data_len.checked_add(::std::convert::TryInto::<u32>::try_into(idl_data.len()).unwrap()).unwrap();
use anchor_lang::idl::IdlTrailingData;
let mut idl_bytes = accounts.idl.trailing_data_mut();
let idl_expansion = &mut idl_bytes[prev_len..new_len];
require_eq!(idl_expansion.len(), idl_data.len());
idl_expansion.copy_from_slice(&idl_data[..]);
Ok(())
}
@ -188,7 +270,16 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream {
#[cfg(not(feature = "no-log-ix-name"))]
anchor_lang::prelude::msg!("Instruction: IdlSetBuffer");
accounts.idl.data = accounts.buffer.data.clone();
accounts.idl.data_len = accounts.buffer.data_len;
use anchor_lang::idl::IdlTrailingData;
let buffer_len = ::std::convert::TryInto::<usize>::try_into(accounts.buffer.data_len).unwrap();
let mut target = accounts.idl.trailing_data_mut();
let source = &accounts.buffer.trailing_data()[..buffer_len];
require_gte!(target.len(), buffer_len);
target[..buffer_len].copy_from_slice(source);
// zero the remainder of target?
Ok(())
}
}

View File

@ -3,6 +3,27 @@
# Write a keypair for program deploy
mkdir -p target/deploy
cp keypairs/idl_commands_one-keypair.json target/deploy
# Generate over 20kb bytes of random data (base64 encoded), surround it with quotes, and store it in a variable
RANDOM_DATA=$(openssl rand -base64 $((10*1680)) | sed 's/.*/"&",/')
# Create the JSON object with the "docs" field containing random data
echo '{
"version": "0.1.0",
"name": "idl_commands_one",
"instructions": [
{
"name": "initialize",
"docs" : [
'"$RANDOM_DATA"'
"trailing comma begone"
],
"accounts": [],
"args": []
}
]
}' > testLargeIdl.json
echo "Building programs"
@ -23,4 +44,4 @@ echo "Running tests"
anchor test --skip-deploy --skip-local-validator
trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT
trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT

View File

@ -4,6 +4,7 @@ import { IdlCommandsOne } from "../target/types/idl_commands_one";
import { IdlCommandsTwo } from "../target/types/idl_commands_two";
import { assert } from "chai";
import { execSync } from "child_process";
import * as fs from "fs";
describe("Test CLI IDL commands", () => {
// Configure the client to use the local cluster.
@ -62,4 +63,25 @@ describe("Test CLI IDL commands", () => {
assert.equal(authority, provider.wallet.publicKey.toString());
});
it("Can close IDL account", async () => {
execSync(`anchor idl close ${programOne.programId}`, { stdio: "inherit" });
const idl = await anchor.Program.fetchIdl(programOne.programId, provider);
assert.isNull(idl);
});
it("Can initialize super massive IDL account", async () => {
execSync(
`anchor idl init --filepath testLargeIdl.json ${programOne.programId}`,
{ stdio: "inherit" }
);
const idlActual = await anchor.Program.fetchIdl(
programOne.programId,
provider
);
const idlExpected = JSON.parse(
fs.readFileSync("testLargeIdl.json", "utf8")
);
assert.deepEqual(idlActual, idlExpected);
});
});