From 7ffaf2ad29481edd7756222c63f3eb9e3feeb6dc Mon Sep 17 00:00:00 2001 From: Michael Vines Date: Sun, 22 Mar 2020 22:44:55 -0700 Subject: [PATCH] Manual v1.0 backports (#9015) automerge --- Cargo.lock | 1 + cli/src/cli.rs | 67 ++-- cli/src/nonce.rs | 12 +- cli/src/stake.rs | 506 ++++++++++++++++++--------- cli/src/storage.rs | 8 +- cli/src/vote.rs | 12 +- cli/tests/stake.rs | 67 ++-- core/src/validator.rs | 3 + core/tests/bank_forks.rs | 2 + docs/src/SUMMARY.md | 1 + docs/src/cli/.usage.md.header | 2 +- docs/src/cli/README.md | 10 +- docs/src/cli/choose-a-wallet.md | 10 +- docs/src/cli/delegate-stake.md | 197 +++++++++++ docs/src/cli/generate-keys.md | 4 +- docs/src/cli/transfer-tokens.md | 35 +- docs/src/offline-signing/README.md | 3 +- docs/src/remote-wallet/README.md | 2 +- docs/src/remote-wallet/ledger.md | 70 +--- ledger-tool/src/main.rs | 51 ++- ledger/src/bank_forks_utils.rs | 1 + ledger/src/blockstore_processor.rs | 10 +- ledger/src/snapshot_utils.rs | 21 +- local-cluster/src/local_cluster.rs | 2 +- local-cluster/tests/local_cluster.rs | 113 +++++- runtime/Cargo.toml | 1 + runtime/benches/accounts.rs | 3 +- runtime/src/accounts.rs | 70 +++- runtime/src/accounts_db.rs | 268 ++++++++++++-- runtime/src/bank.rs | 79 +++-- runtime/src/message_processor.rs | 2 +- validator/src/main.rs | 15 +- 32 files changed, 1201 insertions(+), 447 deletions(-) create mode 100644 docs/src/cli/delegate-stake.md diff --git a/Cargo.lock b/Cargo.lock index 1c5c7f998f..0ca3436e92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4460,6 +4460,7 @@ dependencies = [ "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", "fs_extra 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "itertools 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", "libloading 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/cli/src/cli.rs b/cli/src/cli.rs index c12c1d60df..b7189bcf17 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -301,9 +301,7 @@ pub enum CliCommand { }, StakeAuthorize { stake_account_pubkey: Pubkey, - new_authorized_pubkey: Pubkey, - stake_authorize: StakeAuthorize, - authority: SignerIndex, + new_authorizations: Vec<(StakeAuthorize, Pubkey, SignerIndex)>, sign_only: bool, blockhash_query: BlockhashQuery, nonce_account: Option, @@ -644,18 +642,9 @@ pub fn parse_command( ("split-stake", Some(matches)) => { parse_split_stake(matches, default_signer_path, wallet_manager) } - ("stake-authorize-staker", Some(matches)) => parse_stake_authorize( - matches, - default_signer_path, - wallet_manager, - StakeAuthorize::Staker, - ), - ("stake-authorize-withdrawer", Some(matches)) => parse_stake_authorize( - matches, - default_signer_path, - wallet_manager, - StakeAuthorize::Withdrawer, - ), + ("stake-authorize", Some(matches)) => { + parse_stake_authorize(matches, default_signer_path, wallet_manager) + } ("stake-set-lockup", Some(matches)) => { parse_stake_set_lockup(matches, default_signer_path, wallet_manager) } @@ -1825,9 +1814,7 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { } CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorized_pubkey, - stake_authorize, - authority, + ref new_authorizations, sign_only, blockhash_query, nonce_account, @@ -1837,9 +1824,7 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { &rpc_client, config, &stake_account_pubkey, - &new_authorized_pubkey, - *stake_authorize, - *authority, + new_authorizations, *sign_only, blockhash_query, *nonce_account, @@ -2264,10 +2249,10 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' .arg( Arg::with_name("to") .index(2) - .value_name("RECIPIENT_PUBKEY") + .value_name("RECIPIENT_ADDRESS") .takes_value(true) .validator(is_valid_pubkey) - .help("The pubkey of airdrop recipient"), + .help("The account address of airdrop recipient"), ), ) .subcommand( @@ -2276,10 +2261,10 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' .arg( Arg::with_name("pubkey") .index(1) - .value_name("ACCOUNT_PUBKEY") + .value_name("ACCOUNT_ADDRESS") .takes_value(true) .validator(is_valid_pubkey) - .help("The public key of the balance to check"), + .help("The account address of the balance to check"), ) .arg( Arg::with_name("lamports") @@ -2294,11 +2279,11 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' .arg( Arg::with_name("process_id") .index(1) - .value_name("PROCESS_PUBKEY") + .value_name("ACCOUNT_ADDRESS") .takes_value(true) .required(true) .validator(is_pubkey) - .help("The process id of the transfer to cancel"), + .help("The account address of the transfer to cancel"), ), ) .subcommand( @@ -2327,7 +2312,7 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' .arg( Arg::with_name("program_id") .index(2) - .value_name("PROGRAM_PUBKEY") + .value_name("PROGRAM_ID") .takes_value(true) .required(true) .help( @@ -2363,11 +2348,11 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' .arg( Arg::with_name("to") .index(1) - .value_name("RECIPIENT_PUBKEY") + .value_name("RECIPIENT_ADDRESS") .takes_value(true) .required(true) .validator(is_valid_pubkey) - .help("The pubkey of recipient"), + .help("The account address of recipient"), ) .arg( Arg::with_name("amount") @@ -2432,19 +2417,19 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' .arg( Arg::with_name("to") .index(1) - .value_name("RECIPIENT_PUBKEY") + .value_name("RECIPIENT_ADDRESS") .takes_value(true) .required(true) .validator(is_pubkey) - .help("The pubkey of recipient"), + .help("The account address of recipient"), ) .arg( Arg::with_name("process_id") .index(2) - .value_name("PROCESS_PUBKEY") + .value_name("ACCOUNT_ADDRESS") .takes_value(true) .required(true) - .help("The process id of the transfer to authorize"), + .help("The account address of the transfer to authorize"), ), ) .subcommand( @@ -2453,19 +2438,19 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' .arg( Arg::with_name("to") .index(1) - .value_name("RECIPIENT_PUBKEY") + .value_name("RECIPIENT_ADDRESS") .takes_value(true) .required(true) .validator(is_pubkey) - .help("The pubkey of recipient"), + .help("The account address of recipient"), ) .arg( Arg::with_name("process_id") .index(2) - .value_name("PROCESS_PUBKEY") + .value_name("ACCOUNT_ADDRESS") .takes_value(true) .required(true) - .help("The process id of the transfer to unlock"), + .help("The account address of the transfer to unlock"), ) .arg( Arg::with_name("datetime") @@ -2481,11 +2466,11 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' .arg( Arg::with_name("to") .index(1) - .value_name("RECIPIENT_PUBKEY") + .value_name("RECIPIENT_ADDRESS") .takes_value(true) .required(true) .validator(is_valid_pubkey) - .help("The pubkey of recipient"), + .help("The account address of recipient"), ) .arg( Arg::with_name("amount") @@ -2516,7 +2501,7 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' .arg( Arg::with_name("account_pubkey") .index(1) - .value_name("ACCOUNT_PUBKEY") + .value_name("ACCOUNT_ADDRESS") .takes_value(true) .required(true) .validator(is_valid_pubkey) diff --git a/cli/src/nonce.rs b/cli/src/nonce.rs index b380f685b5..0a08ccc781 100644 --- a/cli/src/nonce.rs +++ b/cli/src/nonce.rs @@ -95,7 +95,7 @@ impl NonceSubCommands for App<'_, '_> { .arg( Arg::with_name("nonce_account_pubkey") .index(1) - .value_name("ACCOUNT_PUBKEY") + .value_name("NONCE_ACCOUNT_ADDRESS") .takes_value(true) .required(true) .validator(is_valid_pubkey) @@ -156,7 +156,7 @@ impl NonceSubCommands for App<'_, '_> { .arg( Arg::with_name("nonce_account_pubkey") .index(1) - .value_name("ACCOUNT_PUBKEY") + .value_name("NONCE_ACCOUNT_ADDRESS") .takes_value(true) .required(true) .validator(is_valid_pubkey) @@ -169,7 +169,7 @@ impl NonceSubCommands for App<'_, '_> { .arg( Arg::with_name("nonce_account_pubkey") .index(1) - .value_name("ACCOUNT_PUBKEY") + .value_name("NONCE_ACCOUNT_ADDRESS") .takes_value(true) .required(true) .validator(is_valid_pubkey) @@ -184,7 +184,7 @@ impl NonceSubCommands for App<'_, '_> { .arg( Arg::with_name("nonce_account_pubkey") .index(1) - .value_name("ACCOUNT_PUBKEY") + .value_name("NONCE_ACCOUNT_ADDRESS") .takes_value(true) .required(true) .validator(is_valid_pubkey) @@ -203,7 +203,7 @@ impl NonceSubCommands for App<'_, '_> { .arg( Arg::with_name("nonce_account_pubkey") .index(1) - .value_name("ACCOUNT_PUBKEY") + .value_name("NONCE_ACCOUNT_ADDRESS") .takes_value(true) .required(true) .validator(is_valid_pubkey) @@ -212,7 +212,7 @@ impl NonceSubCommands for App<'_, '_> { .arg( Arg::with_name("destination_account_pubkey") .index(2) - .value_name("RECIPIENT_PUBKEY") + .value_name("RECIPIENT_ADDRESS") .takes_value(true) .required(true) .validator(is_valid_pubkey) diff --git a/cli/src/stake.rs b/cli/src/stake.rs index 5e91975e74..efc4cf3452 100644 --- a/cli/src/stake.rs +++ b/cli/src/stake.rs @@ -160,7 +160,7 @@ impl StakeSubCommands for App<'_, '_> { .arg( Arg::with_name("stake_account_pubkey") .index(1) - .value_name("ACCOUNT_PUBKEY") + .value_name("STAKE_ACCOUNT_ADDRESS") .takes_value(true) .required(true) .validator(is_valid_pubkey) @@ -169,7 +169,7 @@ impl StakeSubCommands for App<'_, '_> { .arg( Arg::with_name("vote_account_pubkey") .index(2) - .value_name("VOTE_PUBKEY") + .value_name("VOTE_ACCOUNT_ADDRESS") .takes_value(true) .required(true) .validator(is_valid_pubkey) @@ -182,53 +182,36 @@ impl StakeSubCommands for App<'_, '_> { .arg(fee_payer_arg()) ) .subcommand( - SubCommand::with_name("stake-authorize-staker") - .about("Authorize a new stake signing keypair for the given stake account") + SubCommand::with_name("stake-authorize") + .about("Authorize a new signing keypair for the given stake account") .arg( Arg::with_name("stake_account_pubkey") - .index(1) - .value_name("ACCOUNT_PUBKEY") - .takes_value(true) .required(true) + .index(1) + .takes_value(true) + .value_name("STAKE_ACCOUNT_ADDRESS") .validator(is_valid_pubkey) - .help("Stake account in which to set the authorized staker") + .help("Stake account in which to set a new authority") ) .arg( - Arg::with_name("authorized_pubkey") - .index(2) - .value_name("AUTHORIZED_PUBKEY") + Arg::with_name("new_stake_authority") + .long("new-stake-authority") + .required_unless("new_withdraw_authority") .takes_value(true) - .required(true) + .value_name("PUBKEY") .validator(is_valid_pubkey) .help("New authorized staker") ) - .arg(stake_authority_arg()) - .offline_args() - .arg(nonce_arg()) - .arg(nonce_authority_arg()) - .arg(fee_payer_arg()) - ) - .subcommand( - SubCommand::with_name("stake-authorize-withdrawer") - .about("Authorize a new withdraw signing keypair for the given stake account") .arg( - Arg::with_name("stake_account_pubkey") - .index(1) - .value_name("ACCOUNT_PUBKEY") + Arg::with_name("new_withdraw_authority") + .long("new-withdraw-authority") + .required_unless("new_stake_authority") .takes_value(true) - .required(true) - .validator(is_valid_pubkey) - .help("Stake account in which to set the authorized withdrawer") - ) - .arg( - Arg::with_name("authorized_pubkey") - .index(2) - .value_name("AUTHORIZED_PUBKEY") - .takes_value(true) - .required(true) + .value_name("PUBKEY") .validator(is_valid_pubkey) .help("New authorized withdrawer") ) + .arg(stake_authority_arg()) .arg(withdraw_authority_arg()) .offline_args() .arg(nonce_arg()) @@ -241,7 +224,7 @@ impl StakeSubCommands for App<'_, '_> { .arg( Arg::with_name("stake_account_pubkey") .index(1) - .value_name("ACCOUNT_PUBKEY") + .value_name("STAKE_ACCOUNT_ADDRESS") .takes_value(true) .required(true) .validator(is_valid_pubkey) @@ -259,7 +242,7 @@ impl StakeSubCommands for App<'_, '_> { .arg( Arg::with_name("stake_account_pubkey") .index(1) - .value_name("ACCOUNT_PUBKEY") + .value_name("STAKE_ACCOUNT_ADDRESS") .takes_value(true) .required(true) .validator(is_valid_pubkey) @@ -302,7 +285,7 @@ impl StakeSubCommands for App<'_, '_> { .arg( Arg::with_name("stake_account_pubkey") .index(1) - .value_name("ACCOUNT_PUBKEY") + .value_name("STAKE_ACCOUNT_ADDRESS") .takes_value(true) .required(true) .validator(is_valid_pubkey) @@ -311,7 +294,7 @@ impl StakeSubCommands for App<'_, '_> { .arg( Arg::with_name("destination_account_pubkey") .index(2) - .value_name("RECIPIENT_PUBKEY") + .value_name("RECIPIENT_ADDRESS") .takes_value(true) .required(true) .validator(is_valid_pubkey) @@ -338,7 +321,7 @@ impl StakeSubCommands for App<'_, '_> { .arg( Arg::with_name("stake_account_pubkey") .index(1) - .value_name("ACCOUNT_PUBKEY") + .value_name("STAKE_ACCOUNT_ADDRESS") .takes_value(true) .required(true) .validator(is_valid_pubkey) @@ -390,7 +373,7 @@ impl StakeSubCommands for App<'_, '_> { .arg( Arg::with_name("stake_account_pubkey") .index(1) - .value_name("ACCOUNT_PUBKEY") + .value_name("STAKE_ACCOUNT_ADDRESS") .takes_value(true) .required(true) .validator(is_valid_pubkey) @@ -515,37 +498,75 @@ pub fn parse_stake_authorize( matches: &ArgMatches<'_>, default_signer_path: &str, wallet_manager: Option<&Arc>, - stake_authorize: StakeAuthorize, ) -> Result { let stake_account_pubkey = pubkey_of_signer(matches, "stake_account_pubkey", wallet_manager)?.unwrap(); - let new_authorized_pubkey = - pubkey_of_signer(matches, "authorized_pubkey", wallet_manager)?.unwrap(); - let authority_flag = match stake_authorize { - StakeAuthorize::Staker => STAKE_AUTHORITY_ARG.name, - StakeAuthorize::Withdrawer => WITHDRAW_AUTHORITY_ARG.name, + + let mut new_authorizations = Vec::new(); + let mut bulk_signers = Vec::new(); + if let Some(new_authority_pubkey) = + pubkey_of_signer(matches, "new_stake_authority", wallet_manager)? + { + let (authority, authority_pubkey) = { + let (authority, authority_pubkey) = + signer_of(matches, STAKE_AUTHORITY_ARG.name, wallet_manager)?; + // Withdraw authority may also change the staker + if authority.is_none() { + signer_of(matches, WITHDRAW_AUTHORITY_ARG.name, wallet_manager)? + } else { + (authority, authority_pubkey) + } + }; + new_authorizations.push(( + StakeAuthorize::Staker, + new_authority_pubkey, + authority_pubkey, + )); + bulk_signers.push(authority); + }; + if let Some(new_authority_pubkey) = + pubkey_of_signer(matches, "new_withdraw_authority", wallet_manager)? + { + let (authority, authority_pubkey) = + signer_of(matches, WITHDRAW_AUTHORITY_ARG.name, wallet_manager)?; + new_authorizations.push(( + StakeAuthorize::Withdrawer, + new_authority_pubkey, + authority_pubkey, + )); + bulk_signers.push(authority); }; let sign_only = matches.is_present(SIGN_ONLY_ARG.name); - let (authority, authority_pubkey) = signer_of(matches, authority_flag, wallet_manager)?; let blockhash_query = BlockhashQuery::new_from_matches(matches); let nonce_account = pubkey_of(matches, NONCE_ARG.name); let (nonce_authority, nonce_authority_pubkey) = signer_of(matches, NONCE_AUTHORITY_ARG.name, wallet_manager)?; let (fee_payer, fee_payer_pubkey) = signer_of(matches, FEE_PAYER_ARG.name, wallet_manager)?; - let mut bulk_signers = vec![authority, fee_payer]; + bulk_signers.push(fee_payer); if nonce_account.is_some() { bulk_signers.push(nonce_authority); } let signer_info = generate_unique_signers(bulk_signers, matches, default_signer_path, wallet_manager)?; + let new_authorizations = new_authorizations + .into_iter() + .map( + |(stake_authorize, new_authority_pubkey, authority_pubkey)| { + ( + stake_authorize, + new_authority_pubkey, + signer_info.index_of(authority_pubkey).unwrap(), + ) + }, + ) + .collect(); + Ok(CliCommandInfo { command: CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorized_pubkey, - stake_authorize, - authority: signer_info.index_of(authority_pubkey).unwrap(), + new_authorizations, sign_only, blockhash_query, nonce_account, @@ -871,28 +892,30 @@ pub fn process_stake_authorize( rpc_client: &RpcClient, config: &CliConfig, stake_account_pubkey: &Pubkey, - authorized_pubkey: &Pubkey, - stake_authorize: StakeAuthorize, - authority: SignerIndex, + new_authorizations: &[(StakeAuthorize, Pubkey, SignerIndex)], sign_only: bool, blockhash_query: &BlockhashQuery, nonce_account: Option, nonce_authority: SignerIndex, fee_payer: SignerIndex, ) -> ProcessResult { - check_unique_pubkeys( - (stake_account_pubkey, "stake_account_pubkey".to_string()), - (authorized_pubkey, "new_authorized_pubkey".to_string()), - )?; - let authority = config.signers[authority]; + let mut ixs = Vec::new(); + for (stake_authorize, authorized_pubkey, authority) in new_authorizations.iter() { + check_unique_pubkeys( + (stake_account_pubkey, "stake_account_pubkey".to_string()), + (authorized_pubkey, "new_authorized_pubkey".to_string()), + )?; + let authority = config.signers[*authority]; + ixs.push(stake_instruction::authorize( + stake_account_pubkey, // stake account to update + &authority.pubkey(), // currently authorized + authorized_pubkey, // new stake signer + *stake_authorize, // stake or withdraw + )); + } + let (recent_blockhash, fee_calculator) = blockhash_query.get_blockhash_and_fee_calculator(rpc_client)?; - let ixs = vec![stake_instruction::authorize( - stake_account_pubkey, // stake account to update - &authority.pubkey(), // currently authorized - authorized_pubkey, // new stake signer - stake_authorize, // stake or withdraw - )]; let nonce_authority = config.signers[nonce_authority]; let fee_payer = config.signers[fee_payer]; @@ -1236,8 +1259,8 @@ pub fn process_stake_set_lockup( pub fn print_stake_state(stake_lamports: u64, stake_state: &StakeState, use_lamports_unit: bool) { fn show_authorized(authorized: &Authorized) { - println!("Authorized Staker: {}", authorized.staker); - println!("Authorized Withdrawer: {}", authorized.withdrawer); + println!("Stake Authority: {}", authorized.staker); + println!("Withdraw Authority: {}", authorized.withdrawer); } fn show_lockup(lockup: &Lockup) { println!( @@ -1266,7 +1289,10 @@ pub fn print_stake_state(stake_lamports: u64, stake_state: &StakeState, use_lamp build_balance_message(stake.delegation.stake, use_lamports_unit, true) ); if stake.delegation.voter_pubkey != Pubkey::default() { - println!("Delegated Voter Pubkey: {}", stake.delegation.voter_pubkey); + println!( + "Delegated Vote Account Address: {}", + stake.delegation.voter_pubkey + ); } println!( "Stake activates starting from epoch: {}", @@ -1486,83 +1512,279 @@ mod tests { (String::from(tmp_file.path().to_str().unwrap()), tmp_file) } - fn parse_authorize_tests( - test_commands: &App, - stake_account_pubkey: Pubkey, - authority_keypair_file: &str, - stake_authorize: StakeAuthorize, - ) { + #[test] + fn test_parse_command() { + let test_commands = app("test", "desc", "version"); let default_keypair = Keypair::new(); let (default_keypair_file, mut tmp_file) = make_tmp_file(); write_keypair(&default_keypair, tmp_file.as_file_mut()).unwrap(); + let (keypair_file, mut tmp_file) = make_tmp_file(); + let stake_account_keypair = Keypair::new(); + write_keypair(&stake_account_keypair, tmp_file.as_file_mut()).unwrap(); + let stake_account_pubkey = stake_account_keypair.pubkey(); + let (stake_authority_keypair_file, mut tmp_file) = make_tmp_file(); + let stake_authority_keypair = Keypair::new(); + write_keypair(&stake_authority_keypair, tmp_file.as_file_mut()).unwrap(); + // stake-authorize subcommand let stake_account_string = stake_account_pubkey.to_string(); - - let (subcommand, authority_flag) = match stake_authorize { - StakeAuthorize::Staker => ("stake-authorize-staker", "--stake-authority"), - StakeAuthorize::Withdrawer => ("stake-authorize-withdrawer", "--withdraw-authority"), - }; - - // Test Staker Subcommand - let test_authorize = test_commands.clone().get_matches_from(vec![ + let new_stake_authority = Pubkey::new(&[1u8; 32]); + let new_stake_string = new_stake_authority.to_string(); + let new_withdraw_authority = Pubkey::new(&[2u8; 32]); + let new_withdraw_string = new_withdraw_authority.to_string(); + let test_stake_authorize = test_commands.clone().get_matches_from(vec![ "test", - &subcommand, - &stake_account_string, + "stake-authorize", &stake_account_string, + "--new-stake-authority", + &new_stake_string, + "--new-withdraw-authority", + &new_withdraw_string, ]); assert_eq!( - parse_command(&test_authorize, &default_keypair_file, None).unwrap(), + parse_command(&test_stake_authorize, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorized_pubkey: stake_account_pubkey, - stake_authorize, - authority: 0, + new_authorizations: vec![ + (StakeAuthorize::Staker, new_stake_authority, 0,), + (StakeAuthorize::Withdrawer, new_withdraw_authority, 0,), + ], sign_only: false, - blockhash_query: BlockhashQuery::default(), + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), nonce_account: None, nonce_authority: 0, fee_payer: 0, }, - signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()], - } + signers: vec![read_keypair_file(&default_keypair_file).unwrap().into(),], + }, ); - // Test Staker Subcommand w/ authority - let test_authorize = test_commands.clone().get_matches_from(vec![ + let (withdraw_authority_keypair_file, mut tmp_file) = make_tmp_file(); + let withdraw_authority_keypair = Keypair::new(); + write_keypair(&withdraw_authority_keypair, tmp_file.as_file_mut()).unwrap(); + let test_stake_authorize = test_commands.clone().get_matches_from(vec![ "test", - &subcommand, + "stake-authorize", &stake_account_string, - &stake_account_string, - &authority_flag, - &authority_keypair_file, + "--new-stake-authority", + &new_stake_string, + "--new-withdraw-authority", + &new_withdraw_string, + "--stake-authority", + &stake_authority_keypair_file, + "--withdraw-authority", + &withdraw_authority_keypair_file, ]); assert_eq!( - parse_command(&test_authorize, &default_keypair_file, None).unwrap(), + parse_command(&test_stake_authorize, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorized_pubkey: stake_account_pubkey, - stake_authorize, - authority: 1, + new_authorizations: vec![ + (StakeAuthorize::Staker, new_stake_authority, 1,), + (StakeAuthorize::Withdrawer, new_withdraw_authority, 2,), + ], sign_only: false, - blockhash_query: BlockhashQuery::default(), + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), nonce_account: None, nonce_authority: 0, fee_payer: 0, }, signers: vec![ read_keypair_file(&default_keypair_file).unwrap().into(), - read_keypair_file(&authority_keypair_file).unwrap().into() + read_keypair_file(&stake_authority_keypair_file) + .unwrap() + .into(), + read_keypair_file(&withdraw_authority_keypair_file) + .unwrap() + .into(), ], - } + }, ); + // Withdraw authority may set both new authorities + let test_stake_authorize = test_commands.clone().get_matches_from(vec![ + "test", + "stake-authorize", + &stake_account_string, + "--new-stake-authority", + &new_stake_string, + "--new-withdraw-authority", + &new_withdraw_string, + "--withdraw-authority", + &withdraw_authority_keypair_file, + ]); + assert_eq!( + parse_command(&test_stake_authorize, &default_keypair_file, None).unwrap(), + CliCommandInfo { + command: CliCommand::StakeAuthorize { + stake_account_pubkey, + new_authorizations: vec![ + (StakeAuthorize::Staker, new_stake_authority, 1,), + (StakeAuthorize::Withdrawer, new_withdraw_authority, 1,), + ], + sign_only: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + nonce_account: None, + nonce_authority: 0, + fee_payer: 0, + }, + signers: vec![ + read_keypair_file(&default_keypair_file).unwrap().into(), + read_keypair_file(&withdraw_authority_keypair_file) + .unwrap() + .into(), + ], + }, + ); + let test_stake_authorize = test_commands.clone().get_matches_from(vec![ + "test", + "stake-authorize", + &stake_account_string, + "--new-stake-authority", + &new_stake_string, + ]); + assert_eq!( + parse_command(&test_stake_authorize, &default_keypair_file, None).unwrap(), + CliCommandInfo { + command: CliCommand::StakeAuthorize { + stake_account_pubkey, + new_authorizations: vec![(StakeAuthorize::Staker, new_stake_authority, 0,),], + sign_only: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + nonce_account: None, + nonce_authority: 0, + fee_payer: 0, + }, + signers: vec![read_keypair_file(&default_keypair_file).unwrap().into(),], + }, + ); + let test_stake_authorize = test_commands.clone().get_matches_from(vec![ + "test", + "stake-authorize", + &stake_account_string, + "--new-stake-authority", + &new_stake_string, + "--stake-authority", + &stake_authority_keypair_file, + ]); + assert_eq!( + parse_command(&test_stake_authorize, &default_keypair_file, None).unwrap(), + CliCommandInfo { + command: CliCommand::StakeAuthorize { + stake_account_pubkey, + new_authorizations: vec![(StakeAuthorize::Staker, new_stake_authority, 1,),], + sign_only: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + nonce_account: None, + nonce_authority: 0, + fee_payer: 0, + }, + signers: vec![ + read_keypair_file(&default_keypair_file).unwrap().into(), + read_keypair_file(&stake_authority_keypair_file) + .unwrap() + .into(), + ], + }, + ); + // Withdraw authority may set new stake authority + let test_stake_authorize = test_commands.clone().get_matches_from(vec![ + "test", + "stake-authorize", + &stake_account_string, + "--new-stake-authority", + &new_stake_string, + "--withdraw-authority", + &withdraw_authority_keypair_file, + ]); + assert_eq!( + parse_command(&test_stake_authorize, &default_keypair_file, None).unwrap(), + CliCommandInfo { + command: CliCommand::StakeAuthorize { + stake_account_pubkey, + new_authorizations: vec![(StakeAuthorize::Staker, new_stake_authority, 1,),], + sign_only: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + nonce_account: None, + nonce_authority: 0, + fee_payer: 0, + }, + signers: vec![ + read_keypair_file(&default_keypair_file).unwrap().into(), + read_keypair_file(&withdraw_authority_keypair_file) + .unwrap() + .into(), + ], + }, + ); + let test_stake_authorize = test_commands.clone().get_matches_from(vec![ + "test", + "stake-authorize", + &stake_account_string, + "--new-withdraw-authority", + &new_withdraw_string, + ]); + assert_eq!( + parse_command(&test_stake_authorize, &default_keypair_file, None).unwrap(), + CliCommandInfo { + command: CliCommand::StakeAuthorize { + stake_account_pubkey, + new_authorizations: vec![( + StakeAuthorize::Withdrawer, + new_withdraw_authority, + 0, + ),], + sign_only: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + nonce_account: None, + nonce_authority: 0, + fee_payer: 0, + }, + signers: vec![read_keypair_file(&default_keypair_file).unwrap().into(),], + }, + ); + let test_stake_authorize = test_commands.clone().get_matches_from(vec![ + "test", + "stake-authorize", + &stake_account_string, + "--new-withdraw-authority", + &new_withdraw_string, + "--withdraw-authority", + &withdraw_authority_keypair_file, + ]); + assert_eq!( + parse_command(&test_stake_authorize, &default_keypair_file, None).unwrap(), + CliCommandInfo { + command: CliCommand::StakeAuthorize { + stake_account_pubkey, + new_authorizations: vec![( + StakeAuthorize::Withdrawer, + new_withdraw_authority, + 1, + ),], + sign_only: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + nonce_account: None, + nonce_authority: 0, + fee_payer: 0, + }, + signers: vec![ + read_keypair_file(&default_keypair_file).unwrap().into(), + read_keypair_file(&withdraw_authority_keypair_file) + .unwrap() + .into(), + ], + }, + ); + // Test Authorize Subcommand w/ sign-only let blockhash = Hash::default(); let blockhash_string = format!("{}", blockhash); let test_authorize = test_commands.clone().get_matches_from(vec![ "test", - &subcommand, + "stake-authorize", &stake_account_string, + "--new-stake-authority", &stake_account_string, "--blockhash", &blockhash_string, @@ -1573,9 +1795,7 @@ mod tests { CliCommandInfo { command: CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorized_pubkey: stake_account_pubkey, - stake_authorize, - authority: 0, + new_authorizations: vec![(StakeAuthorize::Staker, stake_account_pubkey, 0)], sign_only: true, blockhash_query: BlockhashQuery::None(blockhash), nonce_account: None, @@ -1592,8 +1812,9 @@ mod tests { let signer = format!("{}={}", keypair.pubkey(), sig); let test_authorize = test_commands.clone().get_matches_from(vec![ "test", - &subcommand, + "stake-authorize", &stake_account_string, + "--new-stake-authority", &stake_account_string, "--blockhash", &blockhash_string, @@ -1607,9 +1828,7 @@ mod tests { CliCommandInfo { command: CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorized_pubkey: stake_account_pubkey, - stake_authorize, - authority: 0, + new_authorizations: vec![(StakeAuthorize::Staker, stake_account_pubkey, 0)], sign_only: false, blockhash_query: BlockhashQuery::FeeCalculator( blockhash_query::Source::Cluster, @@ -1633,8 +1852,9 @@ mod tests { let nonce_account = Pubkey::new(&[1u8; 32]); let test_authorize = test_commands.clone().get_matches_from(vec![ "test", - &subcommand, + "stake-authorize", &stake_account_string, + "--new-stake-authority", &stake_account_string, "--blockhash", &blockhash_string, @@ -1654,9 +1874,7 @@ mod tests { CliCommandInfo { command: CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorized_pubkey: stake_account_pubkey, - stake_authorize, - authority: 0, + new_authorizations: vec![(StakeAuthorize::Staker, stake_account_pubkey, 0)], sign_only: false, blockhash_query: BlockhashQuery::FeeCalculator( blockhash_query::Source::NonceAccount(nonce_account), @@ -1676,8 +1894,9 @@ mod tests { // Test Authorize Subcommand w/ blockhash let test_authorize = test_commands.clone().get_matches_from(vec![ "test", - &subcommand, + "stake-authorize", &stake_account_string, + "--new-stake-authority", &stake_account_string, "--blockhash", &blockhash_string, @@ -1687,9 +1906,7 @@ mod tests { CliCommandInfo { command: CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorized_pubkey: stake_account_pubkey, - stake_authorize, - authority: 0, + new_authorizations: vec![(StakeAuthorize::Staker, stake_account_pubkey, 0)], sign_only: false, blockhash_query: BlockhashQuery::FeeCalculator( blockhash_query::Source::Cluster, @@ -1710,8 +1927,9 @@ mod tests { let nonce_account_string = nonce_account_pubkey.to_string(); let test_authorize = test_commands.clone().get_matches_from(vec![ "test", - &subcommand, + "stake-authorize", &stake_account_string, + "--new-stake-authority", &stake_account_string, "--blockhash", &blockhash_string, @@ -1725,9 +1943,7 @@ mod tests { CliCommandInfo { command: CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorized_pubkey: stake_account_pubkey, - stake_authorize, - authority: 0, + new_authorizations: vec![(StakeAuthorize::Staker, stake_account_pubkey, 0)], sign_only: false, blockhash_query: BlockhashQuery::FeeCalculator( blockhash_query::Source::NonceAccount(nonce_account_pubkey), @@ -1751,8 +1967,9 @@ mod tests { let fee_payer_string = fee_payer_pubkey.to_string(); let test_authorize = test_commands.clone().get_matches_from(vec![ "test", - &subcommand, + "stake-authorize", &stake_account_string, + "--new-stake-authority", &stake_account_string, "--fee-payer", &fee_payer_keypair_file, @@ -1762,9 +1979,7 @@ mod tests { CliCommandInfo { command: CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorized_pubkey: stake_account_pubkey, - stake_authorize, - authority: 0, + new_authorizations: vec![(StakeAuthorize::Staker, stake_account_pubkey, 0)], sign_only: false, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), nonce_account: None, @@ -1782,8 +1997,9 @@ mod tests { let signer = format!("{}={}", fee_payer_string, sig); let test_authorize = test_commands.clone().get_matches_from(vec![ "test", - &subcommand, + "stake-authorize", &stake_account_string, + "--new-stake-authority", &stake_account_string, "--fee-payer", &fee_payer_string, @@ -1797,9 +2013,7 @@ mod tests { CliCommandInfo { command: CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorized_pubkey: stake_account_pubkey, - stake_authorize, - authority: 0, + new_authorizations: vec![(StakeAuthorize::Staker, stake_account_pubkey, 0)], sign_only: false, blockhash_query: BlockhashQuery::FeeCalculator( blockhash_query::Source::Cluster, @@ -1815,34 +2029,6 @@ mod tests { ], } ); - } - - #[test] - fn test_parse_command() { - let test_commands = app("test", "desc", "version"); - let default_keypair = Keypair::new(); - let (default_keypair_file, mut tmp_file) = make_tmp_file(); - write_keypair(&default_keypair, tmp_file.as_file_mut()).unwrap(); - let (keypair_file, mut tmp_file) = make_tmp_file(); - let stake_account_keypair = Keypair::new(); - write_keypair(&stake_account_keypair, tmp_file.as_file_mut()).unwrap(); - let stake_account_pubkey = stake_account_keypair.pubkey(); - let (stake_authority_keypair_file, mut tmp_file) = make_tmp_file(); - let stake_authority_keypair = Keypair::new(); - write_keypair(&stake_authority_keypair, tmp_file.as_file_mut()).unwrap(); - - parse_authorize_tests( - &test_commands, - stake_account_pubkey, - &stake_authority_keypair_file, - StakeAuthorize::Staker, - ); - parse_authorize_tests( - &test_commands, - stake_account_pubkey, - &stake_authority_keypair_file, - StakeAuthorize::Withdrawer, - ); // Test CreateStakeAccount SubCommand let custodian = Pubkey::new_rand(); diff --git a/cli/src/storage.rs b/cli/src/storage.rs index d0ac19360d..816d7d0700 100644 --- a/cli/src/storage.rs +++ b/cli/src/storage.rs @@ -66,7 +66,7 @@ impl StorageSubCommands for App<'_, '_> { .arg( Arg::with_name("node_account_pubkey") .index(1) - .value_name("NODE_PUBKEY") + .value_name("NODE_ACCOUNT_ADDRESS") .takes_value(true) .required(true) .validator(is_valid_pubkey) @@ -75,7 +75,7 @@ impl StorageSubCommands for App<'_, '_> { .arg( Arg::with_name("storage_account_pubkey") .index(2) - .value_name("ACCOUNT_PUBKEY") + .value_name("STORAGE_ACCOUNT_ADDRESS") .takes_value(true) .required(true) .validator(is_valid_pubkey) @@ -89,11 +89,11 @@ impl StorageSubCommands for App<'_, '_> { .arg( Arg::with_name("storage_account_pubkey") .index(1) - .value_name("ACCOUNT_PUBKEY") + .value_name("STORAGE_ACCOUNT_ADDRESS") .takes_value(true) .required(true) .validator(is_valid_pubkey) - .help("Storage account pubkey"), + .help("Storage account address"), ), ) } diff --git a/cli/src/vote.rs b/cli/src/vote.rs index 0ae968b87a..2f85d38477 100644 --- a/cli/src/vote.rs +++ b/cli/src/vote.rs @@ -86,7 +86,7 @@ impl VoteSubCommands for App<'_, '_> { .arg( Arg::with_name("vote_account_pubkey") .index(1) - .value_name("ACCOUNT_PUBKEY") + .value_name("VOTE_ACCOUNT_ADDRESS") .takes_value(true) .required(true) .validator(is_valid_pubkey) @@ -108,7 +108,7 @@ impl VoteSubCommands for App<'_, '_> { .arg( Arg::with_name("vote_account_pubkey") .index(1) - .value_name("ACCOUNT_PUBKEY") + .value_name("VOTE_ACCOUNT_ADDRESS") .takes_value(true) .required(true) .validator(is_valid_pubkey) @@ -130,7 +130,7 @@ impl VoteSubCommands for App<'_, '_> { .arg( Arg::with_name("vote_account_pubkey") .index(1) - .value_name("ACCOUNT_PUBKEY") + .value_name("VOTE_ACCOUNT_ADDRESS") .takes_value(true) .required(true) .validator(is_valid_pubkey) @@ -170,7 +170,7 @@ impl VoteSubCommands for App<'_, '_> { .arg( Arg::with_name("vote_account_pubkey") .index(1) - .value_name("ACCOUNT_PUBKEY") + .value_name("VOTE_ACCOUNT_ADDRESS") .takes_value(true) .required(true) .validator(is_valid_pubkey) @@ -189,7 +189,7 @@ impl VoteSubCommands for App<'_, '_> { .arg( Arg::with_name("vote_account_pubkey") .index(1) - .value_name("ACCOUNT_PUBKEY") + .value_name("VOTE_ACCOUNT_ADDRESS") .takes_value(true) .required(true) .validator(is_valid_pubkey) @@ -198,7 +198,7 @@ impl VoteSubCommands for App<'_, '_> { .arg( Arg::with_name("destination_account_pubkey") .index(2) - .value_name("RECIPIENT_PUBKEY") + .value_name("RECIPIENT_ADDRESS") .takes_value(true) .required(true) .validator(is_valid_pubkey) diff --git a/cli/tests/stake.rs b/cli/tests/stake.rs index 6065c02109..808c444304 100644 --- a/cli/tests/stake.rs +++ b/cli/tests/stake.rs @@ -626,9 +626,7 @@ fn test_stake_authorize() { config.signers.pop(); config.command = CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorized_pubkey: online_authority_pubkey, - stake_authorize: StakeAuthorize::Staker, - authority: 0, + new_authorizations: vec![(StakeAuthorize::Staker, online_authority_pubkey, 0)], sign_only: false, blockhash_query: BlockhashQuery::default(), nonce_account: None, @@ -644,13 +642,40 @@ fn test_stake_authorize() { }; assert_eq!(current_authority, online_authority_pubkey); - // Assign new offline stake authority + // Assign new online stake and withdraw authorities + let online_authority2 = Keypair::new(); + let online_authority2_pubkey = online_authority2.pubkey(); + let withdraw_authority = Keypair::new(); + let withdraw_authority_pubkey = withdraw_authority.pubkey(); config.signers.push(&online_authority); config.command = CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorized_pubkey: offline_authority_pubkey, - stake_authorize: StakeAuthorize::Staker, - authority: 1, + new_authorizations: vec![ + (StakeAuthorize::Staker, online_authority2_pubkey, 1), + (StakeAuthorize::Withdrawer, withdraw_authority_pubkey, 0), + ], + sign_only: false, + blockhash_query: BlockhashQuery::default(), + nonce_account: None, + nonce_authority: 0, + fee_payer: 0, + }; + process_command(&config).unwrap(); + let stake_account = rpc_client.get_account(&stake_account_pubkey).unwrap(); + let stake_state: StakeState = stake_account.state().unwrap(); + let (current_staker, current_withdrawer) = match stake_state { + StakeState::Initialized(meta) => (meta.authorized.staker, meta.authorized.withdrawer), + _ => panic!("Unexpected stake state!"), + }; + assert_eq!(current_staker, online_authority2_pubkey); + assert_eq!(current_withdrawer, withdraw_authority_pubkey); + + // Assign new offline stake authority + config.signers.pop(); + config.signers.push(&online_authority2); + config.command = CliCommand::StakeAuthorize { + stake_account_pubkey, + new_authorizations: vec![(StakeAuthorize::Staker, offline_authority_pubkey, 1)], sign_only: false, blockhash_query: BlockhashQuery::default(), nonce_account: None, @@ -672,9 +697,7 @@ fn test_stake_authorize() { let (blockhash, _) = rpc_client.get_recent_blockhash().unwrap(); config_offline.command = CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorized_pubkey: nonced_authority_pubkey, - stake_authorize: StakeAuthorize::Staker, - authority: 0, + new_authorizations: vec![(StakeAuthorize::Staker, nonced_authority_pubkey, 0)], sign_only: true, blockhash_query: BlockhashQuery::None(blockhash), nonce_account: None, @@ -688,9 +711,7 @@ fn test_stake_authorize() { config.signers = vec![&offline_presigner]; config.command = CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorized_pubkey: nonced_authority_pubkey, - stake_authorize: StakeAuthorize::Staker, - authority: 0, + new_authorizations: vec![(StakeAuthorize::Staker, nonced_authority_pubkey, 0)], sign_only: false, blockhash_query: BlockhashQuery::FeeCalculator(blockhash_query::Source::Cluster, blockhash), nonce_account: None, @@ -732,9 +753,7 @@ fn test_stake_authorize() { config_offline.signers.push(&nonced_authority); config_offline.command = CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorized_pubkey: online_authority_pubkey, - stake_authorize: StakeAuthorize::Staker, - authority: 1, + new_authorizations: vec![(StakeAuthorize::Staker, online_authority_pubkey, 1)], sign_only: true, blockhash_query: BlockhashQuery::None(nonce_hash), nonce_account: Some(nonce_account.pubkey()), @@ -750,9 +769,7 @@ fn test_stake_authorize() { config.signers = vec![&offline_presigner, &nonced_authority_presigner]; config.command = CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorized_pubkey: online_authority_pubkey, - stake_authorize: StakeAuthorize::Staker, - authority: 1, + new_authorizations: vec![(StakeAuthorize::Staker, online_authority_pubkey, 1)], sign_only: false, blockhash_query: BlockhashQuery::FeeCalculator( blockhash_query::Source::NonceAccount(nonce_account.pubkey()), @@ -859,9 +876,7 @@ fn test_stake_authorize_with_fee_payer() { config.signers = vec![&default_signer, &payer_keypair]; config.command = CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorized_pubkey: offline_pubkey, - stake_authorize: StakeAuthorize::Staker, - authority: 0, + new_authorizations: vec![(StakeAuthorize::Staker, offline_pubkey, 0)], sign_only: false, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), nonce_account: None, @@ -879,9 +894,7 @@ fn test_stake_authorize_with_fee_payer() { let (blockhash, _) = rpc_client.get_recent_blockhash().unwrap(); config_offline.command = CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorized_pubkey: payer_pubkey, - stake_authorize: StakeAuthorize::Staker, - authority: 0, + new_authorizations: vec![(StakeAuthorize::Staker, payer_pubkey, 0)], sign_only: true, blockhash_query: BlockhashQuery::None(blockhash), nonce_account: None, @@ -895,9 +908,7 @@ fn test_stake_authorize_with_fee_payer() { config.signers = vec![&offline_presigner]; config.command = CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorized_pubkey: payer_pubkey, - stake_authorize: StakeAuthorize::Staker, - authority: 0, + new_authorizations: vec![(StakeAuthorize::Staker, payer_pubkey, 0)], sign_only: false, blockhash_query: BlockhashQuery::FeeCalculator(blockhash_query::Source::Cluster, blockhash), nonce_account: None, diff --git a/core/src/validator.rs b/core/src/validator.rs index fd718fb80f..0283e67cb5 100644 --- a/core/src/validator.rs +++ b/core/src/validator.rs @@ -78,6 +78,7 @@ pub struct ValidatorConfig { pub trusted_validators: Option>, // None = trust all pub halt_on_trusted_validators_accounts_hash_mismatch: bool, pub accounts_hash_fault_injection_slots: u64, // 0 = no fault injection + pub frozen_accounts: Vec, } impl Default for ValidatorConfig { @@ -103,6 +104,7 @@ impl Default for ValidatorConfig { trusted_validators: None, halt_on_trusted_validators_accounts_hash_mismatch: false, accounts_hash_fault_injection_slots: 0, + frozen_accounts: vec![], } } } @@ -594,6 +596,7 @@ fn new_banks_from_blockstore( poh_verify, dev_halt_at_slot: config.dev_halt_at_slot, new_hard_forks: config.new_hard_forks.clone(), + frozen_accounts: config.frozen_accounts.clone(), ..blockstore_processor::ProcessOptions::default() }; diff --git a/core/tests/bank_forks.rs b/core/tests/bank_forks.rs index f2af82adbe..5e1e3350ca 100644 --- a/core/tests/bank_forks.rs +++ b/core/tests/bank_forks.rs @@ -47,6 +47,7 @@ mod tests { let bank0 = Bank::new_with_paths( &genesis_config_info.genesis_config, vec![accounts_dir.path().to_path_buf()], + &[], ); bank0.freeze(); let mut bank_forks = BankForks::new(0, bank0); @@ -82,6 +83,7 @@ mod tests { let deserialized_bank = snapshot_utils::bank_from_archive( &account_paths, + &[], &old_bank_forks .snapshot_config .as_ref() diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 50ab932608..0a4ec07a47 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -10,6 +10,7 @@ * [Paper Wallet Usage](paper-wallet/usage.md) * [Generate Keys](cli/generate-keys.md) * [Send and Receive Tokens](cli/transfer-tokens.md) + * [Delegate Stake](cli/delegate-stake.md) * [Offline Signing](offline-signing/README.md) * [Durable Transaction Nonces](offline-signing/durable-nonce.md) * [Command-line Reference](cli/usage.md) diff --git a/docs/src/cli/.usage.md.header b/docs/src/cli/.usage.md.header index f9e23e4698..5e1267d613 100644 --- a/docs/src/cli/.usage.md.header +++ b/docs/src/cli/.usage.md.header @@ -8,7 +8,7 @@ The [solana-cli crate](https://crates.io/crates/solana-cli) provides a command-l ```bash // Command -$ solana address +$ solana-keygen pubkey // Return diff --git a/docs/src/cli/README.md b/docs/src/cli/README.md index d56fac2d73..7985c13d44 100644 --- a/docs/src/cli/README.md +++ b/docs/src/cli/README.md @@ -1,5 +1,9 @@ # Command-line Guide -This section describes the command-line tools for interacting with Solana. One -could use these tools to send payments, stake validators, and check account -balances. +In this section, we will describe how to create a Solana *wallet*, how to send +and receive tokens, and how to participate in the cluster by delegating stake. +To interact with a Solana cluster, we will use its command-line interface, also +known as the CLI. We use the command-line because it is the first place the +Solana core team deploys new functionality. The command-line interface is not +necessarily the easiest to use, but it provides the most direct, flexible, and +secure access to your Solana accounts. diff --git a/docs/src/cli/choose-a-wallet.md b/docs/src/cli/choose-a-wallet.md index 7681268dc9..581f38454a 100644 --- a/docs/src/cli/choose-a-wallet.md +++ b/docs/src/cli/choose-a-wallet.md @@ -3,7 +3,7 @@ Keypairs are stored in *wallets* and wallets come in many forms. A wallet might be a directory in your computer's file system, a piece of paper, or a specialized device called a *hardware wallet*. Some wallets are easier to use -than others. Some are more secure than others. In this section, we'll compare +than others. Some are more secure than others. In this section, we will compare and contrast different types of wallets and help you choose the wallet that best fits your needs. @@ -16,8 +16,8 @@ file system. Each file in the directory holds a keypair. An FS wallet is the most convenient and least secure form of wallet. It is convenient because the keypair is stored in a simple file. You can generate as -many keys as you'd like and trivially back them up by copying the files. It is -insecure because the keypair files are **unencrypted**. If you are the only +many keys as you would like and trivially back them up by copying the files. It +is insecure because the keypair files are **unencrypted**. If you are the only user of your computer and you are confident it is free of malware, an FS wallet is a fine solution for small amounts of cryptocurrency. If, however, your computer contains malware and is connected to the Internet, that malware may @@ -72,8 +72,8 @@ To keep your hardware wallet tokens safe, we suggest: ## Which Wallet is Best? -Different people will have different needs, but if you are still unsure what's -best for you after reading the comparisons above, go with a +Different people will have different needs, but if you are still unsure what +is best for you after reading the comparisons above, go with a [Ledger Nano S](https://shop.ledger.com/products/ledger-nano-s). The [Nano S is well-integrated into Solana's tool suite](../remote-wallet/ledger) and offers an outstanding blend of security and convenience. diff --git a/docs/src/cli/delegate-stake.md b/docs/src/cli/delegate-stake.md new file mode 100644 index 0000000000..c8de7c0333 --- /dev/null +++ b/docs/src/cli/delegate-stake.md @@ -0,0 +1,197 @@ +# Delegate Stake + +After you have [received SOL](transfer-tokens.md), you might consider putting +it to use by delegating *stake* to a validator. Stake is what we call tokens +in a *stake account*. Solana weights validator votes by the amount of stake +delegated to them, which gives those validators more influence in determining +then next valid block of transactions in the blockchain. Solana then generates +new SOL periodically to reward stakers and validators. You earn more rewards +the more stake you delegate. + +## Create a Stake Account + +To delegate stake, you will need to transfer some tokens into a stake account. +To create an account, you will need a keypair. Its public key will be used as +the stake account address. No need for a password or encryption here; this +keypair will be discarded right after creating the stake account. + +```bash +solana-keygen new --no-passphrase -o stake-account.json +``` + +The output will contain the public key after the text `pubkey:`. + +```text +============================================================================ +pubkey: GKvqsuNcnwWqPzzuhLmGi4rzzh55FhJtGizkhHaEJqiV +============================================================================ +``` + +Copy the public key and store it for safekeeping. You will need it any time you +want to perform an action on the stake account you create next. + +Now, create a stake account: + +```bash +solana create-stake-account --from= stake-account.json --stake-authority= --withdraw-authority= +``` + +`` tokens are transferred from the account at `` to a new +stake account at the public key of stake-account.json. + +The stake-account.json file can now be discarded. To authorize additional +actions, you will use the `--stake-authority` or `withdraw-authority` keypair, +not stake-account.json. + +View the new stake account with the `solana stake-account` command: + +```bash +solana stake-account +``` + +The output will look similar to this: + +```text +Total Stake: 5000 SOL +Stake account is undelegated +Stake Authority: EXU95vqs93yPeCeAU7mPPu6HbRUmTFPEiGug9oCdvQ5F +Withdraw Authority: EXU95vqs93yPeCeAU7mPPu6HbRUmTFPEiGug9oCdvQ5F +``` + +### Set Stake and Withdraw Authorities + +Staking commands look to keypairs to authorize certain stake account +operations. They use the stake authority to authorize stake delegation, +deactivating stake, splitting stake, and setting a new stake authority. They +use the withdraw authority to authorize withdrawing stake, and when setting +either a new stake or withdraw authority. + +Stake and withdraw authorities can be set when creating an account via the +`--stake-authority` and `--withdraw-authority` options, or afterward with the +`solana stake-authorize` command. For example, to set a new stake authority, +run: + +```bash +solana stake-authorize --stake-authority= --new-stake-authority= +``` + +This will use the existing stake authority `` to authorize a new stake +authority `` on the stake account ``. + +### Advanced: Derive Stake Account Addresses + +When you delegate stake, you delegate all tokens in the stake account to a +single validator. To delegate to multiple validators, you will need multiple +stake accounts. Creating a new keypair for each account and managing those +addresses can be cumbersome. Fortunately, you can derive stake addresses using +the `--seed` option: + +```bash +solana create-stake-account --from= --seed= --stake-authority= --withdraw-authority= +``` + +`` is an arbitrary string up to 32 bytes, but will typically be a +number corresponding to which derived account this is. The first account might +be "0", then "1", and so on. The public key of `` acts +as the base address. The command derives a new address from the base address +and seed string. To see what stake address the command will derive, use `solana +create-address-with-seed`: + +```bash +solana create-address-with-seed --from= STAKE +``` + +`` is the public key of the `` passed to +`solana create-stake-account`. + +The command will output a derived address, which can be used for the +`` argument in staking operations. + +## Delegate Stake + +To delegate your stake to a validator, you will need its vote account address. +Find it by querying the cluster for the list of all validators and their vote +accounts with the `solana validators` command: + +```bash +solana validators +``` + +The first column of each row contains the validator's identity and the second +is the vote account address. Choose a validator and use its vote account +address in `solana delegate-stake`: + +```bash +solana delegate-stake --stake-authority= +``` + +`` authorizes the operation on the account with address +``. The stake is delegated to the vote account with +address ``. + +After delegating stake, use `solana stake-account` to observe the changes +to the stake account: + +```bash +solana stake-account +``` + +You will see new fields "Delegated Stake" and "Delegated Vote Account Address" +in the output. The output will look similar to this: + +```text +Total Stake: 5000 SOL +Credits Observed: 147462 +Delegated Stake: 4999.99771712 SOL +Delegated Vote Account Address: CcaHc2L43ZWjwCHART3oZoJvHLAe9hzT2DJNUpBzoTN1 +Stake activates starting from epoch: 42 +Stake Authority: EXU95vqs93yPeCeAU7mPPu6HbRUmTFPEiGug9oCdvQ5F +Withdraw Authority: EXU95vqs93yPeCeAU7mPPu6HbRUmTFPEiGug9oCdvQ5F +``` + +## Deactivate Stake + +Once delegated, you can undelegate stake with the `solana deactivate-stake` +command: + +```bash +solana deactivate-stake --stake-authority= +``` + +`` authorizes the operation on the account with address +``. + +Note that stake takes several epochs to "cool down". Attempts to delegate stake +in the cool down period will fail. + +## Withdraw Stake + +Transfer tokens out of a stake account with the `solana withdraw-stake` command: + +```bash +solana withdraw-stake --withdraw-authority= +``` + +`` is the existing stake account, `` is the +withdraw authority, and `` is the number of tokens to transfer to +``. + +## Split Stake + +You may want to delegate stake to additional validators while your existing +stake is not eligible for withdrawal. It might not be eligible because it is +currently staked, cooling down, or locked up. To transfer tokens from an +existing stake account to a new one, use the `solana split-stake` command: + +```bash +solana split-stake --stake-authority= +``` + +`` is the existing stake account, `` is the +stake authority, `` is the keypair for the new account, +and `` is the number of tokens to transfer to the new account. + +To split a stake account into a derived account address, use the `--seed` +option. See +[Derive Stake Account Addresses](#advanced-derive-stake-account-addresses) +for details. diff --git a/docs/src/cli/generate-keys.md b/docs/src/cli/generate-keys.md index 5f9b0bfffa..fab39e6938 100644 --- a/docs/src/cli/generate-keys.md +++ b/docs/src/cli/generate-keys.md @@ -1,7 +1,7 @@ # Generate a Keypair and its Public Key -In this section, we'll generate a keypair, query it for its public key, -and verify you control its private key. Before you begin, you'll need +In this section, we will generate a keypair, query it for its public key, +and verify you control its private key. Before you begin, you will need to: * [Install the Solana Tool Suite](../install-solana.md) diff --git a/docs/src/cli/transfer-tokens.md b/docs/src/cli/transfer-tokens.md index 580ebb3c1e..eb86893d06 100644 --- a/docs/src/cli/transfer-tokens.md +++ b/docs/src/cli/transfer-tokens.md @@ -6,7 +6,7 @@ To receive tokens, you will need an address for others to send tokens to. In Solana, an address is the public key of a keypair. There are a variety of techniques for generating keypairs. The method you choose will depend on how you choose to store keypairs. Keypairs are stored in wallets. Before receiving -tokens, you'll need to [choose a wallet](choose-a-wallet.md) and +tokens, you will need to [choose a wallet](choose-a-wallet.md) and [generate keys](generate-keys.md). Once completed, you should have a public key for each keypair you generated. The public key is a long string of base58 characters. Its length varies from 32 to 44 characters. @@ -32,14 +32,14 @@ where you replace the text `` with the name of the command you want to learn more about. The command's usage message will typically contain words such as ``, -`` or ``. Each word is a placeholder for the *type* of text -you can execute the command with. For example, you can replace `` -with a number such as `42` or `100.42`. You can replace `` with the -base58 encoding of your public key. For ``, it depends on what type +`` or ``. Each word is a placeholder for the *type* of +text you can execute the command with. For example, you can replace `` +with a number such as `42` or `100.42`. You can replace `` with +the base58 encoding of your public key. For ``, it depends on what type of wallet you chose. If you chose an fs wallet, that path might be `~/my-solana-wallet/my-keypair.json`. If you chose a paper wallet, use the keyword `ASK`, and the Solana CLI will prompt you for your seed phrase. If -you chose a hardware wallet, use your USB URL, such as `usb://ledger?key=0`. +you chose a hardware wallet, use your keypair URL, such as `usb://ledger?key=0`. ### Test-drive your Public Keys @@ -50,23 +50,24 @@ Try and *airdrop* yourself some play tokens on the developer testnet, called Devnet: ```bash -solana airdrop 10 --url http://devnet.solana.com +solana airdrop 10 --url http://devnet.solana.com ``` -where you replace the text `` with your base58 public key. +where you replace the text `` with your base58-encoded +public key. Confirm the airdrop was successful by checking the account's balance. It should output `10 SOL`: ```bash -solana balance --url http://devnet.solana.com +solana balance --url http://devnet.solana.com ``` Next, prove that you own those tokens by transferring them. The Solana cluster will only accept the transfer if you sign the transaction with the private key corresponding to the sender's public key in the transaction. -First, we'll need a public key to receive our tokens. Create a second +First, we will need a public key to receive our tokens. Create a second keypair and record its pubkey: ```bash @@ -74,7 +75,7 @@ solana-keygen new --no-passphrase --no-outfile ``` The output will contain the public key after the text `pubkey:`. Copy the -public key. We'll use it in the next step. +public key. We will use it in the next step. ```text ============================================================================ @@ -83,19 +84,19 @@ pubkey: GKvqsuNcnwWqPzzuhLmGi4rzzh55FhJtGizkhHaEJqiV ``` ```bash -solana transfer --keypair= 5 --url http://devnet.solana.com +solana transfer --from= 5 --url http://devnet.solana.com ``` where you replace `` with the path to a keypair in your wallet, -and replace `` with the output of `solana-keygen new` above. +and replace `` with the output of `solana-keygen new` above. Confirm the updated balances with `solana balance`: ```bash -solana balance --url http://devnet.solana.com +solana balance --url http://devnet.solana.com ``` -where `` is either the public key from your keypair or the +where `` is either the public key from your keypair or the recipient's public key. ## Send Tokens @@ -106,11 +107,11 @@ tokens to transfer. Once you have that collected, you can transfer tokens with the `solana transfer` command: ```bash -solana transfer --keypair= +solana transfer --from= ``` Confirm the updated balances with `solana balance`: ```bash -solana balance +solana balance ``` diff --git a/docs/src/offline-signing/README.md b/docs/src/offline-signing/README.md index 4e90cd1768..e5a78f86fa 100644 --- a/docs/src/offline-signing/README.md +++ b/docs/src/offline-signing/README.md @@ -18,8 +18,7 @@ At present, the following commands support offline signing: * [`deactivate-stake`](../cli/usage.md#solana-deactivate-stake) * [`delegate-stake`](../cli/usage.md#solana-delegate-stake) * [`split-stake`](../cli/usage.md#solana-split-stake) -* [`stake-authorize-staker`](../cli/usage.md#solana-stake-authorize-staker) -* [`stake-authorize-withdrawer`](../cli/usage.md#solana-stake-authorize-withdrawer) +* [`stake-authorize`](../cli/usage.md#solana-stake-authorize) * [`stake-set-lockup`](../cli/usage.md#solana-stake-set-lockup) * [`transfer`](../cli/usage.md#solana-transfer) * [`withdraw-stake`](../cli/usage.md#solana-withdraw-stake) diff --git a/docs/src/remote-wallet/README.md b/docs/src/remote-wallet/README.md index 017a786ad5..3c661a0f27 100644 --- a/docs/src/remote-wallet/README.md +++ b/docs/src/remote-wallet/README.md @@ -52,7 +52,7 @@ usb://ledger/BsNsvfXqQTtJnagwFWdBS7FBXgnsK8VZ5CmuznN85swK?key=0/0 ## Manage Multiple Hardware Wallets It is sometimes useful to sign a transaction with keys from multiple hardware -wallets. Signing with multiple wallets requires *fully qualified USB URLs*. +wallets. Signing with multiple wallets requires *fully qualified keypair URLs*. When the URL is not fully qualified, the Solana CLI will prompt you with the fully qualified URLs of all connected hardware wallets, and ask you to choose which wallet to use for each signature. diff --git a/docs/src/remote-wallet/ledger.md b/docs/src/remote-wallet/ledger.md index a34066de70..a946a863ce 100644 --- a/docs/src/remote-wallet/ledger.md +++ b/docs/src/remote-wallet/ledger.md @@ -72,81 +72,29 @@ To fix, check the following: 3. On your computer, run: ```text -solana address --keypair usb://ledger +solana-keygen pubkey usb://ledger ``` This confirms your Ledger device is connected properly and in the correct state to interact with the Solana CLI. The command returns your Ledger's unique -*wallet key*. When you have multiple Nano S devices connected to the same +*wallet ID*. When you have multiple Nano S devices connected to the same computer, you can use your wallet key to specify which Ledger hardware wallet you want to use. Run the same command again, but this time, with its fully qualified URL: ```text -solana address --keypair usb://ledger/ +solana-keygen pubkey usb://ledger/ ``` -Confirm it prints the same key as when you entered just `usb://ledger`. +where you replace `` with the output of the first command. +Confirm it prints the same wallet ID as before. -To learn more about USB URLs, see +To learn more about keypair URLs, see [Specify A Hardware Wallet Key](index.md#specify-a-hardware-wallet-key) -### Set CLI Configuration - -Configure the `solana` CLI tool to connect to a particular cluster: - -```bash -solana config set --url # (i.e. http://devnet.solana.com) -``` - -If you want to set a Ledger key as the default signer for CLI commands, use the -[CLI configuration settings](../cli/usage.md#solana-config): - -```text -solana config set --keypair -``` - -For example: - -```text -solana config set --keypair usb://ledger?key=0 -``` - -### Check Account Balance - -```text -solana balance --keypair usb://ledger?key=12345 -``` - -Or with the default signer: - -```text -solana balance -``` - -### Send SOL via Ledger Device - -```text -solana transfer --from -``` - -Or with the default signer: - -```text -solana transfer -``` - -### Delegate Stake with Ledger Device - -```text -solana delegate-stake --keypair -``` - -Or with the default signer: - -```text -solana delegate-stake -``` +Read more about [sending and receiving tokens](../transfer-tokens.md) and +[delegating stake](../delegate-stake.md). You can use your Ledger keypair URL +anywhere you see an option or argument that accepts a ``. ## Support diff --git a/ledger-tool/src/main.rs b/ledger-tool/src/main.rs index db40118692..609eb824b7 100644 --- a/ledger-tool/src/main.rs +++ b/ledger-tool/src/main.rs @@ -35,14 +35,15 @@ enum LedgerOutputMethod { Json, } -fn output_slot(blockstore: &Blockstore, slot: Slot, method: &LedgerOutputMethod) { +fn output_slot( + blockstore: &Blockstore, + slot: Slot, + method: &LedgerOutputMethod, +) -> Result<(), String> { println!("Slot Meta {:?}", blockstore.meta(slot)); let entries = blockstore .get_slot_entries(slot, 0, None) - .unwrap_or_else(|err| { - eprintln!("Failed to load entries for slot {}: {:?}", slot, err); - exit(1); - }); + .map_err(|err| format!("Failed to load entries for slot {}: {}", slot, err))?; for (entry_index, entry) in entries.iter().enumerate() { match method { @@ -115,6 +116,7 @@ fn output_slot(blockstore: &Blockstore, slot: Slot, method: &LedgerOutputMethod) } } } + Ok(()) } fn output_ledger(blockstore: Blockstore, starting_slot: Slot, method: LedgerOutputMethod) { @@ -140,7 +142,9 @@ fn output_ledger(blockstore: Blockstore, starting_slot: Slot, method: LedgerOutp } } - output_slot(&blockstore, slot, &method); + if let Err(err) = output_slot(&blockstore, slot, &method) { + eprintln!("{}", err); + } } if method == LedgerOutputMethod::Json { @@ -616,7 +620,21 @@ fn main() { .takes_value(true) .multiple(true) .required(true) - .help("List of slots to print"), + .help("Slots to print"), + ) + ) + .subcommand( + SubCommand::with_name("set-dead-slot") + .about("Mark one or more slots dead") + .arg( + Arg::with_name("slots") + .index(1) + .value_name("SLOTS") + .validator(is_slot) + .takes_value(true) + .multiple(true) + .required(true) + .help("Slots to mark dead"), ) ) .subcommand( @@ -824,13 +842,12 @@ fn main() { } ("slot", Some(arg_matches)) => { let slots = values_t_or_exit!(arg_matches, "slots", Slot); + let blockstore = open_blockstore(&ledger_path); for slot in slots { println!("Slot {}", slot); - output_slot( - &open_blockstore(&ledger_path), - slot, - &LedgerOutputMethod::Print, - ); + if let Err(err) = output_slot(&blockstore, slot, &LedgerOutputMethod::Print) { + eprintln!("{}", err); + } } } ("json", Some(arg_matches)) => { @@ -841,6 +858,16 @@ fn main() { LedgerOutputMethod::Json, ); } + ("set-dead-slot", Some(arg_matches)) => { + let slots = values_t_or_exit!(arg_matches, "slots", Slot); + let blockstore = open_blockstore(&ledger_path); + for slot in slots { + match blockstore.set_dead_slot(slot) { + Ok(_) => println!("Slot {} dead", slot), + Err(err) => eprintln!("Failed to set slot {} dead slot: {}", slot, err), + } + } + } ("verify", Some(arg_matches)) => { let process_options = ProcessOptions { dev_halt_at_slot: value_t!(arg_matches, "halt_at_slot", Slot).ok(), diff --git a/ledger/src/bank_forks_utils.rs b/ledger/src/bank_forks_utils.rs index a32227840c..109a32ee49 100644 --- a/ledger/src/bank_forks_utils.rs +++ b/ledger/src/bank_forks_utils.rs @@ -66,6 +66,7 @@ pub fn load( let deserialized_bank = snapshot_utils::bank_from_archive( &account_paths, + &process_options.frozen_accounts, &snapshot_config.snapshot_path, &archive_filename, ) diff --git a/ledger/src/blockstore_processor.rs b/ledger/src/blockstore_processor.rs index 9d8571bf11..2d03d4a52e 100644 --- a/ledger/src/blockstore_processor.rs +++ b/ledger/src/blockstore_processor.rs @@ -23,6 +23,7 @@ use solana_sdk::{ clock::{Slot, MAX_RECENT_BLOCKHASHES}, genesis_config::GenesisConfig, hash::Hash, + pubkey::Pubkey, signature::Keypair, timing::duration_as_ms, transaction::{Result, Transaction, TransactionError}, @@ -271,6 +272,7 @@ pub struct ProcessOptions { pub entry_callback: Option, pub override_num_threads: Option, pub new_hard_forks: Option>, + pub frozen_accounts: Vec, } pub fn process_blockstore( @@ -289,7 +291,11 @@ pub fn process_blockstore( } // Setup bank for slot 0 - let bank0 = Arc::new(Bank::new_with_paths(&genesis_config, account_paths)); + let bank0 = Arc::new(Bank::new_with_paths( + &genesis_config, + account_paths, + &opts.frozen_accounts, + )); info!("processing ledger for slot 0..."); let recyclers = VerifyRecyclers::default(); process_bank_0(&bank0, blockstore, &opts, &recyclers)?; @@ -2611,7 +2617,7 @@ pub mod tests { genesis_config: &GenesisConfig, account_paths: Vec, ) -> EpochSchedule { - let bank = Bank::new_with_paths(&genesis_config, account_paths); + let bank = Bank::new_with_paths(&genesis_config, account_paths, &[]); bank.epoch_schedule().clone() } diff --git a/ledger/src/snapshot_utils.rs b/ledger/src/snapshot_utils.rs index 6f4b19fb62..5564feaf4e 100644 --- a/ledger/src/snapshot_utils.rs +++ b/ledger/src/snapshot_utils.rs @@ -12,7 +12,7 @@ use solana_runtime::{ MAX_SNAPSHOT_DATA_FILE_SIZE, }, }; -use solana_sdk::{clock::Slot, hash::Hash}; +use solana_sdk::{clock::Slot, hash::Hash, pubkey::Pubkey}; use std::{ cmp::Ordering, env, @@ -432,6 +432,7 @@ pub fn remove_snapshot>(slot: Slot, snapshot_path: P) -> Result<( pub fn bank_from_archive>( account_paths: &[PathBuf], + frozen_account_pubkeys: &[Pubkey], snapshot_path: &PathBuf, snapshot_tar: P, ) -> Result { @@ -450,6 +451,7 @@ pub fn bank_from_archive>( let bank = rebuild_bank_from_snapshots( snapshot_version.trim(), account_paths, + frozen_account_pubkeys, &unpacked_snapshots_dir, unpacked_accounts_dir, )?; @@ -568,6 +570,7 @@ pub fn untar_snapshot_in, Q: AsRef>( fn rebuild_bank_from_snapshots

( snapshot_version: &str, account_paths: &[PathBuf], + frozen_account_pubkeys: &[Pubkey], unpacked_snapshots_dir: &PathBuf, append_vecs_path: P, ) -> Result @@ -599,12 +602,16 @@ where } }; info!("Rebuilding accounts..."); - bank.set_bank_rc( - bank::BankRc::new(account_paths.to_vec(), 0, bank.slot()), - bank::StatusCacheRc::default(), - ); - bank.rc - .accounts_from_stream(stream.by_ref(), account_paths, &append_vecs_path)?; + let rc = bank::BankRc::from_stream( + account_paths, + bank.slot(), + &bank.ancestors, + frozen_account_pubkeys, + stream.by_ref(), + &append_vecs_path, + )?; + + bank.set_bank_rc(rc, bank::StatusCacheRc::default()); Ok(bank) }, )?; diff --git a/local-cluster/src/local_cluster.rs b/local-cluster/src/local_cluster.rs index 020ecf39de..208ca537c1 100644 --- a/local-cluster/src/local_cluster.rs +++ b/local-cluster/src/local_cluster.rs @@ -298,7 +298,7 @@ impl LocalCluster { self.exit(); for (_, node) in self.validators.iter_mut() { if let Some(v) = node.validator.take() { - v.join().unwrap(); + v.join().expect("Validator join failed"); } } diff --git a/local-cluster/tests/local_cluster.rs b/local-cluster/tests/local_cluster.rs index 2b22c4bd85..2b8faa6590 100644 --- a/local-cluster/tests/local_cluster.rs +++ b/local-cluster/tests/local_cluster.rs @@ -17,13 +17,14 @@ use solana_local_cluster::{ local_cluster::{ClusterConfig, LocalCluster}, }; use solana_sdk::{ - client::SyncClient, + client::{AsyncClient, SyncClient}, clock::{self, Slot}, commitment_config::CommitmentConfig, epoch_schedule::{EpochSchedule, MINIMUM_SLOTS_PER_EPOCH}, genesis_config::OperatingMode, hash::Hash, poh_config::PohConfig, + pubkey::Pubkey, signature::{Keypair, Signer}, }; use std::sync::atomic::{AtomicBool, Ordering}; @@ -547,7 +548,7 @@ fn test_listener_startup() { #[test] #[serial] -fn test_softlaunch_operating_mode() { +fn test_stable_operating_mode() { solana_logger::setup(); let config = ClusterConfig { @@ -566,7 +567,7 @@ fn test_softlaunch_operating_mode() { solana_core::cluster_info::VALIDATOR_PORT_RANGE, ); - // Programs that are available at soft launch epoch 0 + // Programs that are available at epoch 0 for program_id in [ &solana_config_program::id(), &solana_sdk::system_program::id(), @@ -586,7 +587,7 @@ fn test_softlaunch_operating_mode() { ); } - // Programs that are not available at soft launch epoch 0 + // Programs that are not available at epoch 0 for program_id in [ &solana_sdk::bpf_loader::id(), &solana_storage_program::id(), @@ -606,6 +607,110 @@ fn test_softlaunch_operating_mode() { } } +fn generate_frozen_account_panic(mut cluster: LocalCluster, frozen_account: Arc) { + let client = cluster + .get_validator_client(&frozen_account.pubkey()) + .unwrap(); + + // Check the validator is alive by poking it over RPC + trace!( + "validator slot: {}", + client + .get_slot_with_commitment(CommitmentConfig::recent()) + .expect("get slot") + ); + + // Reset the frozen account panic signal + solana_runtime::accounts_db::FROZEN_ACCOUNT_PANIC.store(false, Ordering::Relaxed); + + // Wait for the frozen account panic signal + let mut i = 0; + while !solana_runtime::accounts_db::FROZEN_ACCOUNT_PANIC.load(Ordering::Relaxed) { + // Transfer from frozen account + let (blockhash, _fee_calculator) = client + .get_recent_blockhash_with_commitment(CommitmentConfig::recent()) + .unwrap(); + client + .async_transfer(1, &frozen_account, &Pubkey::new_rand(), blockhash) + .unwrap(); + + sleep(Duration::from_secs(1)); + i += 1; + if i > 10 { + panic!("FROZEN_ACCOUNT_PANIC still false"); + } + } + + // The validator is now broken and won't shutdown properly. Avoid LocalCluster panic in Drop + // with some manual cleanup: + cluster.exit(); + cluster.validators = HashMap::default(); +} + +#[test] +#[serial] +fn test_frozen_account_from_genesis() { + solana_logger::setup(); + let validator_identity = + Arc::new(solana_sdk::signature::keypair_from_seed(&[0u8; 32]).unwrap()); + + let config = ClusterConfig { + validator_keys: Some(vec![validator_identity.clone()]), + node_stakes: vec![100; 1], + cluster_lamports: 1_000, + validator_configs: vec![ + ValidatorConfig { + // Freeze the validator identity account + frozen_accounts: vec![validator_identity.pubkey()], + ..ValidatorConfig::default() + }; + 1 + ], + ..ClusterConfig::default() + }; + generate_frozen_account_panic(LocalCluster::new(&config), validator_identity); +} + +#[test] +#[serial] +fn test_frozen_account_from_snapshot() { + solana_logger::setup(); + let validator_identity = + Arc::new(solana_sdk::signature::keypair_from_seed(&[0u8; 32]).unwrap()); + + let mut snapshot_test_config = setup_snapshot_validator_config(5, 1); + // Freeze the validator identity account + snapshot_test_config.validator_config.frozen_accounts = vec![validator_identity.pubkey()]; + + let config = ClusterConfig { + validator_keys: Some(vec![validator_identity.clone()]), + node_stakes: vec![100; 1], + cluster_lamports: 1_000, + validator_configs: vec![snapshot_test_config.validator_config.clone()], + ..ClusterConfig::default() + }; + let mut cluster = LocalCluster::new(&config); + + let snapshot_package_output_path = &snapshot_test_config + .validator_config + .snapshot_config + .as_ref() + .unwrap() + .snapshot_package_output_path; + + trace!("Waiting for snapshot at {:?}", snapshot_package_output_path); + let (archive_filename, _archive_snapshot_hash) = + wait_for_next_snapshot(&cluster, &snapshot_package_output_path); + + trace!("Found snapshot: {:?}", archive_filename); + + // Restart the validator from a snapshot + let validator_info = cluster.exit_node(&validator_identity.pubkey()); + cluster.restart_node(&validator_identity.pubkey(), validator_info); + + generate_frozen_account_panic(cluster, validator_identity); +} + #[test] #[serial] fn test_consistency_halt() { diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index ade58f92ee..2957109824 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -15,6 +15,7 @@ byteorder = "1.3.2" fnv = "1.0.6" fs_extra = "1.1.0" itertools = "0.8.2" +lazy_static = "1.4.0" libc = "0.2.66" libloading = "0.5.2" log = "0.4.8" diff --git a/runtime/benches/accounts.rs b/runtime/benches/accounts.rs index 56018132b2..19b53beeeb 100644 --- a/runtime/benches/accounts.rs +++ b/runtime/benches/accounts.rs @@ -32,7 +32,7 @@ fn bench_has_duplicates(bencher: &mut Bencher) { #[bench] fn test_accounts_create(bencher: &mut Bencher) { let (genesis_config, _) = create_genesis_config(10_000); - let bank0 = Bank::new_with_paths(&genesis_config, vec![PathBuf::from("bench_a0")]); + let bank0 = Bank::new_with_paths(&genesis_config, vec![PathBuf::from("bench_a0")], &[]); bencher.iter(|| { let mut pubkeys: Vec = vec![]; deposit_many(&bank0, &mut pubkeys, 1000); @@ -46,6 +46,7 @@ fn test_accounts_squash(bencher: &mut Bencher) { banks.push(Arc::new(Bank::new_with_paths( &genesis_config, vec![PathBuf::from("bench_a1")], + &[], ))); let mut pubkeys: Vec = vec![]; deposit_many(&banks[0], &mut pubkeys, 250000); diff --git a/runtime/src/accounts.rs b/runtime/src/accounts.rs index c5e52bcde8..e7a867506d 100644 --- a/runtime/src/accounts.rs +++ b/runtime/src/accounts.rs @@ -59,15 +59,9 @@ pub type TransactionLoadResult = (TransactionAccounts, TransactionLoaders, Trans impl Accounts { pub fn new(paths: Vec) -> Self { - let accounts_db = Arc::new(AccountsDB::new(paths)); - - Accounts { - slot: 0, - accounts_db, - account_locks: Mutex::new(HashSet::new()), - readonly_locks: Arc::new(RwLock::new(Some(HashMap::new()))), - } + Self::new_with_frozen_accounts(paths, &HashMap::default(), &[]) } + pub fn new_from_parent(parent: &Accounts, slot: Slot, parent_slot: Slot) -> Self { let accounts_db = parent.accounts_db.clone(); accounts_db.set_hash(slot, parent_slot); @@ -79,14 +73,48 @@ impl Accounts { } } - pub fn accounts_from_stream>( - &self, + pub fn new_with_frozen_accounts( + paths: Vec, + ancestors: &HashMap, + frozen_account_pubkeys: &[Pubkey], + ) -> Self { + let mut accounts = Accounts { + slot: 0, + accounts_db: Arc::new(AccountsDB::new(paths)), + account_locks: Mutex::new(HashSet::new()), + readonly_locks: Arc::new(RwLock::new(Some(HashMap::new()))), + }; + accounts.freeze_accounts(ancestors, frozen_account_pubkeys); + accounts + } + + pub fn freeze_accounts( + &mut self, + ancestors: &HashMap, + frozen_account_pubkeys: &[Pubkey], + ) { + Arc::get_mut(&mut self.accounts_db) + .unwrap() + .freeze_accounts(ancestors, frozen_account_pubkeys); + } + + pub fn from_stream>( + account_paths: &[PathBuf], + ancestors: &HashMap, + frozen_account_pubkeys: &[Pubkey], stream: &mut BufReader, - local_paths: &[PathBuf], - append_vecs_path: P, - ) -> std::result::Result<(), IOError> { - self.accounts_db - .accounts_from_stream(stream, local_paths, append_vecs_path) + stream_append_vecs_path: P, + ) -> std::result::Result { + let mut accounts_db = AccountsDB::new(account_paths.to_vec()); + accounts_db.accounts_from_stream(stream, stream_append_vecs_path)?; + accounts_db.freeze_accounts(ancestors, frozen_account_pubkeys); + + Ok(Accounts { + slot: 0, + accounts_db: Arc::new(accounts_db), + account_locks: Mutex::new(HashSet::new()), + readonly_locks: Arc::new(RwLock::new(Some(HashMap::new()))), + }) } /// Return true if the slice has any duplicate elements @@ -1393,10 +1421,14 @@ mod tests { let buf = writer.into_inner(); let mut reader = BufReader::new(&buf[..]); let (_accounts_dir, daccounts_paths) = get_temp_accounts_paths(2).unwrap(); - let daccounts = Accounts::new(daccounts_paths.clone()); - assert!(daccounts - .accounts_from_stream(&mut reader, &daccounts_paths, copied_accounts.path()) - .is_ok()); + let daccounts = Accounts::from_stream( + &daccounts_paths, + &HashMap::default(), + &[], + &mut reader, + copied_accounts.path(), + ) + .unwrap(); check_accounts(&daccounts, &pubkeys, 100); assert_eq!(accounts.bank_hash_at(0), daccounts.bank_hash_at(0)); } diff --git a/runtime/src/accounts_db.rs b/runtime/src/accounts_db.rs index f4bedc2023..60e15a1394 100644 --- a/runtime/src/accounts_db.rs +++ b/runtime/src/accounts_db.rs @@ -26,6 +26,7 @@ use crate::{ use bincode::{deserialize_from, serialize_into}; use byteorder::{ByteOrder, LittleEndian}; use fs_extra::dir::CopyOptions; +use lazy_static::lazy_static; use log::*; use rand::{thread_rng, Rng}; use rayon::{prelude::*, ThreadPool}; @@ -56,6 +57,12 @@ pub const DEFAULT_FILE_SIZE: u64 = 4 * 1024 * 1024; pub const DEFAULT_NUM_THREADS: u32 = 8; pub const DEFAULT_NUM_DIRS: u32 = 4; +lazy_static! { + // FROZEN_ACCOUNT_PANIC is used to signal local_cluster that an AccountsDB panic has occurred, + // as |cargo test| cannot observe panics in other threads + pub static ref FROZEN_ACCOUNT_PANIC: Arc = { Arc::new(AtomicBool::new(false)) }; +} + #[derive(Debug, Default)] pub struct ErrorCounters { pub total: usize, @@ -426,6 +433,12 @@ pub struct BankHashInfo { pub stats: BankHashStats, } +#[derive(Debug)] +struct FrozenAccountInfo { + pub hash: Hash, // Hash generated by hash_frozen_account_data() + pub lamports: u64, // Account balance cannot be lower than this amount +} + // This structure handles the load/store of the accounts #[derive(Debug)] pub struct AccountsDB { @@ -440,7 +453,7 @@ pub struct AccountsDB { write_version: AtomicUsize, /// Set of storage paths to pick from - paths: RwLock>, + paths: Vec, /// Directory of paths this accounts_db needs to hold/remove temp_paths: Option>, @@ -448,6 +461,9 @@ pub struct AccountsDB { /// Starting file size of appendvecs file_size: u64, + /// Accounts that will cause a panic! if data modified or lamports decrease + frozen_accounts: HashMap, + /// Thread pool used for par_iter pub thread_pool: ThreadPool, @@ -469,7 +485,7 @@ impl Default for AccountsDB { storage: RwLock::new(AccountStorage(HashMap::new())), next_id: AtomicUsize::new(0), write_version: AtomicUsize::new(0), - paths: RwLock::new(vec![]), + paths: vec![], temp_paths: None, file_size: DEFAULT_FILE_SIZE, thread_pool: rayon::ThreadPoolBuilder::new() @@ -478,6 +494,7 @@ impl Default for AccountsDB { .unwrap(), min_num_stores: num_threads, bank_hashes: RwLock::new(bank_hashes), + frozen_accounts: HashMap::new(), } } } @@ -486,23 +503,22 @@ impl AccountsDB { pub fn new(paths: Vec) -> Self { let new = if !paths.is_empty() { Self { - paths: RwLock::new(paths), + paths, temp_paths: None, ..Self::default() } } else { - // Create a temprorary set of accounts directories, used primarily + // Create a temporary set of accounts directories, used primarily // for testing let (temp_dirs, paths) = get_temp_accounts_paths(DEFAULT_NUM_DIRS).unwrap(); Self { - paths: RwLock::new(paths), + paths, temp_paths: Some(temp_dirs), ..Self::default() } }; { - let paths = new.paths.read().unwrap(); - for path in paths.iter() { + for path in new.paths.iter() { std::fs::create_dir_all(path).expect("Create directory failed."); } } @@ -527,8 +543,7 @@ impl AccountsDB { pub fn accounts_from_stream>( &self, mut stream: &mut BufReader, - local_account_paths: &[PathBuf], - append_vecs_path: P, + stream_append_vecs_path: P, ) -> Result<(), IOError> { let _len: usize = deserialize_from(&mut stream).map_err(|e| AccountsDB::get_io_error(&e.to_string()))?; @@ -542,8 +557,8 @@ impl AccountsDB { .map(|(slot_id, mut slot_storage)| { let mut new_slot_storage = HashMap::new(); for (id, storage_entry) in slot_storage.drain() { - let path_index = thread_rng().gen_range(0, local_account_paths.len()); - let local_dir = &local_account_paths[path_index]; + let path_index = thread_rng().gen_range(0, self.paths.len()); + let local_dir = &self.paths[path_index]; std::fs::create_dir_all(local_dir).expect("Create directory failed"); @@ -551,8 +566,9 @@ impl AccountsDB { // at by `local_dir` let append_vec_relative_path = AppendVec::new_relative_path(slot_id, storage_entry.id); - let append_vec_abs_path = - append_vecs_path.as_ref().join(&append_vec_relative_path); + let append_vec_abs_path = stream_append_vecs_path + .as_ref() + .join(&append_vec_relative_path); let target = local_dir.join(append_vec_abs_path.file_name().unwrap()); if std::fs::rename(append_vec_abs_path.clone(), target).is_err() { let mut copy_options = CopyOptions::new(); @@ -602,7 +618,6 @@ impl AccountsDB { self.bank_hashes.write().unwrap().insert(slot, bank_hash); // Process deserialized data, set necessary fields in self - *self.paths.write().unwrap() = local_account_paths.to_vec(); let max_id: usize = *storage .0 .values() @@ -938,22 +953,14 @@ impl AccountsDB { } fn create_and_insert_store(&self, slot_id: Slot, size: u64) -> Arc { + let path_index = thread_rng().gen_range(0, self.paths.len()); + let store = + Arc::new(self.new_storage_entry(slot_id, &Path::new(&self.paths[path_index]), size)); + let store_for_index = store.clone(); + let mut stores = self.storage.write().unwrap(); let slot_storage = stores.0.entry(slot_id).or_insert_with(HashMap::new); - - self.create_store(slot_id, slot_storage, size) - } - - fn create_store( - &self, - slot_id: Slot, - slot_storage: &mut SlotStores, - size: u64, - ) -> Arc { - let paths = self.paths.read().unwrap(); - let path_index = thread_rng().gen_range(0, paths.len()); - let store = Arc::new(self.new_storage_entry(slot_id, &Path::new(&paths[path_index]), size)); - slot_storage.insert(store.id, store.clone()); + slot_storage.insert(store.id, store_for_index); store } @@ -987,6 +994,21 @@ impl AccountsDB { ) } + fn hash_frozen_account_data(account: &Account) -> Hash { + let mut hasher = Hasher::default(); + + hasher.hash(&account.data); + hasher.hash(&account.owner.as_ref()); + + if account.executable { + hasher.hash(&[1u8; 1]); + } else { + hasher.hash(&[0u8; 1]); + } + + hasher.result() + } + pub fn hash_account_data( slot: Slot, lamports: u64, @@ -1398,8 +1420,62 @@ impl AccountsDB { hashes } + pub fn freeze_accounts( + &mut self, + ancestors: &HashMap, + account_pubkeys: &[Pubkey], + ) { + for account_pubkey in account_pubkeys { + if let Some((account, _slot)) = self.load_slow(ancestors, &account_pubkey) { + let frozen_account_info = FrozenAccountInfo { + hash: Self::hash_frozen_account_data(&account), + lamports: account.lamports, + }; + warn!( + "Account {} is now frozen at lamports={}, hash={}", + account_pubkey, frozen_account_info.lamports, frozen_account_info.hash + ); + self.frozen_accounts + .insert(*account_pubkey, frozen_account_info); + } else { + panic!( + "Unable to freeze an account that does not exist: {}", + account_pubkey + ); + } + } + } + + /// Cause a panic if frozen accounts would be affected by data in `accounts` + fn assert_frozen_accounts(&self, accounts: &[(&Pubkey, &Account)]) { + if self.frozen_accounts.is_empty() { + return; + } + for (account_pubkey, account) in accounts.iter() { + if let Some(frozen_account_info) = self.frozen_accounts.get(*account_pubkey) { + if account.lamports < frozen_account_info.lamports { + FROZEN_ACCOUNT_PANIC.store(true, Ordering::Relaxed); + panic!( + "Frozen account {} modified. Lamports decreased from {} to {}", + account_pubkey, frozen_account_info.lamports, account.lamports, + ) + } + + let hash = Self::hash_frozen_account_data(&account); + if hash != frozen_account_info.hash { + FROZEN_ACCOUNT_PANIC.store(true, Ordering::Relaxed); + panic!( + "Frozen account {} modified. Hash changed from {} to {}", + account_pubkey, frozen_account_info.hash, hash, + ) + } + } + } + } + /// Store the account update. pub fn store(&self, slot_id: Slot, accounts: &[(&Pubkey, &Account)]) { + self.assert_frozen_accounts(accounts); let hashes = self.hash_accounts(slot_id, accounts); self.store_with_hashes(slot_id, accounts, &hashes); } @@ -2332,12 +2408,11 @@ pub mod tests { let buf = writer.into_inner(); let mut reader = BufReader::new(&buf[..]); let daccounts = AccountsDB::new(Vec::new()); - let local_paths = daccounts.paths.read().unwrap().clone(); let copied_accounts = TempDir::new().unwrap(); // Simulate obtaining a copy of the AppendVecs from a tarball copy_append_vecs(&accounts, copied_accounts.path()).unwrap(); daccounts - .accounts_from_stream(&mut reader, &local_paths, copied_accounts.path()) + .accounts_from_stream(&mut reader, copied_accounts.path()) .unwrap(); print_count_and_status("daccounts", &daccounts); @@ -2724,6 +2799,139 @@ pub mod tests { Ok(()) } + #[test] + fn test_hash_frozen_account_data() { + let account = Account::new(1, 42, &Pubkey::default()); + + let hash = AccountsDB::hash_frozen_account_data(&account); + assert_ne!(hash, Hash::default()); // Better not be the default Hash + + // Lamports changes to not affect the hash + let mut account_modified = account.clone(); + account_modified.lamports -= 1; + assert_eq!( + hash, + AccountsDB::hash_frozen_account_data(&account_modified) + ); + + // Rent epoch may changes to not affect the hash + let mut account_modified = account.clone(); + account_modified.rent_epoch += 1; + assert_eq!( + hash, + AccountsDB::hash_frozen_account_data(&account_modified) + ); + + // Account data may not be modified + let mut account_modified = account.clone(); + account_modified.data[0] = 42; + assert_ne!( + hash, + AccountsDB::hash_frozen_account_data(&account_modified) + ); + + // Owner may not be modified + let mut account_modified = account.clone(); + account_modified.owner = + Pubkey::from_str("My11111111111111111111111111111111111111111").unwrap(); + assert_ne!( + hash, + AccountsDB::hash_frozen_account_data(&account_modified) + ); + + // Executable may not be modified + let mut account_modified = account.clone(); + account_modified.executable = true; + assert_ne!( + hash, + AccountsDB::hash_frozen_account_data(&account_modified) + ); + } + + #[test] + fn test_frozen_account_lamport_increase() { + let frozen_pubkey = + Pubkey::from_str("My11111111111111111111111111111111111111111").unwrap(); + let mut db = AccountsDB::new(Vec::new()); + + let mut account = Account::new(1, 42, &frozen_pubkey); + db.store(0, &[(&frozen_pubkey, &account)]); + + let ancestors = vec![(0, 0)].into_iter().collect(); + db.freeze_accounts(&ancestors, &[frozen_pubkey]); + + // Store with no account changes is ok + db.store(0, &[(&frozen_pubkey, &account)]); + + // Store with an increase in lamports is ok + account.lamports = 2; + db.store(0, &[(&frozen_pubkey, &account)]); + + // Store with an decrease that does not go below the frozen amount of lamports is tolerated + account.lamports = 1; + db.store(0, &[(&frozen_pubkey, &account)]); + + // A store of any value over the frozen value of '1' across different slots is also ok + account.lamports = 3; + db.store(1, &[(&frozen_pubkey, &account)]); + account.lamports = 2; + db.store(2, &[(&frozen_pubkey, &account)]); + account.lamports = 1; + db.store(3, &[(&frozen_pubkey, &account)]); + } + + #[test] + #[should_panic( + expected = "Frozen account My11111111111111111111111111111111111111111 modified. Lamports decreased from 1 to 0" + )] + fn test_frozen_account_lamport_decrease() { + let frozen_pubkey = + Pubkey::from_str("My11111111111111111111111111111111111111111").unwrap(); + let mut db = AccountsDB::new(Vec::new()); + + let mut account = Account::new(1, 42, &frozen_pubkey); + db.store(0, &[(&frozen_pubkey, &account)]); + + let ancestors = vec![(0, 0)].into_iter().collect(); + db.freeze_accounts(&ancestors, &[frozen_pubkey]); + + // Store with a decrease below the frozen amount of lamports is not ok + account.lamports -= 1; + db.store(0, &[(&frozen_pubkey, &account)]); + } + + #[test] + #[should_panic( + expected = "Unable to freeze an account that does not exist: My11111111111111111111111111111111111111111" + )] + fn test_frozen_account_nonexistent() { + let frozen_pubkey = + Pubkey::from_str("My11111111111111111111111111111111111111111").unwrap(); + let mut db = AccountsDB::new(Vec::new()); + + let ancestors = vec![(0, 0)].into_iter().collect(); + db.freeze_accounts(&ancestors, &[frozen_pubkey]); + } + + #[test] + #[should_panic( + expected = "Frozen account My11111111111111111111111111111111111111111 modified. Hash changed from 8wHcxDkjiwdrkPAsDnmNrF1UDGJFAtZzPQBSVweY3yRA to JdscGYB1uczVssmYuJusDD1Bfe6wpNeeho8XjcH8inN" + )] + fn test_frozen_account_data_modified() { + let frozen_pubkey = + Pubkey::from_str("My11111111111111111111111111111111111111111").unwrap(); + let mut db = AccountsDB::new(Vec::new()); + + let mut account = Account::new(1, 42, &frozen_pubkey); + db.store(0, &[(&frozen_pubkey, &account)]); + + let ancestors = vec![(0, 0)].into_iter().collect(); + db.freeze_accounts(&ancestors, &[frozen_pubkey]); + + account.data[0] = 42; + db.store(0, &[(&frozen_pubkey, &account)]); + } + #[test] fn test_hash_stored_account() { // This test uses some UNSAFE trick to detect most of account's field diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 886504c98e..4d200ae3cf 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -4,9 +4,7 @@ //! already been signed and verified. use crate::{ accounts::{Accounts, TransactionAccounts, TransactionLoadResult, TransactionLoaders}, - accounts_db::{ - AccountsDBSerialize, AppendVecId, ErrorCounters, SnapshotStorage, SnapshotStorages, - }, + accounts_db::{AccountsDBSerialize, ErrorCounters, SnapshotStorage, SnapshotStorages}, blockhash_queue::BlockhashQueue, message_processor::{MessageProcessor, ProcessInstruction}, nonce_utils, @@ -85,31 +83,30 @@ pub struct BankRc { } impl BankRc { - pub fn new(account_paths: Vec, id: AppendVecId, slot: Slot) -> Self { - let accounts = Accounts::new(account_paths); - accounts - .accounts_db - .next_id - .store(id as usize, Ordering::Relaxed); - BankRc { + pub fn from_stream>( + account_paths: &[PathBuf], + slot: Slot, + ancestors: &HashMap, + frozen_account_pubkeys: &[Pubkey], + mut stream: &mut BufReader, + stream_append_vecs_path: P, + ) -> std::result::Result { + let _len: usize = + deserialize_from(&mut stream).map_err(|e| BankRc::get_io_error(&e.to_string()))?; + + let accounts = Accounts::from_stream( + account_paths, + ancestors, + frozen_account_pubkeys, + stream, + stream_append_vecs_path, + )?; + + Ok(BankRc { accounts: Arc::new(accounts), parent: RwLock::new(None), slot, - } - } - - pub fn accounts_from_stream>( - &self, - mut stream: &mut BufReader, - local_paths: &[PathBuf], - append_vecs_path: P, - ) -> std::result::Result<(), IOError> { - let _len: usize = - deserialize_from(&mut stream).map_err(|e| BankRc::get_io_error(&e.to_string()))?; - self.accounts - .accounts_from_stream(stream, local_paths, append_vecs_path)?; - - Ok(()) + }) } pub fn get_snapshot_storages(&self, slot: Slot) -> SnapshotStorages { @@ -359,14 +356,25 @@ impl Default for BlockhashQueue { impl Bank { pub fn new(genesis_config: &GenesisConfig) -> Self { - Self::new_with_paths(&genesis_config, Vec::new()) + Self::new_with_paths(&genesis_config, Vec::new(), &[]) } - pub fn new_with_paths(genesis_config: &GenesisConfig, paths: Vec) -> Self { + pub fn new_with_paths( + genesis_config: &GenesisConfig, + paths: Vec, + frozen_account_pubkeys: &[Pubkey], + ) -> Self { let mut bank = Self::default(); bank.ancestors.insert(bank.slot(), 0); + bank.rc.accounts = Arc::new(Accounts::new(paths)); bank.process_genesis_config(genesis_config); + + // Freeze accounts after process_genesis_config creates the initial append vecs + Arc::get_mut(&mut bank.rc.accounts) + .unwrap() + .freeze_accounts(&bank.ancestors, frozen_account_pubkeys); + // genesis needs stakes for all epochs up to the epoch implied by // slot = 0 and genesis configuration { @@ -4764,14 +4772,21 @@ mod tests { let (_accounts_dir, dbank_paths) = get_temp_accounts_paths(4).unwrap(); let ref_sc = StatusCacheRc::default(); ref_sc.status_cache.write().unwrap().add_root(2); - dbank.set_bank_rc(BankRc::new(dbank_paths.clone(), 0, dbank.slot()), ref_sc); // Create a directory to simulate AppendVecs unpackaged from a snapshot tar let copied_accounts = TempDir::new().unwrap(); copy_append_vecs(&bank2.rc.accounts.accounts_db, copied_accounts.path()).unwrap(); - dbank - .rc - .accounts_from_stream(&mut reader, &dbank_paths, copied_accounts.path()) - .unwrap(); + dbank.set_bank_rc( + BankRc::from_stream( + &dbank_paths, + dbank.slot(), + &dbank.ancestors, + &[], + &mut reader, + copied_accounts.path(), + ) + .unwrap(), + ref_sc, + ); assert_eq!(dbank.get_balance(&key1.pubkey()), 0); assert_eq!(dbank.get_balance(&key2.pubkey()), 10); assert_eq!(dbank.get_balance(&key3.pubkey()), 0); diff --git a/runtime/src/message_processor.rs b/runtime/src/message_processor.rs index 668113b134..25ea2a4d25 100644 --- a/runtime/src/message_processor.rs +++ b/runtime/src/message_processor.rs @@ -109,7 +109,7 @@ impl PreAccount { return Err(InstructionError::ExecutableModified); } - // No one modifies r ent_epoch (yet). + // No one modifies rent_epoch (yet). if self.rent_epoch != post.rent_epoch { return Err(InstructionError::RentEpochModified); } diff --git a/validator/src/main.rs b/validator/src/main.rs index 38c187707f..918b5af150 100644 --- a/validator/src/main.rs +++ b/validator/src/main.rs @@ -1,6 +1,7 @@ use bzip2::bufread::BzDecoder; use clap::{ - crate_description, crate_name, value_t, value_t_or_exit, values_t_or_exit, App, Arg, ArgMatches, + crate_description, crate_name, value_t, value_t_or_exit, values_t, values_t_or_exit, App, Arg, + ArgMatches, }; use console::Emoji; use indicatif::{ProgressBar, ProgressStyle}; @@ -858,6 +859,17 @@ pub fn main() { .takes_value(false) .help("Abort the validator if a bank hash mismatch is detected within trusted validator set"), ) + .arg( + clap::Arg::with_name("frozen_accounts") + .long("frozen-account") + .validator(is_pubkey) + .value_name("PUBKEY") + .multiple(true) + .takes_value(true) + .help("Freeze the specified account. This will cause the validator to \ + intentionally crash should any transaction modify the frozen account in any way \ + other than increasing the account balance"), + ) .get_matches(); let identity_keypair = Arc::new(keypair_of(&matches, "identity").unwrap_or_else(Keypair::new)); @@ -932,6 +944,7 @@ pub fn main() { voting_disabled: matches.is_present("no_voting"), wait_for_supermajority: value_t!(matches, "wait_for_supermajority", Slot).ok(), trusted_validators, + frozen_accounts: values_t!(matches, "frozen_accounts", Pubkey).unwrap_or_default(), ..ValidatorConfig::default() };