Skip to content

Commit

Permalink
Idl: Limit account size to 60kb, allow closing idl accounts (coral-xy…
Browse files Browse the repository at this point in the history
  • Loading branch information
ckamm authored and dovahcrow committed Jan 13, 2023
1 parent c589875 commit 740dbd9
Show file tree
Hide file tree
Showing 7 changed files with 280 additions and 14 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
95 changes: 88 additions & 7 deletions cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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),
Expand All @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions lang/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
50 changes: 48 additions & 2 deletions lang/src/idl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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> {
Expand All @@ -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.
//
Expand All @@ -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 {
Expand All @@ -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..])
}
}
99 changes: 95 additions & 4 deletions lang/syn/src/codegen/program/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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][..]];
Expand Down Expand Up @@ -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,
Expand All @@ -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(())
}

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

0 comments on commit 740dbd9

Please sign in to comment.