From c8021c63eadc7ec4ee85871d8514179b25504a25 Mon Sep 17 00:00:00 2001 From: HaoranYi Date: Wed, 26 Jul 2023 01:34:06 +0000 Subject: [PATCH] move stake tests out of cli --- Cargo.lock | 8 + cli/tests/stake.rs | 754 -------------------------------- programs/stake/Cargo.toml | 8 + programs/stake/tests/stake.rs | 788 ++++++++++++++++++++++++++++++++++ 4 files changed, 804 insertions(+), 754 deletions(-) create mode 100644 programs/stake/tests/stake.rs diff --git a/Cargo.lock b/Cargo.lock index 31faf451160b41..3185da128b24a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6909,10 +6909,18 @@ dependencies = [ "log", "proptest", "rustc_version 0.4.0", + "solana-cli", + "solana-cli-output", "solana-config-program", + "solana-faucet", "solana-logger", "solana-program-runtime", + "solana-rpc-client", + "solana-rpc-client-api", + "solana-rpc-client-nonce-utils", "solana-sdk", + "solana-streamer", + "solana-test-validator", "solana-vote-program", "test-case", ] diff --git a/cli/tests/stake.rs b/cli/tests/stake.rs index 2f88acd559dba5..481c163c68c40a 100644 --- a/cli/tests/stake.rs +++ b/cli/tests/stake.rs @@ -2320,757 +2320,3 @@ fn test_stake_minimum_delegation() { let result = process_command(&config); assert!(matches!(result, Ok(..))); } - -#[test] -fn test_stake_redelegation_then_deactivation_withdraw_not_permitted() { - let mint_keypair = Keypair::new(); - let mint_pubkey = mint_keypair.pubkey(); - let authorized_withdrawer = Keypair::new().pubkey(); - let faucet_addr = run_local_faucet(mint_keypair, None); - - // setup test validator - let slots_per_epoch = 32; - let test_validator = TestValidatorGenesis::default() - .fee_rate_governor(FeeRateGovernor::new(0, 0)) - .rent(Rent { - lamports_per_byte_year: 1, - exemption_threshold: 1.0, - ..Rent::default() - }) - .epoch_schedule(EpochSchedule::custom( - slots_per_epoch, - slots_per_epoch, - /* enable_warmup_epochs = */ false, - )) - .faucet_addr(Some(faucet_addr)) - .start_with_mint_address(mint_pubkey, SocketAddrSpace::Unspecified) - .expect("validator start failed"); - - let rpc_client = - RpcClient::new_with_commitment(test_validator.rpc_url(), CommitmentConfig::processed()); - let default_signer = Keypair::new(); - - let mut config = CliConfig::recent_for_tests(); - config.json_rpc_url = test_validator.rpc_url(); - config.signers = vec![&default_signer]; - - request_and_confirm_airdrop( - &rpc_client, - &config, - &config.signers[0].pubkey(), - 100_000_000_000, - ) - .unwrap(); - - // Create vote account - let vote_keypair = Keypair::new(); - config.signers = vec![&default_signer, &vote_keypair]; - config.command = CliCommand::CreateVoteAccount { - vote_account: 1, - seed: None, - identity_account: 0, - authorized_voter: None, - authorized_withdrawer, - commission: 0, - sign_only: false, - dump_transaction_message: false, - blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), - nonce_account: None, - nonce_authority: 0, - memo: None, - fee_payer: 0, - compute_unit_price: None, - }; - process_command(&config).unwrap(); - - // Create second vote account - let vote2_keypair = Keypair::new(); - config.signers = vec![&default_signer, &vote2_keypair]; - config.command = CliCommand::CreateVoteAccount { - vote_account: 1, - seed: None, - identity_account: 0, - authorized_voter: None, - authorized_withdrawer, - commission: 0, - sign_only: false, - dump_transaction_message: false, - blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), - nonce_account: None, - nonce_authority: 0, - memo: None, - fee_payer: 0, - compute_unit_price: None, - }; - process_command(&config).unwrap(); - - // Create stake account - let stake_keypair = Keypair::new(); - config.signers = vec![&default_signer, &stake_keypair]; - config.command = CliCommand::CreateStakeAccount { - stake_account: 1, - seed: None, - staker: None, - withdrawer: None, - withdrawer_signer: None, - lockup: Lockup::default(), - amount: SpendAmount::Some(50_000_000_000), - sign_only: false, - dump_transaction_message: false, - blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), - nonce_account: None, - nonce_authority: 0, - memo: None, - fee_payer: 0, - from: 0, - compute_unit_price: None, - }; - process_command(&config).unwrap(); - - // Delegate stake to `vote_keypair` - config.signers = vec![&default_signer]; - config.command = CliCommand::DelegateStake { - stake_account_pubkey: stake_keypair.pubkey(), - vote_account_pubkey: vote_keypair.pubkey(), - stake_authority: 0, - force: true, - sign_only: false, - dump_transaction_message: false, - blockhash_query: BlockhashQuery::default(), - nonce_account: None, - nonce_authority: 0, - memo: None, - fee_payer: 0, - redelegation_stake_account: None, - compute_unit_price: None, - }; - process_command(&config).unwrap(); - - // wait for new epoch - wait_for_next_epoch_plus_n_slots(&rpc_client, 0); - - // `stake_keypair` should now be delegated to `vote_keypair` and fully activated - let stake_account = rpc_client.get_account(&stake_keypair.pubkey()).unwrap(); - let stake_state: StakeState = stake_account.state().unwrap(); - - let rent_exempt_reserve = match stake_state { - StakeState::Stake(meta, stake, _) => { - assert_eq!(stake.delegation.voter_pubkey, vote_keypair.pubkey()); - meta.rent_exempt_reserve - } - _ => panic!("Unexpected stake state!"), - }; - - assert_eq!( - rpc_client - .get_stake_activation(stake_keypair.pubkey(), None) - .unwrap(), - RpcStakeActivation { - state: StakeActivationState::Active, - active: 50_000_000_000 - rent_exempt_reserve, - inactive: 0 - } - ); - check_balance!(50_000_000_000, &rpc_client, &stake_keypair.pubkey()); - - // Now try to withdraw from stake_account. It will fail. - let recipient = keypair_from_seed(&[5u8; 32]).unwrap(); - let recipient_pubkey = recipient.pubkey(); - config.command = CliCommand::WithdrawStake { - stake_account_pubkey: stake_keypair.pubkey(), - destination_account_pubkey: recipient_pubkey, - amount: SpendAmount::Some(50_000_000_000), - withdraw_authority: 0, - custodian: None, - sign_only: false, - dump_transaction_message: false, - blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), - nonce_account: None, - nonce_authority: 0, - memo: None, - seed: None, - fee_payer: 0, - compute_unit_price: None, - }; - - let withdraw_result = process_command(&config); - if let Err(e) = withdraw_result { - let s = format!("{}", e); - assert!(s.contains("insufficient funds for instruction")); - } else { - unreachable!(); - } - - // Setup `stake2_keypair. Add an extra `rent_exempt_reserve` amount into `stake2_keypair` - // before redelegation to account for the `rent_exempt_reserve` balance that'll be pealed off - // the stake during the redelegation process. - let stake2_keypair = Keypair::new(); - request_and_confirm_airdrop( - &rpc_client, - &config, - &stake2_keypair.pubkey(), - rent_exempt_reserve, - ) - .unwrap(); - - // Redelegate to `vote2_keypair` via `stake2_keypair - config.signers = vec![&default_signer, &stake2_keypair]; - config.command = CliCommand::DelegateStake { - stake_account_pubkey: stake_keypair.pubkey(), - vote_account_pubkey: vote2_keypair.pubkey(), - stake_authority: 0, - force: true, - sign_only: false, - dump_transaction_message: false, - blockhash_query: BlockhashQuery::default(), - nonce_account: None, - nonce_authority: 0, - memo: None, - fee_payer: 0, - redelegation_stake_account: Some(1), - compute_unit_price: None, - }; - process_command(&config).unwrap(); - - // `stake_keypair` should now be deactivating - assert_eq!( - rpc_client - .get_stake_activation(stake_keypair.pubkey(), None) - .unwrap(), - RpcStakeActivation { - state: StakeActivationState::Deactivating, - active: 50_000_000_000 - rent_exempt_reserve, - inactive: 0, - } - ); - - // `stake_keypair2` should now be activating - assert_eq!( - rpc_client - .get_stake_activation(stake2_keypair.pubkey(), None) - .unwrap(), - RpcStakeActivation { - state: StakeActivationState::Activating, - active: 0, - inactive: 50_000_000_000 - rent_exempt_reserve, - } - ); - - // check that all the stake, save `rent_exempt_reserve`, have been moved from `stake_keypair` - // to `stake2_keypair` - check_balance!(rent_exempt_reserve, &rpc_client, &stake_keypair.pubkey()); - check_balance!(50_000_000_000, &rpc_client, &stake2_keypair.pubkey()); - - // Deactivate stake2_account should fail because stake2_account is not fully activated. - config.signers = vec![&default_signer]; - - config.command = CliCommand::DeactivateStake { - stake_account_pubkey: stake2_keypair.pubkey(), - stake_authority: 0, - sign_only: false, - deactivate_delinquent: false, - dump_transaction_message: true, - blockhash_query: BlockhashQuery::default(), - nonce_account: None, - nonce_authority: 0, - memo: None, - seed: None, - fee_payer: 0, - compute_unit_price: None, - }; - let deactivate_result = process_command(&config); - if let Err(e) = deactivate_result { - let s = format!("{}", e); - assert_eq!( - s, - "redelegated stake must be fully activated before deactivation" - ); - } else { - unreachable!(); - } - - // `stake_keypair2` should still be Activating - assert_eq!( - rpc_client - .get_stake_activation(stake2_keypair.pubkey(), None) - .unwrap(), - RpcStakeActivation { - state: StakeActivationState::Activating, - active: 0, - inactive: 50_000_000_000 - rent_exempt_reserve, - } - ); - - // Withdraw from stake2 account should still fails - let recipient = keypair_from_seed(&[5u8; 32]).unwrap(); - let recipient_pubkey = recipient.pubkey(); - config.command = CliCommand::WithdrawStake { - stake_account_pubkey: stake2_keypair.pubkey(), - destination_account_pubkey: recipient_pubkey, - amount: SpendAmount::Some(50_000_000_000), - withdraw_authority: 0, - custodian: None, - sign_only: false, - dump_transaction_message: false, - blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), - nonce_account: None, - nonce_authority: 0, - memo: None, - seed: None, - fee_payer: 0, - compute_unit_price: None, - }; - - let withdraw_result = process_command(&config); - if let Err(e) = withdraw_result { - let s = format!("{}", e); - assert!(s.contains("insufficient funds for instruction")); - } else { - unreachable!(); - } - - // wait for one epoch, now stake2_account should be fully activated. - wait_for_next_epoch_plus_n_slots(&rpc_client, 0); - - // Deactivate stake2_account should succeed. - config.signers = vec![&default_signer]; - - config.command = CliCommand::DeactivateStake { - stake_account_pubkey: stake2_keypair.pubkey(), - stake_authority: 0, - sign_only: false, - deactivate_delinquent: false, - dump_transaction_message: true, - blockhash_query: BlockhashQuery::default(), - nonce_account: None, - nonce_authority: 0, - memo: None, - seed: None, - fee_payer: 0, - compute_unit_price: None, - }; - let deactivate_result = process_command(&config); - assert!(deactivate_result.is_ok()); -} - -fn test_stake_redelegation_then_merge_deactivation_not_permitted(deactivating_merge_stake: bool) { - let mint_keypair = Keypair::new(); - let mint_pubkey = mint_keypair.pubkey(); - let authorized_withdrawer = Keypair::new().pubkey(); - let faucet_addr = run_local_faucet(mint_keypair, None); - - // setup test validator - let slots_per_epoch = 32; - let test_validator = TestValidatorGenesis::default() - .fee_rate_governor(FeeRateGovernor::new(0, 0)) - .rent(Rent { - lamports_per_byte_year: 1, - exemption_threshold: 1.0, - ..Rent::default() - }) - .epoch_schedule(EpochSchedule::custom( - slots_per_epoch, - slots_per_epoch, - /* enable_warmup_epochs = */ false, - )) - .faucet_addr(Some(faucet_addr)) - .start_with_mint_address(mint_pubkey, SocketAddrSpace::Unspecified) - .expect("validator start failed"); - - let rpc_client = - RpcClient::new_with_commitment(test_validator.rpc_url(), CommitmentConfig::processed()); - let default_signer = Keypair::new(); - - let mut config = CliConfig::recent_for_tests(); - config.json_rpc_url = test_validator.rpc_url(); - config.signers = vec![&default_signer]; - - request_and_confirm_airdrop( - &rpc_client, - &config, - &config.signers[0].pubkey(), - 1_000_000_000_000, - ) - .unwrap(); - - // Create vote account - let vote_keypair = Keypair::new(); - config.signers = vec![&default_signer, &vote_keypair]; - config.command = CliCommand::CreateVoteAccount { - vote_account: 1, - seed: None, - identity_account: 0, - authorized_voter: None, - authorized_withdrawer, - commission: 0, - sign_only: false, - dump_transaction_message: false, - blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), - nonce_account: None, - nonce_authority: 0, - memo: None, - fee_payer: 0, - compute_unit_price: None, - }; - process_command(&config).unwrap(); - - // Create second vote account - let vote2_keypair = Keypair::new(); - config.signers = vec![&default_signer, &vote2_keypair]; - config.command = CliCommand::CreateVoteAccount { - vote_account: 1, - seed: None, - identity_account: 0, - authorized_voter: None, - authorized_withdrawer, - commission: 0, - sign_only: false, - dump_transaction_message: false, - blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), - nonce_account: None, - nonce_authority: 0, - memo: None, - fee_payer: 0, - compute_unit_price: None, - }; - process_command(&config).unwrap(); - - // Create stake account - let stake_keypair = Keypair::new(); - config.signers = vec![&default_signer, &stake_keypair]; - config.command = CliCommand::CreateStakeAccount { - stake_account: 1, - seed: None, - staker: None, - withdrawer: None, - withdrawer_signer: None, - lockup: Lockup::default(), - amount: SpendAmount::Some(50_000_000_000), - sign_only: false, - dump_transaction_message: false, - blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), - nonce_account: None, - nonce_authority: 0, - memo: None, - fee_payer: 0, - from: 0, - compute_unit_price: None, - }; - process_command(&config).unwrap(); - - // Delegate stake to `vote_keypair` - config.signers = vec![&default_signer]; - config.command = CliCommand::DelegateStake { - stake_account_pubkey: stake_keypair.pubkey(), - vote_account_pubkey: vote_keypair.pubkey(), - stake_authority: 0, - force: true, - sign_only: false, - dump_transaction_message: false, - blockhash_query: BlockhashQuery::default(), - nonce_account: None, - nonce_authority: 0, - memo: None, - fee_payer: 0, - redelegation_stake_account: None, - compute_unit_price: None, - }; - process_command(&config).unwrap(); - - // wait for new epoch - wait_for_next_epoch_plus_n_slots(&rpc_client, 0); - - // `stake_keypair` should now be delegated to `vote_keypair` and fully activated - let stake_account = rpc_client.get_account(&stake_keypair.pubkey()).unwrap(); - let stake_state: StakeState = stake_account.state().unwrap(); - - let rent_exempt_reserve = match stake_state { - StakeState::Stake(meta, stake, _) => { - assert_eq!(stake.delegation.voter_pubkey, vote_keypair.pubkey()); - meta.rent_exempt_reserve - } - _ => panic!("Unexpected stake state!"), - }; - - assert_eq!( - rpc_client - .get_stake_activation(stake_keypair.pubkey(), None) - .unwrap(), - RpcStakeActivation { - state: StakeActivationState::Active, - active: 50_000_000_000 - rent_exempt_reserve, - inactive: 0 - } - ); - check_balance!(50_000_000_000, &rpc_client, &stake_keypair.pubkey()); - - // Now try to withdraw from stake_account. It will fail. - let recipient = keypair_from_seed(&[5u8; 32]).unwrap(); - let recipient_pubkey = recipient.pubkey(); - config.command = CliCommand::WithdrawStake { - stake_account_pubkey: stake_keypair.pubkey(), - destination_account_pubkey: recipient_pubkey, - amount: SpendAmount::Some(50_000_000_000), - withdraw_authority: 0, - custodian: None, - sign_only: false, - dump_transaction_message: false, - blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), - nonce_account: None, - nonce_authority: 0, - memo: None, - seed: None, - fee_payer: 0, - compute_unit_price: None, - }; - - let withdraw_result = process_command(&config); - if let Err(e) = withdraw_result { - let s = format!("{}", e); - assert!(s.contains("insufficient funds for instruction")); - } else { - unreachable!(); - } - - // Setup `stake2_keypair. Add an extra `rent_exempt_reserve` amount into `stake2_keypair` - // before redelegation to account for the `rent_exempt_reserve` balance that'll be pealed off - // the stake during the redelegation process. - let stake2_keypair = Keypair::new(); - request_and_confirm_airdrop( - &rpc_client, - &config, - &stake2_keypair.pubkey(), - rent_exempt_reserve, - ) - .unwrap(); - - // Redelegate to `vote2_keypair` via `stake2_keypair - config.signers = vec![&default_signer, &stake2_keypair]; - config.command = CliCommand::DelegateStake { - stake_account_pubkey: stake_keypair.pubkey(), - vote_account_pubkey: vote2_keypair.pubkey(), - stake_authority: 0, - force: true, - sign_only: false, - dump_transaction_message: false, - blockhash_query: BlockhashQuery::default(), - nonce_account: None, - nonce_authority: 0, - memo: None, - fee_payer: 0, - redelegation_stake_account: Some(1), - compute_unit_price: None, - }; - process_command(&config).unwrap(); - - // `stake_keypair` should now be deactivating - assert_eq!( - rpc_client - .get_stake_activation(stake_keypair.pubkey(), None) - .unwrap(), - RpcStakeActivation { - state: StakeActivationState::Deactivating, - active: 50_000_000_000 - rent_exempt_reserve, - inactive: 0, - } - ); - - // `stake_keypair2` should now be activating - assert_eq!( - rpc_client - .get_stake_activation(stake2_keypair.pubkey(), None) - .unwrap(), - RpcStakeActivation { - state: StakeActivationState::Activating, - active: 0, - inactive: 50_000_000_000 - rent_exempt_reserve, - } - ); - - // check that all the stake, save `rent_exempt_reserve`, have been moved from `stake_keypair` - // to `stake2_keypair` - check_balance!(rent_exempt_reserve, &rpc_client, &stake_keypair.pubkey()); - check_balance!(50_000_000_000, &rpc_client, &stake2_keypair.pubkey()); - - // Create stake merge account - let stake_merge_keypair = Keypair::new(); - config.signers = vec![&default_signer, &stake_merge_keypair]; - config.command = CliCommand::CreateStakeAccount { - stake_account: 1, - seed: None, - staker: None, - withdrawer: None, - withdrawer_signer: None, - lockup: Lockup::default(), - amount: SpendAmount::Some(50_000_000_000), - sign_only: false, - dump_transaction_message: false, - blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), - nonce_account: None, - nonce_authority: 0, - memo: None, - fee_payer: 0, - from: 0, - compute_unit_price: None, - }; - process_command(&config).unwrap(); - - // Delegate stake_merge to `vote_keypair2` - config.signers = vec![&default_signer]; - config.command = CliCommand::DelegateStake { - stake_account_pubkey: stake_merge_keypair.pubkey(), - vote_account_pubkey: vote2_keypair.pubkey(), - stake_authority: 0, - force: true, - sign_only: false, - dump_transaction_message: false, - blockhash_query: BlockhashQuery::default(), - nonce_account: None, - nonce_authority: 0, - memo: None, - fee_payer: 0, - redelegation_stake_account: None, - compute_unit_price: None, - }; - process_command(&config).unwrap(); - - // Deactivate stake_merge_account to test (Activating, Inactive) merge case - if deactivating_merge_stake { - config.signers = vec![&default_signer]; - - config.command = CliCommand::DeactivateStake { - stake_account_pubkey: stake_merge_keypair.pubkey(), - stake_authority: 0, - sign_only: false, - deactivate_delinquent: false, - dump_transaction_message: true, - blockhash_query: BlockhashQuery::default(), - nonce_account: None, - nonce_authority: 0, - memo: None, - seed: None, - fee_payer: 0, - compute_unit_price: None, - }; - process_command(&config).unwrap(); - } - - // merge stake_merge account to stake2_account should NOT clear the the `MustFullyActivateBeforeDeactivationIsPermitted` flag. - config.signers = vec![&default_signer]; - - config.command = CliCommand::MergeStake { - stake_account_pubkey: stake2_keypair.pubkey(), - source_stake_account_pubkey: stake_merge_keypair.pubkey(), - stake_authority: 0, - sign_only: false, - dump_transaction_message: true, - blockhash_query: BlockhashQuery::default(), - nonce_account: None, - nonce_authority: 0, - memo: None, - fee_payer: 0, - compute_unit_price: None, - }; - process_command(&config).unwrap(); - - // Deactivate stake2_account should fail because stake2_account is not fully activated. - config.signers = vec![&default_signer]; - - config.command = CliCommand::DeactivateStake { - stake_account_pubkey: stake2_keypair.pubkey(), - stake_authority: 0, - sign_only: false, - deactivate_delinquent: false, - dump_transaction_message: true, - blockhash_query: BlockhashQuery::default(), - nonce_account: None, - nonce_authority: 0, - memo: None, - seed: None, - fee_payer: 0, - compute_unit_price: None, - }; - let deactivate_result = process_command(&config); - if let Err(e) = deactivate_result { - let s = format!("{}", e); - assert_eq!( - s, - "redelegated stake must be fully activated before deactivation" - ); - } else { - unreachable!(); - } - - // `stake_keypair2` should still be Activating - assert_eq!( - rpc_client - .get_stake_activation(stake2_keypair.pubkey(), None) - .unwrap(), - RpcStakeActivation { - state: StakeActivationState::Activating, - active: 0, - inactive: 100_000_000_000 - rent_exempt_reserve, - } - ); - - // Withdraw from stake2 account should still fails - let recipient = keypair_from_seed(&[5u8; 32]).unwrap(); - let recipient_pubkey = recipient.pubkey(); - config.command = CliCommand::WithdrawStake { - stake_account_pubkey: stake2_keypair.pubkey(), - destination_account_pubkey: recipient_pubkey, - amount: SpendAmount::Some(50_000_000_000), - withdraw_authority: 0, - custodian: None, - sign_only: false, - dump_transaction_message: false, - blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), - nonce_account: None, - nonce_authority: 0, - memo: None, - seed: None, - fee_payer: 0, - compute_unit_price: None, - }; - - let withdraw_result = process_command(&config); - if let Err(e) = withdraw_result { - let s = format!("{}", e); - assert!(s.contains("insufficient funds for instruction")); - } else { - unreachable!(); - } - - // wait for one epoch, now stake2_account should be fully activated. - wait_for_next_epoch_plus_n_slots(&rpc_client, 0); - - // Deactivate stake2_account should succeed. - config.signers = vec![&default_signer]; - - config.command = CliCommand::DeactivateStake { - stake_account_pubkey: stake2_keypair.pubkey(), - stake_authority: 0, - sign_only: false, - deactivate_delinquent: false, - dump_transaction_message: true, - blockhash_query: BlockhashQuery::default(), - nonce_account: None, - nonce_authority: 0, - memo: None, - seed: None, - fee_payer: 0, - compute_unit_price: None, - }; - let deactivate_result = process_command(&config); - assert!(deactivate_result.is_ok()); -} - -#[test] -fn test_stake_redelegation_then_merge_activating_stake_then_deactivation_not_permitted() { - test_stake_redelegation_then_merge_deactivation_not_permitted(false); -} - -#[test] -fn test_stake_redelegation_then_merge_inactive_stake_then_deactivation_not_permitted() { - test_stake_redelegation_then_merge_deactivation_not_permitted(true); -} diff --git a/programs/stake/Cargo.toml b/programs/stake/Cargo.toml index d871b3622e3093..78fa770c1edae8 100644 --- a/programs/stake/Cargo.toml +++ b/programs/stake/Cargo.toml @@ -21,6 +21,14 @@ solana-vote-program = { workspace = true } assert_matches = { workspace = true } proptest = { workspace = true } solana-logger = { workspace = true } +solana-cli = { workspace = true } +solana-cli-output = {workspace = true } +solana-faucet = { workspace = true } +solana-rpc-client = { workspace = true, features = ["default"] } +solana-rpc-client-api = { workspace = true } +solana-rpc-client-nonce-utils = { workspace = true } +solana-streamer = { workspace = true } +solana-test-validator = {workspace = true } test-case = { workspace = true } [build-dependencies] diff --git a/programs/stake/tests/stake.rs b/programs/stake/tests/stake.rs new file mode 100644 index 00000000000000..d62b24561cd64e --- /dev/null +++ b/programs/stake/tests/stake.rs @@ -0,0 +1,788 @@ +#![allow(clippy::integer_arithmetic)] +#![allow(clippy::redundant_closure)] +use { + solana_cli::{ + check_balance, + cli::{process_command, request_and_confirm_airdrop, CliCommand, CliConfig}, + spend_utils::SpendAmount, + stake::StakeAuthorizationIndexed, + test_utils::{check_ready, wait_for_next_epoch_plus_n_slots}, + }, + solana_cli_output::{parse_sign_only_reply_string, OutputFormat}, + solana_faucet::faucet::run_local_faucet, + solana_rpc_client::rpc_client::RpcClient, + solana_rpc_client_api::response::{RpcStakeActivation, StakeActivationState}, + solana_rpc_client_nonce_utils::blockhash_query::{self, BlockhashQuery}, + solana_sdk::{ + account_utils::StateMut, + commitment_config::CommitmentConfig, + epoch_schedule::EpochSchedule, + fee::FeeStructure, + fee_calculator::FeeRateGovernor, + nonce::State as NonceState, + pubkey::Pubkey, + rent::Rent, + signature::{keypair_from_seed, Keypair, Signer}, + stake::{ + self, + instruction::LockupArgs, + state::{Lockup, StakeAuthorize, StakeState}, + }, + }, + solana_streamer::socket::SocketAddrSpace, + solana_test_validator::{TestValidator, TestValidatorGenesis}, +}; + +#[test] +fn test_stake_redelegation_then_deactivation_withdraw_not_permitted() { + let mint_keypair = Keypair::new(); + let mint_pubkey = mint_keypair.pubkey(); + let authorized_withdrawer = Keypair::new().pubkey(); + let faucet_addr = run_local_faucet(mint_keypair, None); + + // setup test validator + let slots_per_epoch = 32; + let test_validator = TestValidatorGenesis::default() + .fee_rate_governor(FeeRateGovernor::new(0, 0)) + .rent(Rent { + lamports_per_byte_year: 1, + exemption_threshold: 1.0, + ..Rent::default() + }) + .epoch_schedule(EpochSchedule::custom( + slots_per_epoch, + slots_per_epoch, + /* enable_warmup_epochs = */ false, + )) + .faucet_addr(Some(faucet_addr)) + .start_with_mint_address(mint_pubkey, SocketAddrSpace::Unspecified) + .expect("validator start failed"); + + let rpc_client = + RpcClient::new_with_commitment(test_validator.rpc_url(), CommitmentConfig::processed()); + let default_signer = Keypair::new(); + + let mut config = CliConfig::recent_for_tests(); + config.json_rpc_url = test_validator.rpc_url(); + config.signers = vec![&default_signer]; + + request_and_confirm_airdrop( + &rpc_client, + &config, + &config.signers[0].pubkey(), + 100_000_000_000, + ) + .unwrap(); + + // Create vote account + let vote_keypair = Keypair::new(); + config.signers = vec![&default_signer, &vote_keypair]; + config.command = CliCommand::CreateVoteAccount { + vote_account: 1, + seed: None, + identity_account: 0, + authorized_voter: None, + authorized_withdrawer, + commission: 0, + sign_only: false, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + nonce_account: None, + nonce_authority: 0, + memo: None, + fee_payer: 0, + compute_unit_price: None, + }; + process_command(&config).unwrap(); + + // Create second vote account + let vote2_keypair = Keypair::new(); + config.signers = vec![&default_signer, &vote2_keypair]; + config.command = CliCommand::CreateVoteAccount { + vote_account: 1, + seed: None, + identity_account: 0, + authorized_voter: None, + authorized_withdrawer, + commission: 0, + sign_only: false, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + nonce_account: None, + nonce_authority: 0, + memo: None, + fee_payer: 0, + compute_unit_price: None, + }; + process_command(&config).unwrap(); + + // Create stake account + let stake_keypair = Keypair::new(); + config.signers = vec![&default_signer, &stake_keypair]; + config.command = CliCommand::CreateStakeAccount { + stake_account: 1, + seed: None, + staker: None, + withdrawer: None, + withdrawer_signer: None, + lockup: Lockup::default(), + amount: SpendAmount::Some(50_000_000_000), + sign_only: false, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + nonce_account: None, + nonce_authority: 0, + memo: None, + fee_payer: 0, + from: 0, + compute_unit_price: None, + }; + process_command(&config).unwrap(); + + // Delegate stake to `vote_keypair` + config.signers = vec![&default_signer]; + config.command = CliCommand::DelegateStake { + stake_account_pubkey: stake_keypair.pubkey(), + vote_account_pubkey: vote_keypair.pubkey(), + stake_authority: 0, + force: true, + sign_only: false, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::default(), + nonce_account: None, + nonce_authority: 0, + memo: None, + fee_payer: 0, + redelegation_stake_account: None, + compute_unit_price: None, + }; + process_command(&config).unwrap(); + + // wait for new epoch + wait_for_next_epoch_plus_n_slots(&rpc_client, 0); + + // `stake_keypair` should now be delegated to `vote_keypair` and fully activated + let stake_account = rpc_client.get_account(&stake_keypair.pubkey()).unwrap(); + let stake_state: StakeState = stake_account.state().unwrap(); + + let rent_exempt_reserve = match stake_state { + StakeState::Stake(meta, stake, _) => { + assert_eq!(stake.delegation.voter_pubkey, vote_keypair.pubkey()); + meta.rent_exempt_reserve + } + _ => panic!("Unexpected stake state!"), + }; + + assert_eq!( + rpc_client + .get_stake_activation(stake_keypair.pubkey(), None) + .unwrap(), + RpcStakeActivation { + state: StakeActivationState::Active, + active: 50_000_000_000 - rent_exempt_reserve, + inactive: 0 + } + ); + check_balance!(50_000_000_000, &rpc_client, &stake_keypair.pubkey()); + + // Now try to withdraw from stake_account. It will fail. + let recipient = keypair_from_seed(&[5u8; 32]).unwrap(); + let recipient_pubkey = recipient.pubkey(); + config.command = CliCommand::WithdrawStake { + stake_account_pubkey: stake_keypair.pubkey(), + destination_account_pubkey: recipient_pubkey, + amount: SpendAmount::Some(50_000_000_000), + withdraw_authority: 0, + custodian: None, + sign_only: false, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + nonce_account: None, + nonce_authority: 0, + memo: None, + seed: None, + fee_payer: 0, + compute_unit_price: None, + }; + + let withdraw_result = process_command(&config); + if let Err(e) = withdraw_result { + let s = format!("{}", e); + assert!(s.contains("insufficient funds for instruction")); + } else { + unreachable!(); + } + + // Setup `stake2_keypair. Add an extra `rent_exempt_reserve` amount into `stake2_keypair` + // before redelegation to account for the `rent_exempt_reserve` balance that'll be pealed off + // the stake during the redelegation process. + let stake2_keypair = Keypair::new(); + request_and_confirm_airdrop( + &rpc_client, + &config, + &stake2_keypair.pubkey(), + rent_exempt_reserve, + ) + .unwrap(); + + // Redelegate to `vote2_keypair` via `stake2_keypair + config.signers = vec![&default_signer, &stake2_keypair]; + config.command = CliCommand::DelegateStake { + stake_account_pubkey: stake_keypair.pubkey(), + vote_account_pubkey: vote2_keypair.pubkey(), + stake_authority: 0, + force: true, + sign_only: false, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::default(), + nonce_account: None, + nonce_authority: 0, + memo: None, + fee_payer: 0, + redelegation_stake_account: Some(1), + compute_unit_price: None, + }; + process_command(&config).unwrap(); + + // `stake_keypair` should now be deactivating + assert_eq!( + rpc_client + .get_stake_activation(stake_keypair.pubkey(), None) + .unwrap(), + RpcStakeActivation { + state: StakeActivationState::Deactivating, + active: 50_000_000_000 - rent_exempt_reserve, + inactive: 0, + } + ); + + // `stake_keypair2` should now be activating + assert_eq!( + rpc_client + .get_stake_activation(stake2_keypair.pubkey(), None) + .unwrap(), + RpcStakeActivation { + state: StakeActivationState::Activating, + active: 0, + inactive: 50_000_000_000 - rent_exempt_reserve, + } + ); + + // check that all the stake, save `rent_exempt_reserve`, have been moved from `stake_keypair` + // to `stake2_keypair` + check_balance!(rent_exempt_reserve, &rpc_client, &stake_keypair.pubkey()); + check_balance!(50_000_000_000, &rpc_client, &stake2_keypair.pubkey()); + + // Deactivate stake2_account should fail because stake2_account is not fully activated. + config.signers = vec![&default_signer]; + + config.command = CliCommand::DeactivateStake { + stake_account_pubkey: stake2_keypair.pubkey(), + stake_authority: 0, + sign_only: false, + deactivate_delinquent: false, + dump_transaction_message: true, + blockhash_query: BlockhashQuery::default(), + nonce_account: None, + nonce_authority: 0, + memo: None, + seed: None, + fee_payer: 0, + compute_unit_price: None, + }; + let deactivate_result = process_command(&config); + if let Err(e) = deactivate_result { + let s = format!("{}", e); + assert_eq!( + s, + "redelegated stake must be fully activated before deactivation" + ); + } else { + unreachable!(); + } + + // `stake_keypair2` should still be Activating + assert_eq!( + rpc_client + .get_stake_activation(stake2_keypair.pubkey(), None) + .unwrap(), + RpcStakeActivation { + state: StakeActivationState::Activating, + active: 0, + inactive: 50_000_000_000 - rent_exempt_reserve, + } + ); + + // Withdraw from stake2 account should still fails + let recipient = keypair_from_seed(&[5u8; 32]).unwrap(); + let recipient_pubkey = recipient.pubkey(); + config.command = CliCommand::WithdrawStake { + stake_account_pubkey: stake2_keypair.pubkey(), + destination_account_pubkey: recipient_pubkey, + amount: SpendAmount::Some(50_000_000_000), + withdraw_authority: 0, + custodian: None, + sign_only: false, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + nonce_account: None, + nonce_authority: 0, + memo: None, + seed: None, + fee_payer: 0, + compute_unit_price: None, + }; + + let withdraw_result = process_command(&config); + if let Err(e) = withdraw_result { + let s = format!("{}", e); + assert!(s.contains("insufficient funds for instruction")); + } else { + unreachable!(); + } + + // wait for one epoch, now stake2_account should be fully activated. + wait_for_next_epoch_plus_n_slots(&rpc_client, 0); + + // Deactivate stake2_account should succeed. + config.signers = vec![&default_signer]; + + config.command = CliCommand::DeactivateStake { + stake_account_pubkey: stake2_keypair.pubkey(), + stake_authority: 0, + sign_only: false, + deactivate_delinquent: false, + dump_transaction_message: true, + blockhash_query: BlockhashQuery::default(), + nonce_account: None, + nonce_authority: 0, + memo: None, + seed: None, + fee_payer: 0, + compute_unit_price: None, + }; + let deactivate_result = process_command(&config); + assert!(deactivate_result.is_ok()); +} + +fn test_stake_redelegation_then_merge_deactivation_not_permitted(deactivating_merge_stake: bool) { + let mint_keypair = Keypair::new(); + let mint_pubkey = mint_keypair.pubkey(); + let authorized_withdrawer = Keypair::new().pubkey(); + let faucet_addr = run_local_faucet(mint_keypair, None); + + // setup test validator + let slots_per_epoch = 32; + let test_validator = TestValidatorGenesis::default() + .fee_rate_governor(FeeRateGovernor::new(0, 0)) + .rent(Rent { + lamports_per_byte_year: 1, + exemption_threshold: 1.0, + ..Rent::default() + }) + .epoch_schedule(EpochSchedule::custom( + slots_per_epoch, + slots_per_epoch, + /* enable_warmup_epochs = */ false, + )) + .faucet_addr(Some(faucet_addr)) + .start_with_mint_address(mint_pubkey, SocketAddrSpace::Unspecified) + .expect("validator start failed"); + + let rpc_client = + RpcClient::new_with_commitment(test_validator.rpc_url(), CommitmentConfig::processed()); + let default_signer = Keypair::new(); + + let mut config = CliConfig::recent_for_tests(); + config.json_rpc_url = test_validator.rpc_url(); + config.signers = vec![&default_signer]; + + request_and_confirm_airdrop( + &rpc_client, + &config, + &config.signers[0].pubkey(), + 1_000_000_000_000, + ) + .unwrap(); + + // Create vote account + let vote_keypair = Keypair::new(); + config.signers = vec![&default_signer, &vote_keypair]; + config.command = CliCommand::CreateVoteAccount { + vote_account: 1, + seed: None, + identity_account: 0, + authorized_voter: None, + authorized_withdrawer, + commission: 0, + sign_only: false, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + nonce_account: None, + nonce_authority: 0, + memo: None, + fee_payer: 0, + compute_unit_price: None, + }; + process_command(&config).unwrap(); + + // Create second vote account + let vote2_keypair = Keypair::new(); + config.signers = vec![&default_signer, &vote2_keypair]; + config.command = CliCommand::CreateVoteAccount { + vote_account: 1, + seed: None, + identity_account: 0, + authorized_voter: None, + authorized_withdrawer, + commission: 0, + sign_only: false, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + nonce_account: None, + nonce_authority: 0, + memo: None, + fee_payer: 0, + compute_unit_price: None, + }; + process_command(&config).unwrap(); + + // Create stake account + let stake_keypair = Keypair::new(); + config.signers = vec![&default_signer, &stake_keypair]; + config.command = CliCommand::CreateStakeAccount { + stake_account: 1, + seed: None, + staker: None, + withdrawer: None, + withdrawer_signer: None, + lockup: Lockup::default(), + amount: SpendAmount::Some(50_000_000_000), + sign_only: false, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + nonce_account: None, + nonce_authority: 0, + memo: None, + fee_payer: 0, + from: 0, + compute_unit_price: None, + }; + process_command(&config).unwrap(); + + // Delegate stake to `vote_keypair` + config.signers = vec![&default_signer]; + config.command = CliCommand::DelegateStake { + stake_account_pubkey: stake_keypair.pubkey(), + vote_account_pubkey: vote_keypair.pubkey(), + stake_authority: 0, + force: true, + sign_only: false, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::default(), + nonce_account: None, + nonce_authority: 0, + memo: None, + fee_payer: 0, + redelegation_stake_account: None, + compute_unit_price: None, + }; + process_command(&config).unwrap(); + + // wait for new epoch + wait_for_next_epoch_plus_n_slots(&rpc_client, 0); + + // `stake_keypair` should now be delegated to `vote_keypair` and fully activated + let stake_account = rpc_client.get_account(&stake_keypair.pubkey()).unwrap(); + let stake_state: StakeState = stake_account.state().unwrap(); + + let rent_exempt_reserve = match stake_state { + StakeState::Stake(meta, stake, _) => { + assert_eq!(stake.delegation.voter_pubkey, vote_keypair.pubkey()); + meta.rent_exempt_reserve + } + _ => panic!("Unexpected stake state!"), + }; + + assert_eq!( + rpc_client + .get_stake_activation(stake_keypair.pubkey(), None) + .unwrap(), + RpcStakeActivation { + state: StakeActivationState::Active, + active: 50_000_000_000 - rent_exempt_reserve, + inactive: 0 + } + ); + check_balance!(50_000_000_000, &rpc_client, &stake_keypair.pubkey()); + + // Now try to withdraw from stake_account. It will fail. + let recipient = keypair_from_seed(&[5u8; 32]).unwrap(); + let recipient_pubkey = recipient.pubkey(); + config.command = CliCommand::WithdrawStake { + stake_account_pubkey: stake_keypair.pubkey(), + destination_account_pubkey: recipient_pubkey, + amount: SpendAmount::Some(50_000_000_000), + withdraw_authority: 0, + custodian: None, + sign_only: false, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + nonce_account: None, + nonce_authority: 0, + memo: None, + seed: None, + fee_payer: 0, + compute_unit_price: None, + }; + + let withdraw_result = process_command(&config); + if let Err(e) = withdraw_result { + let s = format!("{}", e); + assert!(s.contains("insufficient funds for instruction")); + } else { + unreachable!(); + } + + // Setup `stake2_keypair. Add an extra `rent_exempt_reserve` amount into `stake2_keypair` + // before redelegation to account for the `rent_exempt_reserve` balance that'll be pealed off + // the stake during the redelegation process. + let stake2_keypair = Keypair::new(); + request_and_confirm_airdrop( + &rpc_client, + &config, + &stake2_keypair.pubkey(), + rent_exempt_reserve, + ) + .unwrap(); + + // Redelegate to `vote2_keypair` via `stake2_keypair + config.signers = vec![&default_signer, &stake2_keypair]; + config.command = CliCommand::DelegateStake { + stake_account_pubkey: stake_keypair.pubkey(), + vote_account_pubkey: vote2_keypair.pubkey(), + stake_authority: 0, + force: true, + sign_only: false, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::default(), + nonce_account: None, + nonce_authority: 0, + memo: None, + fee_payer: 0, + redelegation_stake_account: Some(1), + compute_unit_price: None, + }; + process_command(&config).unwrap(); + + // `stake_keypair` should now be deactivating + assert_eq!( + rpc_client + .get_stake_activation(stake_keypair.pubkey(), None) + .unwrap(), + RpcStakeActivation { + state: StakeActivationState::Deactivating, + active: 50_000_000_000 - rent_exempt_reserve, + inactive: 0, + } + ); + + // `stake_keypair2` should now be activating + assert_eq!( + rpc_client + .get_stake_activation(stake2_keypair.pubkey(), None) + .unwrap(), + RpcStakeActivation { + state: StakeActivationState::Activating, + active: 0, + inactive: 50_000_000_000 - rent_exempt_reserve, + } + ); + + // check that all the stake, save `rent_exempt_reserve`, have been moved from `stake_keypair` + // to `stake2_keypair` + check_balance!(rent_exempt_reserve, &rpc_client, &stake_keypair.pubkey()); + check_balance!(50_000_000_000, &rpc_client, &stake2_keypair.pubkey()); + + // Create stake merge account + let stake_merge_keypair = Keypair::new(); + config.signers = vec![&default_signer, &stake_merge_keypair]; + config.command = CliCommand::CreateStakeAccount { + stake_account: 1, + seed: None, + staker: None, + withdrawer: None, + withdrawer_signer: None, + lockup: Lockup::default(), + amount: SpendAmount::Some(50_000_000_000), + sign_only: false, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + nonce_account: None, + nonce_authority: 0, + memo: None, + fee_payer: 0, + from: 0, + compute_unit_price: None, + }; + process_command(&config).unwrap(); + + // Delegate stake_merge to `vote_keypair2` + config.signers = vec![&default_signer]; + config.command = CliCommand::DelegateStake { + stake_account_pubkey: stake_merge_keypair.pubkey(), + vote_account_pubkey: vote2_keypair.pubkey(), + stake_authority: 0, + force: true, + sign_only: false, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::default(), + nonce_account: None, + nonce_authority: 0, + memo: None, + fee_payer: 0, + redelegation_stake_account: None, + compute_unit_price: None, + }; + process_command(&config).unwrap(); + + // Deactivate stake_merge_account to test (Activating, Inactive) merge case + if deactivating_merge_stake { + config.signers = vec![&default_signer]; + + config.command = CliCommand::DeactivateStake { + stake_account_pubkey: stake_merge_keypair.pubkey(), + stake_authority: 0, + sign_only: false, + deactivate_delinquent: false, + dump_transaction_message: true, + blockhash_query: BlockhashQuery::default(), + nonce_account: None, + nonce_authority: 0, + memo: None, + seed: None, + fee_payer: 0, + compute_unit_price: None, + }; + process_command(&config).unwrap(); + } + + // merge stake_merge account to stake2_account should NOT clear the the `MustFullyActivateBeforeDeactivationIsPermitted` flag. + config.signers = vec![&default_signer]; + + config.command = CliCommand::MergeStake { + stake_account_pubkey: stake2_keypair.pubkey(), + source_stake_account_pubkey: stake_merge_keypair.pubkey(), + stake_authority: 0, + sign_only: false, + dump_transaction_message: true, + blockhash_query: BlockhashQuery::default(), + nonce_account: None, + nonce_authority: 0, + memo: None, + fee_payer: 0, + compute_unit_price: None, + }; + process_command(&config).unwrap(); + + // Deactivate stake2_account should fail because stake2_account is not fully activated. + config.signers = vec![&default_signer]; + + config.command = CliCommand::DeactivateStake { + stake_account_pubkey: stake2_keypair.pubkey(), + stake_authority: 0, + sign_only: false, + deactivate_delinquent: false, + dump_transaction_message: true, + blockhash_query: BlockhashQuery::default(), + nonce_account: None, + nonce_authority: 0, + memo: None, + seed: None, + fee_payer: 0, + compute_unit_price: None, + }; + let deactivate_result = process_command(&config); + if let Err(e) = deactivate_result { + let s = format!("{}", e); + assert_eq!( + s, + "redelegated stake must be fully activated before deactivation" + ); + } else { + unreachable!(); + } + + // `stake_keypair2` should still be Activating + assert_eq!( + rpc_client + .get_stake_activation(stake2_keypair.pubkey(), None) + .unwrap(), + RpcStakeActivation { + state: StakeActivationState::Activating, + active: 0, + inactive: 100_000_000_000 - rent_exempt_reserve, + } + ); + + // Withdraw from stake2 account should still fails + let recipient = keypair_from_seed(&[5u8; 32]).unwrap(); + let recipient_pubkey = recipient.pubkey(); + config.command = CliCommand::WithdrawStake { + stake_account_pubkey: stake2_keypair.pubkey(), + destination_account_pubkey: recipient_pubkey, + amount: SpendAmount::Some(50_000_000_000), + withdraw_authority: 0, + custodian: None, + sign_only: false, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + nonce_account: None, + nonce_authority: 0, + memo: None, + seed: None, + fee_payer: 0, + compute_unit_price: None, + }; + + let withdraw_result = process_command(&config); + if let Err(e) = withdraw_result { + let s = format!("{}", e); + assert!(s.contains("insufficient funds for instruction")); + } else { + unreachable!(); + } + + // wait for one epoch, now stake2_account should be fully activated. + wait_for_next_epoch_plus_n_slots(&rpc_client, 0); + + // Deactivate stake2_account should succeed. + config.signers = vec![&default_signer]; + + config.command = CliCommand::DeactivateStake { + stake_account_pubkey: stake2_keypair.pubkey(), + stake_authority: 0, + sign_only: false, + deactivate_delinquent: false, + dump_transaction_message: true, + blockhash_query: BlockhashQuery::default(), + nonce_account: None, + nonce_authority: 0, + memo: None, + seed: None, + fee_payer: 0, + compute_unit_price: None, + }; + let deactivate_result = process_command(&config); + assert!(deactivate_result.is_ok()); +} + +#[test] +fn test_stake_redelegation_then_merge_activating_stake_then_deactivation_not_permitted() { + test_stake_redelegation_then_merge_deactivation_not_permitted(false); +} + +#[test] +fn test_stake_redelegation_then_merge_inactive_stake_then_deactivation_not_permitted() { + test_stake_redelegation_then_merge_deactivation_not_permitted(true); +}