diff --git a/CHANGELOG.md b/CHANGELOG.md index caa4baaf1f..a41229552d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/cli/src/lib.rs b/cli/src/lib.rs index cfe55d6c1e..d5d2aad848 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -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 { 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, diff --git a/lang/src/error.rs b/lang/src/error.rs index 6d88596db1..33d382cf5c 100644 --- a/lang/src/error.rs +++ b/lang/src/error.rs @@ -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 diff --git a/lang/src/idl.rs b/lang/src/idl.rs index 8bbd89316a..bcb3d521a4 100644 --- a/lang/src/idl.rs +++ b/lang/src/idl.rs @@ -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, + // 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..]) + } +} diff --git a/lang/syn/src/codegen/program/handlers.rs b/lang/syn/src/codegen/program/handlers.rs index 0172aab507..990a617611 100644 --- a/lang/syn/src/codegen/program/handlers.rs +++ b/lang/syn/src/codegen/program/handlers.rs @@ -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::::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::::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::::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(()) } } diff --git a/tests/anchor-cli-idl/test.sh b/tests/anchor-cli-idl/test.sh index 7a4c215bf0..3a351dcf5b 100755 --- a/tests/anchor-cli-idl/test.sh +++ b/tests/anchor-cli-idl/test.sh @@ -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 \ No newline at end of file diff --git a/tests/anchor-cli-idl/tests/idl.ts b/tests/anchor-cli-idl/tests/idl.ts index 245a04084b..b50e6d506e 100644 --- a/tests/anchor-cli-idl/tests/idl.ts +++ b/tests/anchor-cli-idl/tests/idl.ts @@ -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); + }); });