diff --git a/cli-output/src/cli_output.rs b/cli-output/src/cli_output.rs index c90fc820f..4c60e598f 100644 --- a/cli-output/src/cli_output.rs +++ b/cli-output/src/cli_output.rs @@ -1392,7 +1392,7 @@ impl fmt::Display for CliAccountBalances { writeln!( f, "{}", - style(format!("{:<44} {}", "Address", "Balance",)).bold() + style(format!("{:<44} {}", "Address", "Balance")).bold() )?; for account in &self.accounts { writeln!( @@ -1684,6 +1684,8 @@ pub struct CliUpgradeableBuffer { pub address: String, pub authority: String, pub data_len: usize, + pub lamports: u64, + pub use_lamports_unit: bool, } impl QuietDisplay for CliUpgradeableBuffer {} impl VerboseDisplay for CliUpgradeableBuffer {} @@ -1692,11 +1694,53 @@ impl fmt::Display for CliUpgradeableBuffer { writeln!(f)?; writeln_name_value(f, "Buffer Address:", &self.address)?; writeln_name_value(f, "Authority:", &self.authority)?; + writeln_name_value( + f, + "Balance:", + &build_balance_message(self.lamports, self.use_lamports_unit, true), + )?; writeln_name_value( f, "Data Length:", &format!("{:?} ({:#x?}) bytes", self.data_len, self.data_len), )?; + + Ok(()) + } +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CliUpgradeableBuffers { + pub buffers: Vec, + pub use_lamports_unit: bool, +} +impl QuietDisplay for CliUpgradeableBuffers {} +impl VerboseDisplay for CliUpgradeableBuffers {} +impl fmt::Display for CliUpgradeableBuffers { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f)?; + writeln!( + f, + "{}", + style(format!( + "{:<44} | {:<44} | {}", + "Buffer Address", "Authority", "Balance" + )) + .bold() + )?; + for buffer in self.buffers.iter() { + writeln!( + f, + "{}", + &format!( + "{:<44} | {:<44} | {}", + buffer.address, + buffer.authority, + build_balance_message(buffer.lamports, self.use_lamports_unit, true) + ) + )?; + } Ok(()) } } diff --git a/cli/src/program.rs b/cli/src/program.rs index 6d6874283..8ecca1c7e 100644 --- a/cli/src/program.rs +++ b/cli/src/program.rs @@ -10,15 +10,22 @@ use bincode::serialize; use bip39::{Language, Mnemonic, MnemonicType, Seed}; use clap::{App, AppSettings, Arg, ArgMatches, SubCommand}; use log::*; +use solana_account_decoder::{UiAccountEncoding, UiDataSliceConfig}; use solana_bpf_loader_program::{bpf_verifier, BpfError, ThisInstructionMeter}; use solana_clap_utils::{self, input_parsers::*, input_validators::*, keypair::*}; use solana_cli_output::{ display::new_spinner_progress_bar, CliProgram, CliProgramAccountType, CliProgramAuthority, - CliProgramBuffer, CliProgramId, CliUpgradeableBuffer, CliUpgradeableProgram, + CliProgramBuffer, CliProgramId, CliUpgradeableBuffer, CliUpgradeableBuffers, + CliUpgradeableProgram, }; use solana_client::{ - rpc_client::RpcClient, rpc_config::RpcSendTransactionConfig, - rpc_request::MAX_GET_SIGNATURE_STATUSES_QUERY_ITEMS, rpc_response::RpcLeaderSchedule, + client_error::ClientErrorKind, + rpc_client::RpcClient, + rpc_config::RpcSendTransactionConfig, + rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig}, + rpc_filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType}, + rpc_request::MAX_GET_SIGNATURE_STATUSES_QUERY_ITEMS, + rpc_response::RpcLeaderSchedule, }; use solana_rbpf::vm::{Config, Executable}; use solana_remote_wallet::remote_wallet::RemoteWalletManager; @@ -30,6 +37,7 @@ use solana_sdk::{ clock::Slot, commitment_config::CommitmentConfig, instruction::Instruction, + instruction::InstructionError, loader_instruction, message::Message, native_token::Sol, @@ -39,6 +47,7 @@ use solana_sdk::{ system_instruction::{self, SystemError}, system_program, transaction::Transaction, + transaction::TransactionError, }; use solana_transaction_status::TransactionConfirmationStatus; use std::{ @@ -89,11 +98,20 @@ pub enum ProgramCliCommand { }, Show { account_pubkey: Option, + authority_pubkey: Pubkey, + all: bool, + use_lamports_unit: bool, }, Dump { account_pubkey: Option, output_location: String, }, + Close { + account_pubkey: Option, + recipient_pubkey: Pubkey, + authority_index: SignerIndex, + use_lamports_unit: bool, + }, } pub trait ProgramSubCommands { @@ -200,7 +218,7 @@ impl ProgramSubCommands for App<'_, '_> { ) .subcommand( SubCommand::with_name("set-buffer-authority") - .about("Set a new buffer authority") // TODO deploy with buffer and no file path? + .about("Set a new buffer authority") .arg( Arg::with_name("buffer") .index(1) @@ -266,9 +284,34 @@ impl ProgramSubCommands for App<'_, '_> { .index(1) .value_name("ACCOUNT_ADDRESS") .takes_value(true) - .required(true) .help("Address of the buffer or program to show") ) + .arg( + Arg::with_name("buffers") + .long("buffers") + .conflicts_with("account") + .required_unless("account") + .help("Show every buffer account that matches the authority") + ) + .arg( + Arg::with_name("all") + .long("all") + .conflicts_with("account") + .help("Show accounts for all authorities") + ) + .arg( + pubkey!(Arg::with_name("buffer_authority") + .long("buffer-authority") + .value_name("AUTHORITY") + .conflicts_with("all"), + "Authority [default: the default configured keypair]"), + ) + .arg( + Arg::with_name("lamports") + .long("lamports") + .takes_value(false) + .help("Display balance in lamports instead of SOL"), + ), ) .subcommand( SubCommand::with_name("dump") @@ -290,6 +333,44 @@ impl ProgramSubCommands for App<'_, '_> { .help("/path/to/program.so"), ), ) + .subcommand( + SubCommand::with_name("close") + .about("Close an acount and withdraw all lamports") + .arg( + Arg::with_name("account") + .index(1) + .value_name("BUFFER_ACCOUNT_ADDRESS") + .takes_value(true) + .help("Address of the buffer account to close"), + ) + .arg( + Arg::with_name("buffers") + .long("buffers") + .conflicts_with("account") + .required_unless("account") + .help("Close every buffer accounts that match the authority") + ) + .arg( + Arg::with_name("buffer_authority") + .long("buffer-authority") + .value_name("AUTHORITY_SIGNER") + .takes_value(true) + .validator(is_valid_signer) + .help("Authority [default: the default configured keypair]") + ) + .arg( + pubkey!(Arg::with_name("recipient_account") + .long("recipient") + .value_name("RECIPIENT_ADDRESS"), + "Address of the account to deposit the closed account's lamports [default: the default configured keypair]"), + ) + .arg( + Arg::with_name("lamports") + .long("lamports") + .takes_value(false) + .help("Display balance in lamports instead of SOL"), + ), + ) ) } } @@ -425,15 +506,8 @@ pub fn parse_program_subcommand( let (buffer_authority_signer, buffer_authority_pubkey) = signer_of(matches, "buffer_authority", wallet_manager)?; - let new_buffer_authority = if let Some(new_buffer_authority) = - pubkey_of_signer(matches, "new_buffer_authority", wallet_manager)? - { - new_buffer_authority - } else { - let (_, new_buffer_authority) = - signer_of(matches, "new_buffer_authority", wallet_manager)?; - new_buffer_authority.unwrap() - }; + let new_buffer_authority = + pubkey_of_signer(matches, "new_buffer_authority", wallet_manager)?.unwrap(); let signer_info = default_signer.generate_unique_signers( vec![ @@ -459,14 +533,8 @@ pub fn parse_program_subcommand( let program_pubkey = pubkey_of(matches, "program_id").unwrap(); let new_upgrade_authority = if matches.is_present("final") { None - } else if let Some(new_upgrade_authority) = - pubkey_of_signer(matches, "new_upgrade_authority", wallet_manager)? - { - Some(new_upgrade_authority) } else { - let (_, new_upgrade_authority) = - signer_of(matches, "new_upgrade_authority", wallet_manager)?; - new_upgrade_authority + pubkey_of_signer(matches, "new_upgrade_authority", wallet_manager)? }; let signer_info = default_signer.generate_unique_signers( @@ -487,12 +555,33 @@ pub fn parse_program_subcommand( signers: signer_info.signers, } } - ("show", Some(matches)) => CliCommandInfo { - command: CliCommand::Program(ProgramCliCommand::Show { - account_pubkey: pubkey_of(matches, "account"), - }), - signers: vec![], - }, + ("show", Some(matches)) => { + let account_pubkey = if matches.is_present("buffers") { + None + } else { + pubkey_of(matches, "account") + }; + + let authority_pubkey = if let Some(authority_pubkey) = + pubkey_of_signer(matches, "buffer_authority", wallet_manager)? + { + authority_pubkey + } else { + default_signer + .signer_from_path(matches, wallet_manager)? + .pubkey() + }; + + CliCommandInfo { + command: CliCommand::Program(ProgramCliCommand::Show { + account_pubkey, + authority_pubkey, + all: matches.is_present("all"), + use_lamports_unit: matches.is_present("lamports"), + }), + signers: vec![], + } + } ("dump", Some(matches)) => CliCommandInfo { command: CliCommand::Program(ProgramCliCommand::Dump { account_pubkey: pubkey_of(matches, "account"), @@ -500,6 +589,45 @@ pub fn parse_program_subcommand( }), signers: vec![], }, + ("close", Some(matches)) => { + let account_pubkey = if matches.is_present("buffers") { + None + } else { + pubkey_of(matches, "account") + }; + + let recipient_pubkey = if let Some(recipient_pubkey) = + pubkey_of_signer(matches, "recipient_account", wallet_manager)? + { + recipient_pubkey + } else { + default_signer + .signer_from_path(matches, wallet_manager)? + .pubkey() + }; + + let (authority_signer, authority_pubkey) = + signer_of(matches, "buffer_authority", wallet_manager)?; + + let signer_info = default_signer.generate_unique_signers( + vec![ + Some(default_signer.signer_from_path(matches, wallet_manager)?), + authority_signer, + ], + matches, + wallet_manager, + )?; + + CliCommandInfo { + command: CliCommand::Program(ProgramCliCommand::Close { + account_pubkey, + recipient_pubkey, + authority_index: signer_info.index_of(authority_pubkey).unwrap(), + use_lamports_unit: matches.is_present("lamports"), + }), + signers: signer_info.signers, + } + } _ => unreachable!(), }; Ok(response) @@ -573,13 +701,36 @@ pub fn process_program_subcommand( *upgrade_authority_index, *new_upgrade_authority, ), - ProgramCliCommand::Show { account_pubkey } => { - process_show(&rpc_client, config, *account_pubkey) - } + ProgramCliCommand::Show { + account_pubkey, + authority_pubkey, + all, + use_lamports_unit, + } => process_show( + &rpc_client, + config, + *account_pubkey, + *authority_pubkey, + *all, + *use_lamports_unit, + ), ProgramCliCommand::Dump { account_pubkey, output_location, } => process_dump(&rpc_client, config, *account_pubkey, output_location), + ProgramCliCommand::Close { + account_pubkey, + recipient_pubkey, + authority_index, + use_lamports_unit, + } => process_close( + &rpc_client, + config, + *account_pubkey, + *recipient_pubkey, + *authority_index, + *use_lamports_unit, + ), } } @@ -945,10 +1096,41 @@ fn process_set_authority( Ok(config.output_format.formatted_string(&authority)) } +fn get_buffers( + rpc_client: &RpcClient, + authority_pubkey: Option, +) -> Result, Box> { + let mut bytes = vec![1, 0, 0, 0, 1]; + let length = bytes.len() + 32; // Pubkey length + if let Some(authority_pubkey) = authority_pubkey { + bytes.extend_from_slice(authority_pubkey.as_ref()); + } + + let results = rpc_client.get_program_accounts_with_config( + &bpf_loader_upgradeable::id(), + RpcProgramAccountsConfig { + filters: Some(vec![RpcFilterType::Memcmp(Memcmp { + offset: 0, + bytes: MemcmpEncodedBytes::Binary(bs58::encode(bytes).into_string()), + encoding: None, + })]), + account_config: RpcAccountInfoConfig { + encoding: Some(UiAccountEncoding::Base64), + data_slice: Some(UiDataSliceConfig { offset: 0, length }), + ..RpcAccountInfoConfig::default() + }, + }, + )?; + Ok(results) +} + fn process_show( rpc_client: &RpcClient, config: &CliConfig, account_pubkey: Option, + authority_pubkey: Pubkey, + all: bool, + use_lamports_unit: bool, ) -> ProcessResult { if let Some(account_pubkey) = account_pubkey { if let Some(account) = rpc_client @@ -1013,6 +1195,8 @@ fn process_show( .unwrap_or_else(|| "none".to_string()), data_len: account.data.len() - UpgradeableLoaderState::buffer_data_offset()?, + lamports: account.lamports, + use_lamports_unit, })) } else { Err(format!( @@ -1028,7 +1212,30 @@ fn process_show( Err(format!("Unable to find the account {}", account_pubkey).into()) } } else { - Err("No account specified".into()) + let authority_pubkey = if all { None } else { Some(authority_pubkey) }; + let mut buffers = vec![]; + let results = get_buffers(rpc_client, authority_pubkey)?; + for (address, account) in results.iter() { + if let Ok(UpgradeableLoaderState::Buffer { authority_address }) = account.state() { + buffers.push(CliUpgradeableBuffer { + address: address.to_string(), + authority: authority_address + .map(|pubkey| pubkey.to_string()) + .unwrap_or_else(|| "none".to_string()), + data_len: 0, + lamports: account.lamports, + use_lamports_unit, + }); + } else { + return Err(format!("Error parsing account {}", address).into()); + } + } + Ok(config + .output_format + .formatted_string(&CliUpgradeableBuffers { + buffers, + use_lamports_unit, + })) } } @@ -1103,6 +1310,162 @@ fn process_dump( } } +fn close( + rpc_client: &RpcClient, + config: &CliConfig, + account_pubkey: &Pubkey, + recipient_pubkey: &Pubkey, + authority_signer: &dyn Signer, +) -> Result<(), Box> { + let (blockhash, _) = rpc_client.get_recent_blockhash()?; + + let mut tx = Transaction::new_unsigned(Message::new( + &[bpf_loader_upgradeable::close( + &account_pubkey, + &recipient_pubkey, + &authority_signer.pubkey(), + )], + Some(&config.signers[0].pubkey()), + )); + + tx.try_sign(&[config.signers[0], authority_signer], blockhash)?; + let result = rpc_client.send_and_confirm_transaction_with_spinner_and_config( + &tx, + config.commitment, + RpcSendTransactionConfig { + skip_preflight: true, + preflight_commitment: Some(config.commitment.commitment), + ..RpcSendTransactionConfig::default() + }, + ); + if let Err(err) = result { + if let ClientErrorKind::TransactionError(TransactionError::InstructionError( + _, + InstructionError::InvalidInstructionData, + )) = err.kind() + { + return Err("Closing a buffer account is not supported by the cluster".into()); + } else { + return Err(format!("Close failed: {}", err).into()); + } + } + Ok(()) +} + +fn process_close( + rpc_client: &RpcClient, + config: &CliConfig, + account_pubkey: Option, + recipient_pubkey: Pubkey, + authority_index: SignerIndex, + use_lamports_unit: bool, +) -> ProcessResult { + let authority_signer = config.signers[authority_index]; + let mut buffers = vec![]; + + if let Some(account_pubkey) = account_pubkey { + if let Some(account) = rpc_client + .get_account_with_commitment(&account_pubkey, config.commitment)? + .value + { + if let Ok(UpgradeableLoaderState::Buffer { authority_address }) = account.state() { + if authority_address != Some(authority_signer.pubkey()) { + return Err(format!( + "Buffer account authority {:?} does not match {:?}", + authority_address, + Some(authority_signer.pubkey()) + ) + .into()); + } else { + close( + rpc_client, + config, + &account_pubkey, + &recipient_pubkey, + authority_signer, + )?; + + if let Ok(UpgradeableLoaderState::Buffer { authority_address }) = + account.state() + { + buffers.push(CliUpgradeableBuffer { + address: account_pubkey.to_string(), + authority: authority_address + .map(|pubkey| pubkey.to_string()) + .unwrap_or_else(|| "none".to_string()), + data_len: 0, + lamports: account.lamports, + use_lamports_unit, + }); + } else { + return Err(format!("Error parsing account {}", account_pubkey).into()); + } + } + } else { + return Err(format!( + "{} is not an upgradeble loader buffer account", + account_pubkey + ) + .into()); + } + } else { + return Err(format!("Unable to find the account {}", account_pubkey).into()); + } + } else { + let mut bytes = vec![1, 0, 0, 0, 1]; + bytes.extend_from_slice(authority_signer.pubkey().as_ref()); + let length = bytes.len(); + + let results = rpc_client.get_program_accounts_with_config( + &bpf_loader_upgradeable::id(), + RpcProgramAccountsConfig { + filters: Some(vec![RpcFilterType::Memcmp(Memcmp { + offset: 0, + bytes: MemcmpEncodedBytes::Binary(bs58::encode(bytes).into_string()), + encoding: None, + })]), + account_config: RpcAccountInfoConfig { + encoding: Some(UiAccountEncoding::Base64), + data_slice: Some(UiDataSliceConfig { offset: 0, length }), + ..RpcAccountInfoConfig::default() + }, + }, + )?; + + for (address, account) in results.iter() { + if close( + rpc_client, + config, + &address, + &recipient_pubkey, + authority_signer, + ) + .is_ok() + { + if let Ok(UpgradeableLoaderState::Buffer { authority_address }) = account.state() { + buffers.push(CliUpgradeableBuffer { + address: address.to_string(), + authority: authority_address + .map(|address| address.to_string()) + .unwrap_or_else(|| "none".to_string()), + data_len: 0, + lamports: account.lamports, + use_lamports_unit, + }); + } else { + return Err(format!("Error parsing account {}", address).into()); + } + } + } + } + Ok(config + .output_format + .formatted_string(&CliUpgradeableBuffers { + buffers, + use_lamports_unit, + })) +} + /// Deploy using non-upgradeable loader pub fn process_deploy( rpc_client: &RpcClient, @@ -1614,16 +1977,21 @@ fn report_ephemeral_mnemonic(words: usize, mnemonic: bip39::Mnemonic) { let phrase: &str = mnemonic.phrase(); let divider = String::from_utf8(vec![b'='; phrase.len()]).unwrap(); eprintln!( - "{}\nTo resume a failed deploy, recover the ephemeral keypair file with", + "{}\nRecover the intermediate account's ephemeral keypair file with", divider ); eprintln!( - "`solana-keygen recover` and the following {}-word seed phrase,", + "`solana-keygen recover` and the following {}-word seed phrase:", words ); + eprintln!("{}\n{}\n{}", divider, phrase, divider); + eprintln!("To resume a deploy, pass the recovered keypair as"); + eprintln!("the [PROGRAM_ADDRESS_SIGNER] argument to `solana deploy` or"); + eprintln!("as the [BUFFER_SIGNER] to `solana program deploy` or `solana write-buffer'."); + eprintln!("Or to recover the account's lamports, pass it as the"); eprintln!( - "then pass it as the [BUFFER_SIGNER] argument to `solana deploy` or `solana write-buffer`\n{}\n{}\n{}", - divider, phrase, divider + "[BUFFER_ACCOUNT_ADDRESS] argument to `solana program close`.\n{}", + divider ); } @@ -2267,9 +2635,6 @@ mod tests { let authority = Keypair::new(); let authority_keypair_file = make_tmp_path("authority_keypair_file"); write_keypair_file(&authority, &authority_keypair_file).unwrap(); - let new_authority_pubkey = Keypair::new(); - let new_authority_pubkey_file = make_tmp_path("authority_keypair_file"); - write_keypair_file(&new_authority_pubkey, &new_authority_pubkey_file).unwrap(); let test_deploy = test_commands.clone().get_matches_from(vec![ "test", "program", @@ -2331,16 +2696,16 @@ mod tests { ); let buffer_pubkey = Pubkey::new_unique(); - let new_authority_pubkey = Keypair::new(); - let new_authority_pubkey_file = make_tmp_path("authority_keypair_file"); - write_keypair_file(&new_authority_pubkey, &new_authority_pubkey_file).unwrap(); + let new_authority_keypair = Keypair::new(); + let new_authority_keypair_file = make_tmp_path("authority_keypair_file"); + write_keypair_file(&new_authority_keypair, &new_authority_keypair_file).unwrap(); let test_deploy = test_commands.clone().get_matches_from(vec![ "test", "program", "set-buffer-authority", &buffer_pubkey.to_string(), "--new-buffer-authority", - &new_authority_pubkey_file, + &new_authority_keypair_file, ]); assert_eq!( parse_command(&test_deploy, &default_signer, &mut None).unwrap(), @@ -2348,13 +2713,223 @@ mod tests { command: CliCommand::Program(ProgramCliCommand::SetBufferAuthority { buffer_pubkey, buffer_authority_index: Some(0), - new_buffer_authority: new_authority_pubkey.pubkey(), + new_buffer_authority: new_authority_keypair.pubkey(), }), signers: vec![read_keypair_file(&keypair_file).unwrap().into()], } ); } + #[test] + #[allow(clippy::cognitive_complexity)] + fn test_cli_parse_show() { + let test_commands = app("test", "desc", "version"); + + let default_keypair = Keypair::new(); + let keypair_file = make_tmp_path("keypair_file"); + write_keypair_file(&default_keypair, &keypair_file).unwrap(); + let default_signer = DefaultSigner { + path: keypair_file, + arg_name: "".to_string(), + }; + + // defaults + let buffer_pubkey = Pubkey::new_unique(); + let authority_keypair = Keypair::new(); + let authority_keypair_file = make_tmp_path("authority_keypair_file"); + write_keypair_file(&authority_keypair, &authority_keypair_file).unwrap(); + + let test_command = test_commands.clone().get_matches_from(vec![ + "test", + "program", + "show", + &buffer_pubkey.to_string(), + ]); + assert_eq!( + parse_command(&test_command, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::Program(ProgramCliCommand::Show { + account_pubkey: Some(buffer_pubkey), + authority_pubkey: default_keypair.pubkey(), + all: false, + use_lamports_unit: false, + }), + signers: vec![], + } + ); + + let test_command = test_commands.clone().get_matches_from(vec![ + "test", + "program", + "show", + "--buffers", + "--all", + "--lamports", + ]); + assert_eq!( + parse_command(&test_command, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::Program(ProgramCliCommand::Show { + account_pubkey: None, + authority_pubkey: default_keypair.pubkey(), + all: true, + use_lamports_unit: true, + }), + signers: vec![], + } + ); + + let test_command = test_commands.clone().get_matches_from(vec![ + "test", + "program", + "show", + "--buffers", + "--buffer-authority", + &authority_keypair.pubkey().to_string(), + ]); + assert_eq!( + parse_command(&test_command, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::Program(ProgramCliCommand::Show { + account_pubkey: None, + authority_pubkey: authority_keypair.pubkey(), + all: false, + use_lamports_unit: false, + }), + signers: vec![], + } + ); + + let test_command = test_commands.clone().get_matches_from(vec![ + "test", + "program", + "show", + "--buffers", + "--buffer-authority", + &authority_keypair_file, + ]); + assert_eq!( + parse_command(&test_command, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::Program(ProgramCliCommand::Show { + account_pubkey: None, + authority_pubkey: authority_keypair.pubkey(), + all: false, + use_lamports_unit: false, + }), + signers: vec![], + } + ); + } + + #[test] + #[allow(clippy::cognitive_complexity)] + fn test_cli_parse_close() { + let test_commands = app("test", "desc", "version"); + + let default_keypair = Keypair::new(); + let keypair_file = make_tmp_path("keypair_file"); + write_keypair_file(&default_keypair, &keypair_file).unwrap(); + let default_signer = DefaultSigner { + path: keypair_file.clone(), + arg_name: "".to_string(), + }; + + // defaults + let buffer_pubkey = Pubkey::new_unique(); + let recipient_pubkey = Pubkey::new_unique(); + let authority_keypair = Keypair::new(); + let authority_keypair_file = make_tmp_path("authority_keypair_file"); + + let test_command = test_commands.clone().get_matches_from(vec![ + "test", + "program", + "close", + &buffer_pubkey.to_string(), + ]); + assert_eq!( + parse_command(&test_command, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::Program(ProgramCliCommand::Close { + account_pubkey: Some(buffer_pubkey), + recipient_pubkey: default_keypair.pubkey(), + authority_index: 0, + use_lamports_unit: false, + }), + signers: vec![read_keypair_file(&keypair_file).unwrap().into()], + } + ); + + // with authority + write_keypair_file(&authority_keypair, &authority_keypair_file).unwrap(); + let test_command = test_commands.clone().get_matches_from(vec![ + "test", + "program", + "close", + &buffer_pubkey.to_string(), + "--buffer-authority", + &authority_keypair_file, + ]); + assert_eq!( + parse_command(&test_command, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::Program(ProgramCliCommand::Close { + account_pubkey: Some(buffer_pubkey), + recipient_pubkey: default_keypair.pubkey(), + authority_index: 1, + use_lamports_unit: false, + }), + signers: vec![ + read_keypair_file(&keypair_file).unwrap().into(), + read_keypair_file(&authority_keypair_file).unwrap().into(), + ], + } + ); + + // with recipient + let test_command = test_commands.clone().get_matches_from(vec![ + "test", + "program", + "close", + &buffer_pubkey.to_string(), + "--recipient", + &recipient_pubkey.to_string(), + ]); + assert_eq!( + parse_command(&test_command, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::Program(ProgramCliCommand::Close { + account_pubkey: Some(buffer_pubkey), + recipient_pubkey, + authority_index: 0, + use_lamports_unit: false, + }), + signers: vec![read_keypair_file(&keypair_file).unwrap().into(),], + } + ); + + // --buffers and lamports + let test_command = test_commands.clone().get_matches_from(vec![ + "test", + "program", + "close", + "--buffers", + "--lamports", + ]); + assert_eq!( + parse_command(&test_command, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::Program(ProgramCliCommand::Close { + account_pubkey: None, + recipient_pubkey: default_keypair.pubkey(), + authority_index: 0, + use_lamports_unit: true, + }), + signers: vec![read_keypair_file(&keypair_file).unwrap().into(),], + } + ); + } + #[test] fn test_cli_keypair_file() { solana_logger::setup(); diff --git a/cli/tests/program.rs b/cli/tests/program.rs index 1b126d6a6..e04b58b2f 100644 --- a/cli/tests/program.rs +++ b/cli/tests/program.rs @@ -442,6 +442,9 @@ fn test_cli_program_deploy_with_authority() { config.signers = vec![&keypair]; config.command = CliCommand::Program(ProgramCliCommand::Show { account_pubkey: Some(program_pubkey), + authority_pubkey: keypair.pubkey(), + all: false, + use_lamports_unit: false, }); let response = process_command(&config); let json: Value = serde_json::from_str(&response.unwrap()).unwrap(); @@ -530,6 +533,9 @@ fn test_cli_program_deploy_with_authority() { config.signers = vec![&keypair]; config.command = CliCommand::Program(ProgramCliCommand::Show { account_pubkey: Some(program_pubkey), + authority_pubkey: keypair.pubkey(), + all: false, + use_lamports_unit: false, }); let response = process_command(&config); let json: Value = serde_json::from_str(&response.unwrap()).unwrap(); @@ -657,9 +663,12 @@ fn test_cli_program_write_buffer() { ); // Get buffer authority - config.signers = vec![&keypair]; + config.signers = vec![]; config.command = CliCommand::Program(ProgramCliCommand::Show { account_pubkey: Some(buffer_keypair.pubkey()), + authority_pubkey: keypair.pubkey(), + all: false, + use_lamports_unit: false, }); let response = process_command(&config); let json: Value = serde_json::from_str(&response.unwrap()).unwrap(); @@ -747,9 +756,12 @@ fn test_cli_program_write_buffer() { ); // Get buffer authority - config.signers = vec![&keypair]; + config.signers = vec![]; config.command = CliCommand::Program(ProgramCliCommand::Show { account_pubkey: Some(buffer_pubkey), + authority_pubkey: keypair.pubkey(), + all: false, + use_lamports_unit: false, }); let response = process_command(&config); let json: Value = serde_json::from_str(&response.unwrap()).unwrap(); @@ -764,6 +776,60 @@ fn test_cli_program_write_buffer() { authority_keypair.pubkey(), Pubkey::from_str(&authority_pubkey_str).unwrap() ); + + // Close buffer + let close_account = rpc_client.get_account(&buffer_pubkey).unwrap(); + assert_eq!(minimum_balance_for_buffer, close_account.lamports); + let recipient_pubkey = Pubkey::new_unique(); + config.signers = vec![&keypair, &authority_keypair]; + config.command = CliCommand::Program(ProgramCliCommand::Close { + account_pubkey: Some(buffer_pubkey), + recipient_pubkey, + authority_index: 1, + use_lamports_unit: false, + }); + process_command(&config).unwrap(); + rpc_client.get_account(&buffer_pubkey).unwrap_err(); + let recipient_account = rpc_client.get_account(&recipient_pubkey).unwrap(); + assert_eq!(minimum_balance_for_buffer, recipient_account.lamports); + + // Write a buffer with default params + config.signers = vec![&keypair]; + config.command = CliCommand::Program(ProgramCliCommand::WriteBuffer { + program_location: pathbuf.to_str().unwrap().to_string(), + buffer_signer_index: None, + buffer_pubkey: None, + buffer_authority_signer_index: None, + max_len: None, + }); + config.output_format = OutputFormat::JsonCompact; + let response = process_command(&config); + let json: Value = serde_json::from_str(&response.unwrap()).unwrap(); + let buffer_pubkey_str = json + .as_object() + .unwrap() + .get("buffer") + .unwrap() + .as_str() + .unwrap(); + let new_buffer_pubkey = Pubkey::from_str(&buffer_pubkey_str).unwrap(); + + // Close buffers and deposit default keypair + let pre_lamports = rpc_client.get_account(&keypair.pubkey()).unwrap().lamports; + config.signers = vec![&keypair]; + config.command = CliCommand::Program(ProgramCliCommand::Close { + account_pubkey: Some(new_buffer_pubkey), + recipient_pubkey: keypair.pubkey(), + authority_index: 0, + use_lamports_unit: false, + }); + process_command(&config).unwrap(); + rpc_client.get_account(&new_buffer_pubkey).unwrap_err(); + let recipient_account = rpc_client.get_account(&keypair.pubkey()).unwrap(); + assert_eq!( + pre_lamports + minimum_balance_for_buffer, + recipient_account.lamports + ); } #[test] @@ -1029,6 +1095,9 @@ fn test_cli_program_show() { config.signers = vec![&keypair]; config.command = CliCommand::Program(ProgramCliCommand::Show { account_pubkey: Some(buffer_keypair.pubkey()), + authority_pubkey: keypair.pubkey(), + all: false, + use_lamports_unit: false, }); let response = process_command(&config); let json: Value = serde_json::from_str(&response.unwrap()).unwrap(); @@ -1086,6 +1155,9 @@ fn test_cli_program_show() { config.signers = vec![&keypair]; config.command = CliCommand::Program(ProgramCliCommand::Show { account_pubkey: Some(program_keypair.pubkey()), + authority_pubkey: keypair.pubkey(), + all: false, + use_lamports_unit: false, }); let response = process_command(&config); let json: Value = serde_json::from_str(&response.unwrap()).unwrap(); diff --git a/docs/src/cli/deploy-a-program.md b/docs/src/cli/deploy-a-program.md index dec85fd31..bd12de446 100644 --- a/docs/src/cli/deploy-a-program.md +++ b/docs/src/cli/deploy-a-program.md @@ -124,20 +124,25 @@ call to `deploy`. Deployment failures will print an error message specifying the seed phrase needed to recover the generated intermediate buffer's keypair: -```bash -======================================================================= -To resume a failed deploy, recover the ephemeral keypair file with -`solana-keygen recover` and the following 12-word seed phrase, -then pass it as the [BUFFER_SIGNER] argument to `solana deploy` or `solana write-buffer` -======================================================================= -spy axis cream equip bonus daring muffin fish noise churn broken diesel -======================================================================= +``` +================================================================================== +Recover the intermediate account's ephemeral keypair file with +`solana-keygen recover` and the following 12-word seed phrase: +================================================================================== +valley flat great hockey share token excess clever benefit traffic avocado athlete +================================================================================== +To resume a deploy, pass the recovered keypair as +the [PROGRAM_ADDRESS_SIGNER] argument to `solana deploy` or +as the [BUFFER_SIGNER] to `solana program deploy` or `solana write-buffer'. +Or to recover the account's lamports, pass it as the +[BUFFER_ACCOUNT_ADDRESS] argument to `solana program drain`. +================================================================================== ``` To recover the keypair: ```bash -$ solana-keypair recover -o +solana-keypair recover -o ``` When asked, enter the 12-word seed phrase. @@ -145,7 +150,57 @@ When asked, enter the 12-word seed phrase. Then issue a new `deploy` command and specify the buffer: ```bash -$ solana program deploy --buffer +solana program deploy --buffer +``` + +### Closing buffer accounts and reclaiming their lamports + +If deployment fails there will be a left over buffer account that holds +lamports. The buffer account can either be used to [resume a +deploy](#resuming-a-failed-deploy) or closed. When closed, the full balance of +the buffer account will be transferred to the recipient's account. + +The buffer account's authority must be present to close a buffer account, to +list all the open buffer accounts that match the default authority: + +```bash +solana program show --buffers +``` + +To specify a different authority: + +```bash +solana program show --buffers --buffer-authority +``` + +To close a single account: + +```bash +solana program close +``` + +To close a single account and specify a different authority than the default: + +```bash +solana program close --buffer-authority +``` + +To close a single account and specify a different recipient than the default: + +```bash +solana program close --recipient +``` + +To close all the buffer accounts associated with the current authority: + +```bash +solana program close --buffers +``` + +To show all buffer accounts regardless of the authority + +```bash +solana program show --buffers --all ``` ### Set a program's upgrade authority diff --git a/programs/bpf_loader/src/lib.rs b/programs/bpf_loader/src/lib.rs index c33de0787..64a76a9db 100644 --- a/programs/bpf_loader/src/lib.rs +++ b/programs/bpf_loader/src/lib.rs @@ -30,7 +30,7 @@ use solana_sdk::{ bpf_loader_upgradeable::{self, UpgradeableLoaderState}, clock::Clock, entrypoint::SUCCESS, - feature_set::skip_ro_deserialization, + feature_set::{skip_ro_deserialization, upgradeable_close_instruction}, ic_logger_msg, ic_msg, instruction::InstructionError, keyed_account::{from_keyed_account, next_keyed_account, KeyedAccount}, @@ -566,11 +566,9 @@ fn process_loader_upgradeable_instruction( programdata.try_account_ref_mut()?.data_as_mut_slice() [programdata_data_offset..programdata_data_offset + buffer_data_len] .copy_from_slice(&buffer.try_account_ref()?.data()[buffer_data_offset..]); - for i in &mut programdata.try_account_ref_mut()?.data_as_mut_slice() + programdata.try_account_ref_mut()?.data_as_mut_slice() [programdata_data_offset + buffer_data_len..] - { - *i = 0 - } + .fill(0); // Fund ProgramData to rent-exemption, spill the rest @@ -634,12 +632,49 @@ fn process_loader_upgradeable_instruction( } _ => { ic_logger_msg!(logger, "Account does not support authorities"); - return Err(InstructionError::InvalidAccountData); + return Err(InstructionError::InvalidArgument); } } ic_logger_msg!(logger, "New authority {:?}", new_authority); } + UpgradeableLoaderInstruction::Close => { + if !invoke_context.is_feature_active(&upgradeable_close_instruction::id()) { + return Err(InstructionError::InvalidInstructionData); + } + let close_account = next_keyed_account(account_iter)?; + let recipient_account = next_keyed_account(account_iter)?; + let authority = next_keyed_account(account_iter)?; + + if close_account.unsigned_key() == recipient_account.unsigned_key() { + ic_logger_msg!(logger, "Recipient is the same as the account being closed"); + return Err(InstructionError::InvalidArgument); + } + + if let UpgradeableLoaderState::Buffer { authority_address } = close_account.state()? { + if authority_address.is_none() { + ic_logger_msg!(logger, "Buffer is immutable"); + return Err(InstructionError::Immutable); + } + if authority_address != Some(*authority.unsigned_key()) { + ic_logger_msg!(logger, "Incorrect buffer authority provided"); + return Err(InstructionError::IncorrectAuthority); + } + if authority.signer_key().is_none() { + ic_logger_msg!(logger, "Buffer authority did not sign"); + return Err(InstructionError::MissingRequiredSignature); + } + + recipient_account.try_account_ref_mut()?.lamports += close_account.lamports()?; + close_account.try_account_ref_mut()?.lamports = 0; + close_account.try_account_ref_mut()?.data.fill(0); + } else { + ic_logger_msg!(logger, "Account does not support closing"); + return Err(InstructionError::InvalidArgument); + } + + ic_logger_msg!(logger, "Closed {}", close_account.unsigned_key()); + } } Ok(()) @@ -3011,7 +3046,7 @@ mod tests { }) .unwrap(); assert_eq!( - Err(InstructionError::InvalidAccountData), + Err(InstructionError::InvalidArgument), process_instruction( &bpf_loader_upgradeable::id(), &[ @@ -3184,7 +3219,7 @@ mod tests { }) .unwrap(); assert_eq!( - Err(InstructionError::InvalidAccountData), + Err(InstructionError::InvalidArgument), process_instruction( &bpf_loader_upgradeable::id(), &[ @@ -3217,6 +3252,87 @@ mod tests { ); } + #[test] + fn test_bpf_loader_upgradeable_close() { + let instruction = bincode::serialize(&UpgradeableLoaderInstruction::Close).unwrap(); + let authority_address = Pubkey::new_unique(); + let authority_account = AccountSharedData::new_ref(1, 0, &Pubkey::new_unique()); + let recipient_address = Pubkey::new_unique(); + let recipient_account = AccountSharedData::new_ref(1, 0, &Pubkey::new_unique()); + let buffer_address = Pubkey::new_unique(); + let buffer_account = AccountSharedData::new_ref( + 1, + UpgradeableLoaderState::buffer_len(0).unwrap(), + &bpf_loader_upgradeable::id(), + ); + + // Case: close a buffer account + buffer_account + .borrow_mut() + .set_state(&UpgradeableLoaderState::Buffer { + authority_address: Some(authority_address), + }) + .unwrap(); + assert_eq!( + Ok(()), + process_instruction( + &bpf_loader_upgradeable::id(), + &[ + KeyedAccount::new(&buffer_address, false, &buffer_account), + KeyedAccount::new(&recipient_address, false, &recipient_account), + KeyedAccount::new_readonly(&authority_address, true, &authority_account), + ], + &instruction, + &mut MockInvokeContext::default() + ) + ); + assert_eq!(0, buffer_account.borrow().lamports()); + assert_eq!(2, recipient_account.borrow().lamports()); + assert!(buffer_account.borrow().data.iter().all(|&value| value == 0)); + + // Case: close with wrong authority + buffer_account + .borrow_mut() + .set_state(&UpgradeableLoaderState::Buffer { + authority_address: Some(authority_address), + }) + .unwrap(); + assert_eq!( + Err(InstructionError::IncorrectAuthority), + process_instruction( + &bpf_loader_upgradeable::id(), + &[ + KeyedAccount::new(&buffer_address, false, &buffer_account), + KeyedAccount::new(&recipient_address, false, &recipient_account), + KeyedAccount::new_readonly(&Pubkey::new_unique(), true, &authority_account), + ], + &instruction, + &mut MockInvokeContext::default() + ) + ); + + // Case: close but not a buffer account + buffer_account + .borrow_mut() + .set_state(&UpgradeableLoaderState::Program { + programdata_address: Pubkey::new_unique(), + }) + .unwrap(); + assert_eq!( + Err(InstructionError::InvalidArgument), + process_instruction( + &bpf_loader_upgradeable::id(), + &[ + KeyedAccount::new(&buffer_address, false, &buffer_account), + KeyedAccount::new(&recipient_address, false, &recipient_account), + KeyedAccount::new_readonly(&Pubkey::new_unique(), true, &authority_account), + ], + &instruction, + &mut MockInvokeContext::default() + ) + ); + } + /// fuzzing utility function fn fuzz( bytes: &[u8], diff --git a/sdk/program/src/bpf_loader_upgradeable.rs b/sdk/program/src/bpf_loader_upgradeable.rs index 697c4a4e9..131a9206d 100644 --- a/sdk/program/src/bpf_loader_upgradeable.rs +++ b/sdk/program/src/bpf_loader_upgradeable.rs @@ -227,6 +227,20 @@ pub fn set_upgrade_authority( Instruction::new_with_bincode(id(), &UpgradeableLoaderInstruction::SetAuthority, metas) } +/// Returns the instructions required to close an account +pub fn close( + close_address: &Pubkey, + recipient_address: &Pubkey, + authority_address: &Pubkey, +) -> Instruction { + let metas = vec![ + AccountMeta::new(*close_address, false), + AccountMeta::new(*recipient_address, false), + AccountMeta::new_readonly(*authority_address, true), + ]; + Instruction::new_with_bincode(id(), &UpgradeableLoaderInstruction::Close, metas) +} + #[cfg(test)] mod tests { use super::*; diff --git a/sdk/program/src/loader_upgradeable_instruction.rs b/sdk/program/src/loader_upgradeable_instruction.rs index 9adfbd696..76fd0e34e 100644 --- a/sdk/program/src/loader_upgradeable_instruction.rs +++ b/sdk/program/src/loader_upgradeable_instruction.rs @@ -107,4 +107,13 @@ pub enum UpgradeableLoaderInstruction { /// 2. `[]` The new authority, optional, if omitted then the program will /// not be upgradeable. SetAuthority, + + /// Closes an account owned by the upgradeable loader of all lamports and + /// withdraws all the lamports + /// + /// # Account references + /// 0. `[writable]` The account to close. + /// 1. `[writable]` The account to deposit the closed account's lamports. + /// 2. `[signer]` The account's authority. + Close, } diff --git a/sdk/src/feature_set.rs b/sdk/src/feature_set.rs index e447bcf2e..8ead147e6 100644 --- a/sdk/src/feature_set.rs +++ b/sdk/src/feature_set.rs @@ -119,6 +119,10 @@ pub mod cpi_data_cost { solana_sdk::declare_id!("Hrg5bXePPGiAVWZfDHbvjqytSeyBDPAGAQ7v6N5i4gCX"); } +pub mod upgradeable_close_instruction { + solana_sdk::declare_id!("FsPaByos3gA9bUEhp3EimQpQPCoSvCEigHod496NmABQ"); +} + lazy_static! { /// Map of feature identifiers to user-visible description pub static ref FEATURE_NAMES: HashMap = [ @@ -149,6 +153,7 @@ lazy_static! { (skip_ro_deserialization::id(), "skip deserialization of read-only accounts"), (require_stake_for_gossip::id(), "require stakes for propagating crds values through gossip #15561"), (cpi_data_cost::id(), "charge the compute budget for data passed via CPI"), + (upgradeable_close_instruction::id(), "close upgradeable buffer accounts"), /*************** ADD NEW FEATURES HERE ***************/ ] .iter() diff --git a/transaction-status/src/parse_bpf_loader.rs b/transaction-status/src/parse_bpf_loader.rs index a99f82b4e..cd1a45947 100644 --- a/transaction-status/src/parse_bpf_loader.rs +++ b/transaction-status/src/parse_bpf_loader.rs @@ -131,6 +131,17 @@ pub fn parse_bpf_upgradeable_loader( }), }) } + UpgradeableLoaderInstruction::Close => { + check_num_bpf_upgradeable_loader_accounts(&instruction.accounts, 3)?; + Ok(ParsedInstructionEnum { + instruction_type: "close".to_string(), + info: json!({ + "account": account_keys[instruction.accounts[0] as usize].to_string(), + "recipient": account_keys[instruction.accounts[1] as usize].to_string(), + "authority": account_keys[instruction.accounts[2] as usize].to_string() + }), + }) + } } } @@ -352,5 +363,20 @@ mod test { } ); assert!(parse_bpf_upgradeable_loader(&message.instructions[0], &keys[0..1]).is_err()); + + let instruction = solana_sdk::bpf_loader_upgradeable::close(&keys[0], &keys[1], &keys[2]); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_bpf_upgradeable_loader(&message.instructions[0], &keys[..3]).unwrap(), + ParsedInstructionEnum { + instruction_type: "close".to_string(), + info: json!({ + "account": keys[1].to_string(), + "recipient": keys[2].to_string(), + "authority": keys[0].to_string(), + }), + } + ); + assert!(parse_bpf_upgradeable_loader(&message.instructions[0], &keys[0..1]).is_err()); } }